File size: 16,774 Bytes
54ab546
c6579d0
a799b96
dc606a1
3e38742
 
e40116e
 
52c998b
d1a8f64
 
 
 
 
52c998b
f06305d
 
8279124
468074f
f06305d
8279124
 
 
 
468074f
 
76b6487
c6579d0
e21a5c9
52c998b
57488cc
868b389
e21a5c9
 
 
 
f529a5c
54ab546
e46779b
c476b0e
54ab546
 
 
9eb2560
c476b0e
 
9eb2560
c476b0e
 
 
e46779b
44079b2
c476b0e
e46779b
aed4711
e46779b
e40116e
57488cc
e46779b
 
 
 
 
 
57488cc
e46779b
57488cc
 
e46779b
b5974f4
3e38742
e40116e
57488cc
aed4711
57488cc
 
 
aed4711
e46779b
57488cc
e46779b
 
 
 
 
 
 
 
 
 
57488cc
e46779b
57488cc
 
e46779b
57488cc
 
e46779b
aed4711
76b6487
44079b2
f06305d
 
93a86c3
44079b2
 
 
57488cc
3e38742
f06305d
57488cc
f06305d
868b389
f06305d
57488cc
868b389
f06305d
44079b2
57488cc
44079b2
57488cc
 
76b6487
f06305d
 
aed4711
f06305d
aed4711
f06305d
44079b2
 
 
 
e21a5c9
44079b2
 
 
cee8f1f
e40116e
 
 
44079b2
8279124
 
 
e46779b
868b389
 
dc606a1
 
57488cc
dc606a1
8279124
44079b2
aed4711
 
 
c6579d0
868b389
aed4711
57488cc
 
 
 
 
 
468074f
 
 
 
cee8f1f
468074f
 
e40116e
e21a5c9
 
 
 
 
dc606a1
e21a5c9
57488cc
 
dc606a1
 
 
 
e46779b
dc606a1
 
 
e21a5c9
dc606a1
57488cc
e46779b
dc606a1
 
 
e21a5c9
 
 
76b6487
468074f
e21a5c9
468074f
44079b2
a799b96
 
44079b2
a799b96
44079b2
a799b96
 
468074f
dc606a1
 
 
 
52c998b
 
 
e46779b
 
52c998b
 
e46779b
 
57488cc
 
 
52c998b
 
e46779b
52c998b
e46779b
 
 
57488cc
e46779b
 
57488cc
 
e46779b
52c998b
 
 
e46779b
57488cc
 
 
 
 
 
 
 
e46779b
57488cc
 
52c998b
 
 
 
e46779b
57488cc
e46779b
57488cc
52c998b
e46779b
57488cc
52c998b
57488cc
 
52c998b
 
 
 
 
57488cc
e46779b
57488cc
 
52c998b
57488cc
52c998b
dc606a1
 
52c998b
dc606a1
e46779b
52c998b
57488cc
 
e46779b
 
57488cc
 
 
 
 
 
52c998b
 
 
 
 
 
 
57488cc
 
52c998b
 
 
 
 
 
57488cc
52c998b
 
 
e46779b
 
57488cc
52c998b
 
 
 
57488cc
 
 
52c998b
 
 
 
 
e46779b
 
57488cc
52c998b
 
 
 
57488cc
52c998b
dc606a1
52c998b
 
dc606a1
52c998b
 
 
 
 
dc606a1
 
 
 
 
 
c6579d0
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
import os, asyncio, uuid, time, uvicorn, random, shutil, tempfile, httpx, re, json
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, UploadFile, File, BackgroundTasks
from fastapi.responses import JSONResponse, HTMLResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware
from faster_whisper import WhisperModel
from gtts import gTTS

# --- KONFIGURASI CACHE PERSISTEN ---
CACHE_DIR = "/home/user/app/models_cache"
os.makedirs(CACHE_DIR, exist_ok=True)
os.environ["HF_HOME"] = os.path.join(CACHE_DIR, "huggingface")
os.environ["OLLAMA_MODELS"] = os.path.join(CACHE_DIR, ".ollama")

# --- STRATEGI KONFIGURASI MODEL ---
MODEL_STRATEGY = {
    "deepseek": [
        {"provider": "ollama", "model": "deepseek-r1:7b"},
        {"provider": "openrouter", "model": "deepseek/deepseek-r1"}
    ],
    "qwen": [
        {"provider": "ollama", "model": "qwen2.5:7b-instruct"},
        {"provider": "openrouter", "model": "qwen/qwen-2.5-7b-instruct"}
    ],
    "gpt": [{"provider": "openrouter", "model": "openai/gpt-4o-mini"}],
    "gemini": [{"provider": "openrouter", "model": "google/gemini-2.0-flash"}]
}
QUEUE = asyncio.Queue()

# Load Whisper Base secara global
print(f"[*] Memuat model Whisper BASE dari {CACHE_DIR}...")
whisper_model = WhisperModel("base", device="cpu", compute_type="int8", download_root=CACHE_DIR)

def remove_file(path: str):
    if os.path.exists(path):
        os.remove(path)

def clean_json_output(text: str):
    """Pembersih JSON yang lebih agresif untuk menangani noise dari model reasoning"""
    if not text: return "[]"
    text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)
    text = re.sub(r'```json\s*', '', text)
    text = re.sub(r'```\s*', '', text)
    
    match = re.search(r'(\[.*\]|\{.*\})', text, re.DOTALL)
    if not match: return "[]"
    
    candidate = match.group(1).strip()
    try:
        data = json.loads(candidate)
        return json.dumps([data] if isinstance(data, dict) else data)
    except Exception:
        valid_match = re.search(r'(\[.*?\]|\{.*?\})', candidate, re.DOTALL)
        return clean_json_output(valid_match.group(1)) if valid_match else "[]"

# --- PROVIDERS DENGAN RETRY LOGIC ---
async def call_ollama(messages, model):
    print(f"[Ollama] Menghubungi model {model}...")
    for attempt in range(2):
        try:
            async with httpx.AsyncClient(timeout=httpx.Timeout(240.0, connect=10.0)) as client:
                res = await client.post("http://127.0.0.1:11434/api/chat", 
                                        json={"model": model, "messages": messages, "stream": False})
                if res.status_code == 200:
                    print(f"[Ollama] Berhasil mendapatkan respon dari {model}.")
                    return res.json()["message"]["content"]
        except Exception as e:
            print(f"[Ollama] Gagal (Percobaan {attempt+1}): {str(e)}")
            await asyncio.sleep(2)
    return None

async def call_openrouter(messages, model):
    print(f"[OpenRouter] Menghubungi model {model}...")
    api_key = os.getenv("OPENROUTER_KEYS")
    if not api_key: 
        print("[OpenRouter] API KEY tidak ditemukan!")
        return None
    keys = api_key.split(",")
    
    for i in range(3):
        try:
            async with httpx.AsyncClient(timeout=60.0) as client:
                res = await client.post("https://openrouter.ai/api/v1/chat/completions",
                    headers={
                        "Authorization": f"Bearer {random.choice(keys)}", 
                        "HTTP-Referer": "https://huggingface.co/",
                        "Content-Type": "application/json"
                    },
                    json={"model": model, "messages": messages})
                if res.status_code == 200:
                    print(f"[OpenRouter] Berhasil mendapatkan respon dari {model}.")
                    return res.json()["choices"][0]["message"]["content"]
                elif res.status_code == 429:
                    print(f"[OpenRouter] Rate limited, menunggu {2**i} detik...")
                    await asyncio.sleep(2 ** i)
        except Exception as e:
            print(f"[OpenRouter] Gagal: {str(e)}")
            await asyncio.sleep(1)
    return None

async def smart_router(messages, alias, is_json=False):
    strategies = MODEL_STRATEGY.get(alias, MODEL_STRATEGY["deepseek"])
    for attempt in strategies:
        prov, mod = attempt["provider"], attempt["model"]
        raw = await call_ollama(messages, mod) if prov == "ollama" else await call_openrouter(messages, mod)
        if raw:
            return clean_json_output(raw) if is_json else raw
    return "[]" if is_json else "Error: Semua provider (Ollama & OpenRouter) gagal merespon."

async def worker():
    print("[Worker] Antrean AI dimulai...")
    while True:
        messages, alias, is_json, future = await QUEUE.get()
        try:
            print(f"[Worker] Memproses permintaan untuk alias: {alias}")
            result = await smart_router(messages, alias, is_json)
            future.set_result(result)
        except Exception as e:
            print(f"[Worker] Error fatal: {str(e)}")
            future.set_exception(e)
        finally: 
            QUEUE.task_done()

@asynccontextmanager
async def lifespan(app: FastAPI):
    worker_task = asyncio.create_task(worker())
    yield
    worker_task.cancel()

app = FastAPI(title="AI Debug Gateway", lifespan=lifespan)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

# --- API ENDPOINTS ---

@app.get("/v1/models")
async def list_models():
    return {"object": "list", "data": [{"id": "qwen"}, {"id": "deepseek"}, {"id": "gpt"}, {"id": "gemini"}]}

@app.post("/v1/chat/completions")
async def chat(request: Request):
    body = await request.json()
    model_input = body.get("model", "qwen").lower()
    messages = body.get("messages", [])
    
    prompt_str = str(messages).lower()
    is_json = any(x in prompt_str for x in ["json", "highlight", "clipper", "short", "segment"])

    if is_json:
        messages.insert(0, {
            "role": "system", 
            "content": "You are a professional Video Editor AI. Respond ONLY with a JSON LIST of objects: [{'start': 'MM:SS', 'end': 'MM:SS', 'description': '...', 'reason': '...'}]. No conversational text."
        })

    alias = "qwen" if "qwen" in model_input else "deepseek"
    if "gpt" in model_input: alias = "gpt"
    elif "gemini" in model_input: alias = "gemini"

    future = asyncio.get_event_loop().create_future()
    await QUEUE.put((messages, alias, is_json, future))
    
    # Tunggu hasil dari worker
    try:
        result = await asyncio.wait_for(future, timeout=300.0) # Max 5 menit
    except asyncio.TimeoutError:
        result = "Error: Timeout 5 menit tercapai."

    return {
        "id": f"chatcmpl-{uuid.uuid4()}",
        "object": "chat.completion",
        "created": int(time.time()),
        "model": model_input,
        "choices": [{"message": {"role": "assistant", "content": result}, "finish_reason": "stop"}]
    }

@app.post("/v1/audio/transcriptions")
async def transcribe(background_tasks: BackgroundTasks, file: UploadFile = File(...)):
    tmp_path = os.path.join(tempfile.gettempdir(), f"{uuid.uuid4()}_{file.filename}")
    with open(tmp_path, "wb") as buffer:
        shutil.copyfileobj(file.file, buffer)
    
    try:
        print(f"[*] Transkripsi dimulai: {file.filename}")
        segments, info = whisper_model.transcribe(tmp_path, beam_size=5, language="id", vad_filter=True)
        
        segments_list = []
        full_text = ""
        for s in segments:
            segment_dict = {"start": round(s.start, 2), "end": round(s.end, 2), "text": s.text.strip()}
            segments_list.append(segment_dict)
            full_text += s.text.strip() + " "

        background_tasks.add_task(remove_file, tmp_path)
        return {
            "text": full_text.strip() or "[Tidak ada suara]",
            "segments": segments_list,
            "language": info.language,
            "duration": info.duration
        }
    except Exception as e:
        remove_file(tmp_path)
        return JSONResponse({"error": str(e)}, status_code=500)

@app.post("/v1/audio/speech")
async def tts(request: Request, background_tasks: BackgroundTasks):
    body = await request.json()
    text = body.get("input", "")
    tmp_path = os.path.join(tempfile.gettempdir(), f"{uuid.uuid4()}.mp3")
    try:
        gTTS(text=text, lang="id").save(tmp_path)
        background_tasks.add_task(remove_file, tmp_path)
        return FileResponse(tmp_path, media_type="audio/mpeg")
    except Exception as e:
        return JSONResponse({"error": str(e)}, status_code=500)

@app.get("/", response_class=HTMLResponse)
async def dashboard():
    return """
    <!DOCTYPE html>
    <html lang="id">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>IMBECOME DEBUGGER</title>
        <script src="https://cdn.tailwindcss.com"></script>
        <style>
            body { background: #0f172a; color: #e2e8f0; font-family: ui-sans-serif, system-ui; }
            .card { background: #1e293b; border: 1px solid #334155; border-radius: 12px; }
            #output { scroll-behavior: smooth; }
            .status-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
            @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }
        </style>
    </head>
    <body class="p-4 md:p-8">
        <div class="max-w-6xl mx-auto">
            <header class="flex flex-col md:flex-row justify-between items-center mb-8 gap-4 border-b border-slate-800 pb-6">
                <div>
                    <h1 class="text-3xl font-black text-sky-400">IMBECOME <span class="text-white">DEBUGGER</span></h1>
                    <p class="text-xs text-slate-500 mt-1">Gateway for YT Short Clipper</p>
                </div>
                <div class="flex items-center gap-3">
                    <div id="loadingIndicator" class="hidden status-pulse px-3 py-1 bg-amber-500/10 text-amber-400 text-[10px] font-bold rounded-full border border-amber-500/20">PROCESSING...</div>
                    <div class="px-3 py-1 bg-emerald-500/10 text-emerald-400 text-xs font-bold rounded-full border border-emerald-500/20">READY</div>
                </div>
            </header>

            <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
                <div class="card p-6">
                    <h2 class="font-bold mb-4 flex items-center gap-2">LLM Tester</h2>
                    <select id="modelSelect" class="w-full bg-slate-900 border border-slate-700 text-sm p-2 rounded mb-4 outline-none">
                        <option value="qwen">Qwen 2.5 (7B)</option>
                        <option value="deepseek">DeepSeek R1 (7B)</option>
                        <option value="gemini">Gemini Flash</option>
                        <option value="gpt">GPT-4o Mini</option>
                    </select>
                    <textarea id="chatInput" rows="5" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm mb-4 outline-none" placeholder="Masukkan teks..."></textarea>
                    <div class="grid grid-cols-2 gap-3">
                        <button onclick="runTest('chat')" class="bg-slate-700 hover:bg-slate-600 py-2 rounded-lg text-xs font-bold transition">Chat</button>
                        <button onclick="runTest('json')" class="bg-sky-600 hover:bg-sky-500 py-2 rounded-lg text-xs font-bold transition">Extract Highlight</button>
                    </div>
                </div>

                <div class="space-y-6">
                    <div class="card p-6">
                        <h2 class="font-bold mb-4">Whisper STT</h2>
                        <input type="file" id="audioFile" class="text-xs w-full mb-4 text-slate-400">
                        <button onclick="runWhisper()" class="w-full bg-emerald-600 hover:bg-emerald-500 py-2 rounded-lg text-xs font-bold transition">Transcribe</button>
                    </div>
                    <div class="card p-6">
                        <h2 class="font-bold mb-4">gTTS Tester</h2>
                        <div class="flex gap-2">
                            <input id="ttsText" type="text" class="flex-1 bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm outline-none" value="Cek sound clipper.">
                            <button onclick="runTTS()" class="bg-orange-600 hover:bg-orange-500 px-4 py-2 rounded-lg text-xs font-bold transition">Play</button>
                        </div>
                        <audio id="audioPlayer" controls class="w-full mt-4 hidden h-8"></audio>
                    </div>
                </div>

                <div class="lg:col-span-2 card">
                    <div class="bg-slate-800/50 px-6 py-3 border-b border-slate-700 flex justify-between items-center">
                        <h2 class="text-sm font-bold text-slate-400">Console Log</h2>
                        <button onclick="document.getElementById('output').innerText = ''" class="text-[10px] text-slate-500 hover:text-white uppercase">Clear</button>
                    </div>
                    <pre id="output" class="p-6 h-80 overflow-y-auto text-[11px] text-emerald-400 font-mono leading-relaxed bg-black/20 flex flex-col-reverse"></pre>
                </div>
            </div>
        </div>

        <script>
            const log = (msg, isData = false) => {
                const out = document.getElementById('output');
                const div = document.createElement('div');
                div.className = 'mb-1 border-l-2 border-slate-700 pl-2 py-1';
                const time = new Date().toLocaleTimeString();
                const content = isData ? JSON.stringify(msg, null, 2) : msg;
                div.innerHTML = `<span class="text-slate-500">[${time}]</span> <span class="${isData ? 'text-sky-300' : ''}">${content}</span>`;
                out.prepend(div);
            };

            const toggleLoading = (show) => {
                document.getElementById('loadingIndicator').classList.toggle('hidden', !show);
            };

            async function runTest(type) {
                const model = document.getElementById('modelSelect').value;
                const text = document.getElementById('chatInput').value;
                if(!text) return alert('Input kosong!');

                log(`Memulai ${type} (${model})...`);
                toggleLoading(true);
                try {
                    const res = await fetch('/v1/chat/completions', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify({
                            model: model,
                            messages: [{role: 'user', content: type === 'json' ? "extract highlight: " + text : text}]
                        })
                    });
                    const data = await res.json();
                    log(data, true);
                } catch (err) { log("Error: " + err.message); }
                finally { toggleLoading(false); }
            }

            async function runWhisper() {
                const file = document.getElementById('audioFile').files[0];
                if(!file) return alert('Pilih file!');
                log("Transkripsi dimulai...");
                toggleLoading(true);
                const fd = new FormData();
                fd.append('file', file);
                try {
                    const res = await fetch('/v1/audio/transcriptions', { method: 'POST', body: fd });
                    const data = await res.json();
                    log(data, true);
                } catch (err) { log("Whisper Error: " + err.message); }
                finally { toggleLoading(false); }
            }

            async function runTTS() {
                const text = document.getElementById('ttsText').value;
                log("Memutar suara...");
                const res = await fetch('/v1/audio/speech', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({ input: text })
                });
                const blob = await res.blob();
                const player = document.getElementById('audioPlayer');
                player.src = URL.createObjectURL(blob);
                player.classList.remove('hidden');
                player.play();
            }
        </script>
    </body>
    </html>
    """

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=7860)