Spaces:
Sleeping
Sleeping
Heebin Moon
Claude Opus 4.6
Switch default LLM to Gemini Flash-Lite and fix SSE for Cloudflare
97fa2d2 Download web.py from heebin/YoutubeScript: direct link, hf CLI and curl.
- Browser
- Download file 8.14 kB
-
https://huggingface.co/spaces/heebin/YoutubeScript/resolve/main/web.py
- Command line
-
hf download hf://spaces/heebin/YoutubeScript/web.py
-
curl -L -o web.py https://huggingface.co/spaces/heebin/YoutubeScript/resolve/main/web.py
8.14 kB
| #!/usr/bin/env python3 | |
| """YouTube Script Extractor - Web Server.""" | |
| import asyncio | |
| import json | |
| import os | |
| import uuid | |
| from pathlib import Path | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| from fastapi import FastAPI, Request | |
| from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.templating import Jinja2Templates | |
| from transcriber import process_video | |
| from formatter import LLM_MODELS, get_models_sorted, get_languages | |
| app = FastAPI(title="YouTube Script Extractor") | |
| templates = Jinja2Templates(directory="templates") | |
| OUTPUT_DIR = "./output" | |
| os.makedirs(OUTPUT_DIR, exist_ok=True) | |
| # μ§ν μ€μΈ μμ μΆμ | |
| jobs: dict[str, dict] = {} | |
| async def index(request: Request): | |
| return templates.TemplateResponse("index.html", {"request": request}) | |
| async def get_llm_models(sort: str = "price"): | |
| """LLM λͺ¨λΈ λͺ©λ‘μ μ λ ¬νμ¬ λ°ννλ€.""" | |
| models = get_models_sorted(sort_by=sort) | |
| return {"models": models} | |
| async def get_supported_languages(): | |
| """λ²μ μ§μ μΈμ΄ λͺ©λ‘μ λ°ννλ€.""" | |
| return {"languages": get_languages()} | |
| async def get_vision_models(sort: str = "price"): | |
| """Vision μ§μ LLM λͺ¨λΈ λͺ©λ‘μ λ°ννλ€.""" | |
| key = "price_rank" if sort == "price" else "quality_rank" | |
| models = [ | |
| {"id": k, **v} for k, v in LLM_MODELS.items() | |
| if v.get("supports_vision", False) | |
| ] | |
| return {"models": sorted(models, key=lambda x: x[key])} | |
| async def start_transcription(request: Request): | |
| body = await request.json() | |
| url = body.get("url", "").strip() | |
| mode = body.get("mode", "api") | |
| api_key = body.get("api_key", "") or os.environ.get("OPENAI_API_KEY", "") | |
| model_size = body.get("model_size", "base") | |
| formats = body.get("formats", ["txt", "srt"]) | |
| output_dir = body.get("output_dir", "").strip() or OUTPUT_DIR | |
| # LLM μ΅μ (μ μ κΈ°λ³Έκ°) | |
| md_llm = body.get("md_llm", "") | |
| md_api_key = body.get("md_api_key", "") | |
| md_ollama_model = body.get("md_ollama_model", "llama3.2") | |
| translate_lang = body.get("translate_lang", "") | |
| # λ¨κ³λ³ LLM μ€λ²λΌμ΄λ | |
| format_llm = body.get("format_llm", "") | |
| format_api_key = body.get("format_api_key", "") | |
| translate_llm = body.get("translate_llm", "") | |
| translate_api_key = body.get("translate_api_key", "") | |
| keyframe_llm = body.get("keyframe_llm", "") | |
| keyframe_api_key = body.get("keyframe_api_key", "") | |
| # ν€νλ μ μ΅μ | |
| enable_keyframes = body.get("enable_keyframes", False) | |
| keyframe_method = body.get("keyframe_method", "scene") | |
| keyframe_interval = body.get("keyframe_interval", 30) | |
| if not url: | |
| return {"error": "YouTube URLμ μ λ ₯ν΄μ£ΌμΈμ."} | |
| if mode == "api" and not api_key: | |
| return {"error": "API λͺ¨λμμλ OpenAI API ν€κ° νμν©λλ€."} | |
| job_id = str(uuid.uuid4())[:8] | |
| jobs[job_id] = {"status": "started", "progress": [], "result": None, "error": None} | |
| asyncio.create_task( | |
| _run_transcription( | |
| job_id, url, mode, api_key, model_size, formats, output_dir, | |
| md_llm=md_llm, md_api_key=md_api_key, md_ollama_model=md_ollama_model, | |
| translate_lang=translate_lang, | |
| format_llm=format_llm, format_api_key=format_api_key, | |
| translate_llm=translate_llm, translate_api_key=translate_api_key, | |
| keyframe_llm=keyframe_llm, keyframe_api_key=keyframe_api_key, | |
| enable_keyframes=enable_keyframes, | |
| keyframe_method=keyframe_method, keyframe_interval=keyframe_interval, | |
| ) | |
| ) | |
| return {"job_id": job_id} | |
| async def _run_transcription( | |
| job_id: str, | |
| url: str, | |
| mode: str, | |
| api_key: str, | |
| model_size: str, | |
| formats: list[str], | |
| output_dir: str = OUTPUT_DIR, | |
| md_llm: str = "", | |
| md_api_key: str = "", | |
| md_ollama_model: str = "llama3.2", | |
| translate_lang: str = "", | |
| format_llm: str = "", | |
| format_api_key: str = "", | |
| translate_llm: str = "", | |
| translate_api_key: str = "", | |
| keyframe_llm: str = "", | |
| keyframe_api_key: str = "", | |
| enable_keyframes: bool = False, | |
| keyframe_method: str = "scene", | |
| keyframe_interval: int = 30, | |
| ): | |
| def on_progress(msg: str): | |
| jobs[job_id]["progress"].append(msg) | |
| try: | |
| result = await asyncio.to_thread( | |
| process_video, | |
| url=url, | |
| mode=mode, | |
| api_key=api_key, | |
| model_size=model_size, | |
| output_dir=output_dir, | |
| formats=formats, | |
| on_progress=on_progress, | |
| md_llm=md_llm or None, | |
| md_api_key=md_api_key or None, | |
| md_ollama_model=md_ollama_model, | |
| translate_lang=translate_lang or None, | |
| format_llm=format_llm or None, | |
| format_api_key=format_api_key or None, | |
| translate_llm=translate_llm or None, | |
| translate_api_key=translate_api_key or None, | |
| keyframe_llm=keyframe_llm or None, | |
| keyframe_api_key=keyframe_api_key or None, | |
| enable_keyframes=enable_keyframes, | |
| keyframe_method=keyframe_method, | |
| keyframe_interval=keyframe_interval, | |
| ) | |
| jobs[job_id]["status"] = "completed" | |
| # μ λ κ²½λ‘λ‘ λ³ννμ¬ μ μ₯ μμΉλ₯Ό μ νν νμ | |
| abs_output = os.path.abspath(output_dir) | |
| jobs[job_id]["result"] = { | |
| "title": result["title"], | |
| "language": result["language"], | |
| "text": result["text"], | |
| "files": { | |
| fmt: os.path.basename(path) for fmt, path in result["files"].items() | |
| }, | |
| "output_dir": abs_output, | |
| "output_dir_raw": output_dir, | |
| } | |
| except Exception as e: | |
| jobs[job_id]["status"] = "error" | |
| jobs[job_id]["error"] = str(e) | |
| async def get_status(job_id: str): | |
| job = jobs.get(job_id) | |
| if not job: | |
| return {"error": "μμ μ μ°Ύμ μ μμ΅λλ€."} | |
| return job | |
| async def stream_status(job_id: str): | |
| async def event_generator(): | |
| seen = 0 | |
| while True: | |
| job = jobs.get(job_id) | |
| if not job: | |
| yield f"data: {json.dumps({'type': 'error', 'message': 'μμ μ μ°Ύμ μ μμ΅λλ€.'})}\n\n" | |
| break | |
| # μ μ§ν λ©μμ§ μ μ‘ | |
| while seen < len(job["progress"]): | |
| msg = job["progress"][seen] | |
| if isinstance(msg, dict): | |
| yield f"data: {json.dumps({'type': 'progress', **msg})}\n\n" | |
| else: | |
| yield f"data: {json.dumps({'type': 'progress', 'message': msg})}\n\n" | |
| seen += 1 | |
| if job["status"] == "completed": | |
| yield f"data: {json.dumps({'type': 'completed', 'result': job['result']})}\n\n" | |
| break | |
| elif job["status"] == "error": | |
| yield f"data: {json.dumps({'type': 'error', 'message': job['error']})}\n\n" | |
| break | |
| await asyncio.sleep(0.5) | |
| return StreamingResponse( | |
| event_generator(), | |
| media_type="text/event-stream", | |
| headers={ | |
| "Cache-Control": "no-cache", | |
| "X-Accel-Buffering": "no", | |
| "Connection": "keep-alive", | |
| }, | |
| ) | |
| async def download_file(filename: str, dir: str = ""): | |
| base_dir = dir if dir else OUTPUT_DIR | |
| file_path = os.path.join(base_dir, filename) | |
| if not os.path.exists(file_path): | |
| return {"error": "νμΌμ μ°Ύμ μ μμ΅λλ€."} | |
| return FileResponse(file_path, filename=filename) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| port = int(os.environ.get("PORT", 8000)) | |
| print("YouTube Script Extractor μΉ μλ²") | |
| print(f" http://localhost:{port}") | |
| print() | |
| uvicorn.run(app, host="0.0.0.0", port=port) | |