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'.*?', '', 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 """ IMBECOME DEBUGGER

IMBECOME DEBUGGER

Gateway for YT Short Clipper

READY

LLM Tester

Whisper STT

gTTS Tester

Console Log


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