Update app.py
Browse files
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 =
|
| 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
|
| 42 |
-
|
| 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.
|
| 54 |
-
|
| 55 |
-
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
| 68 |
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
# --- PROVIDERS ---
|
| 72 |
async def call_ollama(messages, model):
|
|
|
|
| 73 |
try:
|
| 74 |
-
async with httpx.AsyncClient(timeout=
|
| 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:
|
|
|
|
| 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 "
|
| 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 |
-
|
| 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 |
-
#
|
| 298 |
-
messages.
|
| 299 |
"role": "system",
|
| 300 |
-
"content": "You are a JSON generator. Respond ONLY with a
|
| 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:
|