imbecome commited on
Commit
c476b0e
·
verified ·
1 Parent(s): 9eb2560

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +37 -189
app.py CHANGED
@@ -25,7 +25,7 @@ MODEL_STRATEGY = {
25
  "gpt": [{"provider": "openrouter", "model": "openai/gpt-4o-mini"}],
26
  "gemini": [{"provider": "openrouter", "model": "google/gemini-2.0-flash"}]
27
  }
28
- TIMEOUT = 300
29
  QUEUE = asyncio.Queue()
30
 
31
  # Load Whisper Base
@@ -38,45 +38,54 @@ def remove_file(path: str):
38
 
39
  def clean_json_output(text: str):
40
  """
41
- Membersihkan output AI agar menjadi JSON murni yang valid untuk Clipper.
42
- Menangani error 'Extra data' dengan memotong teks setelah objek JSON pertama.
43
  """
44
- if not text: return "{}"
45
 
46
- # 1. Hapus tag <think> (khusus DeepSeek)
47
  text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)
48
 
49
  # 2. Hapus markdown code blocks
50
  text = re.sub(r'```json\s*', '', text)
51
  text = re.sub(r'```\s*', '', text)
52
 
53
- # 3. Cari kurung kurawal pertama '{' dan rfind '}'
54
- start_idx = text.find('{')
55
- end_idx = text.rfind('}')
56
 
57
- if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
58
- candidate = text[start_idx:end_idx+1].strip()
59
- try:
60
- # Coba validasi JSON
61
- json.loads(candidate)
62
- return candidate
63
- except json.JSONDecodeError as e:
64
- # Jika ada "Extra data", potong string tepat di posisi error
65
- if "Extra data" in str(e):
66
- return candidate[:e.pos].strip()
67
- return candidate
 
68
 
69
- return text.strip()
 
 
 
 
 
 
70
 
71
  # --- PROVIDERS ---
72
  async def call_ollama(messages, model):
 
73
  try:
74
- async with httpx.AsyncClient(timeout=120) as client:
75
  res = await client.post("http://127.0.0.1:11434/api/chat",
76
  json={"model": model, "messages": messages, "stream": False})
77
  if res.status_code == 200:
78
  return res.json()["message"]["content"]
79
- except: pass
 
80
  return None
81
 
82
  async def call_openrouter(messages, model):
@@ -102,7 +111,7 @@ async def smart_router(messages, alias, is_json_request=False):
102
  if is_json_request:
103
  return clean_json_output(raw_res)
104
  return raw_res
105
- return "{}" if is_json_request else "Maaf, AI gagal merespon. Cek koneksi Ollama atau API Key."
106
 
107
  async def worker():
108
  while True:
@@ -122,167 +131,9 @@ async def lifespan(app: FastAPI):
122
  app = FastAPI(title="AI Gateway", lifespan=lifespan)
123
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], allow_credentials=True)
124
 
125
- # --- STARTUP UI PLAYGROUND ---
126
- @app.get("/", response_class=HTMLResponse)
127
  async def home():
128
- return """
129
- <!DOCTYPE html>
130
- <html lang="id">
131
- <head>
132
- <meta charset="UTF-8">
133
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
134
- <title>Imbecome AI Infrastructure</title>
135
- <script src="https://cdn.tailwindcss.com"></script>
136
- <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;800&display=swap" rel="stylesheet">
137
- <style>
138
- body { background-color: #020617; color: #f8fafc; font-family: 'Plus Jakarta Sans', sans-serif; background-image: radial-gradient(circle at 50% 0%, #1e1b4b 0%, #020617 70%); }
139
- .glass { background: rgba(30, 41, 59, 0.5); backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.1); }
140
- .neon-border:focus { border-color: #38bdf8; box-shadow: 0 0 15px rgba(56, 189, 248, 0.3); }
141
- .btn-gradient { background: linear-gradient(135deg, #38bdf8 0%, #818cf8 100%); transition: all 0.2s; }
142
- .btn-gradient:hover { transform: translateY(-2px); filter: brightness(1.1); }
143
- .status-dot { width: 8px; height: 8px; background: #4ade80; border-radius: 50%; box-shadow: 0 0 10px #4ade80; }
144
- </style>
145
- </head>
146
- <body class="min-h-screen p-6">
147
- <div class="max-w-6xl mx-auto">
148
- <nav class="flex justify-between items-center mb-12 glass p-4 rounded-2xl">
149
- <div class="flex items-center gap-2">
150
- <div class="bg-sky-500 w-8 h-8 rounded-lg flex items-center justify-center font-bold">I</div>
151
- <span class="text-xl font-extrabold tracking-tight text-white">IMBECOME <span class="text-sky-400">AI</span></span>
152
- </div>
153
- <div class="flex items-center gap-4 text-sm text-slate-400">
154
- <div class="status-dot"></div> Gateway Online
155
- </div>
156
- </nav>
157
-
158
- <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
159
- <div class="lg:col-span-2 glass rounded-3xl p-8 flex flex-col">
160
- <div class="flex justify-between items-center mb-6">
161
- <h3 class="text-xl font-bold flex items-center gap-2 text-white">Smart AI Chat</h3>
162
- <select id="chatModel" class="bg-slate-900 border border-slate-700 text-sm rounded-xl px-3 py-1 outline-none neon-border text-white">
163
- <option value="deepseek">DeepSeek R1 (7b)</option>
164
- <option value="qwen">Qwen 2.5 (7b)</option>
165
- <option value="gpt">GPT-4o Mini</option>
166
- <option value="gemini">Gemini 2.0</option>
167
- </select>
168
- </div>
169
- <div id="chatBox" class="bg-slate-900/50 border border-slate-800 rounded-2xl p-4 mb-4 overflow-y-auto h-[350px] text-sm space-y-4 text-white"></div>
170
- <form onsubmit="testChat(event)" class="relative">
171
- <input id="chatInput" type="text" placeholder="Tanya sesuatu..." class="w-full bg-slate-900 border border-slate-700 rounded-2xl px-5 py-4 pr-16 outline-none neon-border text-white">
172
- <button type="submit" id="btnChat" class="absolute right-2 top-2 bottom-2 bg-sky-500 px-4 rounded-xl font-bold btn-gradient text-white">Kirim</button>
173
- </form>
174
- </div>
175
-
176
- <div class="space-y-8">
177
- <div class="glass rounded-3xl p-6">
178
- <h3 class="text-lg font-bold mb-4 text-white">Voice Engine (TTS)</h3>
179
- <textarea id="ttsInput" rows="2" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-3 text-sm outline-none mb-3 text-white" placeholder="Teks ke suara..."></textarea>
180
- <button onclick="testTTS()" id="btnTTS" class="w-full bg-emerald-500 py-3 rounded-xl font-bold btn-gradient text-white">Generate Suara</button>
181
- <div id="ttsPlayer" class="hidden mt-4">
182
- <audio id="audioElement" controls class="w-full h-8"></audio>
183
- </div>
184
- </div>
185
-
186
- <div class="glass rounded-3xl p-6">
187
- <h3 class="text-lg font-bold mb-4 text-white">Whisper AI (STT)</h3>
188
- <input type="file" id="whisperFile" class="text-xs text-slate-400 mb-4 w-full">
189
- <button onclick="testWhisper()" id="btnWhisper" class="w-full bg-purple-600 py-3 rounded-xl font-bold btn-gradient text-white">Transkripsi (Base)</button>
190
- <div id="whisperResult" class="hidden mt-4 p-3 bg-slate-900 rounded-xl border border-slate-800 text-xs text-slate-300 max-h-32 overflow-y-auto"></div>
191
- </div>
192
- </div>
193
- </div>
194
- </div>
195
-
196
- <script>
197
- async function testChat(e) {
198
- if(e) e.preventDefault();
199
- const input = document.getElementById('chatInput');
200
- const box = document.getElementById('chatBox');
201
- const btn = document.getElementById('btnChat');
202
- if(!input.value || btn.disabled) return;
203
- const userMsg = input.value;
204
- input.value = '';
205
- btn.disabled = true;
206
- btn.innerText = '...';
207
-
208
- const userDiv = document.createElement('div');
209
- userDiv.className = 'ml-auto bg-sky-500/20 p-3 rounded-2xl max-w-[80%] text-right border border-sky-500/30 text-white';
210
- userDiv.innerText = userMsg;
211
- box.appendChild(userDiv);
212
- box.scrollTop = box.scrollHeight;
213
-
214
- const botDiv = document.createElement('div');
215
- botDiv.className = 'mr-auto bg-slate-800 p-3 rounded-2xl max-w-[80%] border border-slate-700 text-white animate-pulse';
216
- botDiv.innerText = 'Sedang berpikir...';
217
- box.appendChild(botDiv);
218
- box.scrollTop = box.scrollHeight;
219
-
220
- try {
221
- const res = await fetch('/v1/chat/completions', {
222
- method: 'POST',
223
- headers: {'Content-Type': 'application/json'},
224
- body: JSON.stringify({
225
- model: document.getElementById('chatModel').value,
226
- messages: [{role: 'user', content: userMsg}]
227
- })
228
- });
229
- if (!res.ok) throw new Error('API Error ' + res.status);
230
- const data = await res.json();
231
- botDiv.classList.remove('animate-pulse');
232
- botDiv.innerText = data.choices[0].message.content;
233
- } catch (err) {
234
- botDiv.classList.remove('animate-pulse');
235
- botDiv.className = 'mr-auto bg-red-900/20 p-3 rounded-2xl max-w-[80%] border border-red-500/30 text-red-200';
236
- botDiv.innerText = 'Kesalahan: ' + err.message;
237
- } finally {
238
- btn.disabled = false;
239
- btn.innerText = 'Kirim';
240
- box.scrollTop = box.scrollHeight;
241
- }
242
- }
243
-
244
- async function testTTS() {
245
- const res = await fetch('/v1/audio/speech', {
246
- method: 'POST',
247
- headers: {'Content-Type': 'application/json'},
248
- body: JSON.stringify({ input: document.getElementById('ttsInput').value, voice: 'id' })
249
- });
250
- const blob = await res.blob();
251
- document.getElementById('audioElement').src = URL.createObjectURL(blob);
252
- document.getElementById('ttsPlayer').classList.remove('hidden');
253
- }
254
-
255
- async function testWhisper() {
256
- const file = document.getElementById('whisperFile').files[0];
257
- if(!file) return;
258
- document.getElementById('whisperResult').classList.remove('hidden');
259
- document.getElementById('whisperResult').innerText = "Sedang memproses...";
260
- const fd = new FormData();
261
- fd.append('file', file);
262
- const res = await fetch('/v1/audio/transcriptions', { method: 'POST', body: fd });
263
- const data = await res.json();
264
- document.getElementById('whisperResult').innerText = data.text || "Gagal transkripsi.";
265
- }
266
- </script>
267
- </body>
268
- </html>
269
- """
270
-
271
- # --- API ENDPOINTS ---
272
-
273
- @app.get("/v1/models")
274
- async def list_models():
275
- return {
276
- "object": "list",
277
- "data": [
278
- {"id": "deepseek", "object": "model", "owned_by": "ollama"},
279
- {"id": "qwen", "object": "model", "owned_by": "ollama"},
280
- {"id": "gpt", "object": "model", "owned_by": "openrouter"},
281
- {"id": "gemini", "object": "model", "owned_by": "openrouter"},
282
- {"id": "whisper-1", "object": "model", "owned_by": "openai-style"},
283
- {"id": "tts-1", "object": "model", "owned_by": "openai-style"}
284
- ]
285
- }
286
 
287
  @app.post("/v1/chat/completions")
288
  async def chat(request: Request):
@@ -294,10 +145,10 @@ async def chat(request: Request):
294
  is_json = "json" in prompt_str or "highlights" in prompt_str
295
 
296
  if is_json:
297
- # Tambahkan instruksi sistem yang SANGAT KETAT untuk Qwen/DeepSeek
298
- messages.append({
299
  "role": "system",
300
- "content": "You are a JSON generator. Respond ONLY with a valid JSON object. Do not include any text before or after the JSON. Do not include thinking tags. If you cannot fulfill the request, return {}."
301
  })
302
 
303
  alias = "deepseek"
@@ -323,7 +174,6 @@ async def transcribe(background_tasks: BackgroundTasks, file: UploadFile = File(
323
  with open(tmp_path, "wb") as buffer:
324
  shutil.copyfileobj(file.file, buffer)
325
  try:
326
- # Optimasi VAD filter untuk suara pelan (min_speech_duration_ms=500)
327
  segments, _ = whisper_model.transcribe(
328
  tmp_path,
329
  beam_size=5,
@@ -333,8 +183,6 @@ async def transcribe(background_tasks: BackgroundTasks, file: UploadFile = File(
333
  )
334
  text = " ".join([s.text for s in segments])
335
  background_tasks.add_task(remove_file, tmp_path)
336
-
337
- # Placeholder jika hasil tetap kosong agar Clipper tidak melempar Exception
338
  final_text = text.strip() if text.strip() else "[Suara tidak terdeteksi]"
339
  return {"text": final_text}
340
  except Exception as e:
 
25
  "gpt": [{"provider": "openrouter", "model": "openai/gpt-4o-mini"}],
26
  "gemini": [{"provider": "openrouter", "model": "google/gemini-2.0-flash"}]
27
  }
28
+ TIMEOUT = 600
29
  QUEUE = asyncio.Queue()
30
 
31
  # Load Whisper Base
 
38
 
39
  def clean_json_output(text: str):
40
  """
41
+ Membersihkan output AI dan memastikan formatnya adalah LIST [ {...} ].
42
+ Mencegah error 'string indices must be integers' di Clipper.
43
  """
44
+ if not text: return "[]"
45
 
46
+ # 1. Hapus tag <think>
47
  text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)
48
 
49
  # 2. Hapus markdown code blocks
50
  text = re.sub(r'```json\s*', '', text)
51
  text = re.sub(r'```\s*', '', text)
52
 
53
+ # 3. Ekstrak pola JSON (mencari yang diawali [ atau {)
54
+ match = re.search(r'(\[.*\]|\{.*\})', text, re.DOTALL)
55
+ if not match: return "[]"
56
 
57
+ candidate = match.group(1).strip()
58
+
59
+ try:
60
+ data = json.loads(candidate)
61
+ # SINKRONISASI: Jika AI mengirim satu objek {}, bungkus jadi daftar [{}]
62
+ # Ini adalah kunci untuk memperbaiki error 'TypeError' Anda
63
+ if isinstance(data, dict):
64
+ return json.dumps([data])
65
+
66
+ # Jika data sudah dalam bentuk list [], kembalikan apa adanya
67
+ if isinstance(data, list):
68
+ return json.dumps(data)
69
 
70
+ return "[]"
71
+ except json.JSONDecodeError as e:
72
+ # Jika ada 'Extra data', ambil bagian yang valid saja
73
+ valid_match = re.search(r'(\[.*?\]|\{.*?\})', candidate, re.DOTALL)
74
+ if valid_match:
75
+ return clean_json_output(valid_match.group(1))
76
+ return "[]"
77
 
78
  # --- PROVIDERS ---
79
  async def call_ollama(messages, model):
80
+ print(f"DEBUG: Calling Ollama ({model})...")
81
  try:
82
+ async with httpx.AsyncClient(timeout=600.0) as client:
83
  res = await client.post("http://127.0.0.1:11434/api/chat",
84
  json={"model": model, "messages": messages, "stream": False})
85
  if res.status_code == 200:
86
  return res.json()["message"]["content"]
87
+ except Exception as e:
88
+ print(f"DEBUG: Ollama Error: {str(e)}")
89
  return None
90
 
91
  async def call_openrouter(messages, model):
 
111
  if is_json_request:
112
  return clean_json_output(raw_res)
113
  return raw_res
114
+ return "[]" if is_json_request else "Gagal merespon."
115
 
116
  async def worker():
117
  while True:
 
131
  app = FastAPI(title="AI Gateway", lifespan=lifespan)
132
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], allow_credentials=True)
133
 
134
+ @app.get("/")
 
135
  async def home():
136
+ return HTMLResponse("<h1>AI Gateway Active - Dual Core Optimized</h1>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
  @app.post("/v1/chat/completions")
139
  async def chat(request: Request):
 
145
  is_json = "json" in prompt_str or "highlights" in prompt_str
146
 
147
  if is_json:
148
+ # Perintah sistem yang sangat spesifik agar AI selalu memberikan LIST
149
+ messages.insert(0, {
150
  "role": "system",
151
+ "content": "You are a JSON generator. Respond ONLY with a LIST of objects inside square brackets: [{\"title\":...}]. No explanation."
152
  })
153
 
154
  alias = "deepseek"
 
174
  with open(tmp_path, "wb") as buffer:
175
  shutil.copyfileobj(file.file, buffer)
176
  try:
 
177
  segments, _ = whisper_model.transcribe(
178
  tmp_path,
179
  beam_size=5,
 
183
  )
184
  text = " ".join([s.text for s in segments])
185
  background_tasks.add_task(remove_file, tmp_path)
 
 
186
  final_text = text.strip() if text.strip() else "[Suara tidak terdeteksi]"
187
  return {"text": final_text}
188
  except Exception as e: