| |
| import os, uuid, json, shutil, threading, traceback |
| from fastapi import FastAPI, UploadFile, File, Form, Request |
| from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse |
| from fastapi.staticfiles import StaticFiles |
| from fastapi.templating import Jinja2Templates |
|
|
| from .processor import run_job |
|
|
| BASE_DIR = os.path.dirname(os.path.dirname(__file__)) |
| JOBS_DIR = os.path.join(BASE_DIR, "jobs") |
| os.makedirs(JOBS_DIR, exist_ok=True) |
|
|
| app = FastAPI(title="FlexCut — AI Video Editor") |
| app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "app", "static")), name="static") |
| templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "app", "templates")) |
|
|
| STAT = {} |
|
|
| def _worker(job_id, job_dir): |
| STAT[job_id] = {"status":"running","message":"در حال پردازش...","paths":[]} |
| try: |
| paths = run_job(job_dir) |
| STAT[job_id] = {"status":"done","message":"انجام شد","paths":paths} |
| except Exception as e: |
| STAT[job_id] = {"status":"error","message":f"{e}\n{traceback.format_exc()}","paths":[]} |
|
|
| @app.get("/", response_class=HTMLResponse) |
| def index(request: Request): |
| |
| presets = [ |
| {"name":"Reels Fast 20s (9:16)","duration":"00:00:20","aspects":["9:16"],"crossfade":0.2,"intro_text":"Flex Studio","scene_threshold":27.0}, |
| {"name":"Showreel 45s (16:9)","duration":"00:00:45","aspects":["16:9"],"crossfade":0.15,"intro_text":"Showreel","scene_threshold":25.0}, |
| {"name":"Dual Export (9:16 + 16:9)","duration":"00:00:30","aspects":["9:16","16:9"],"crossfade":0.1,"intro_text":"Dual Export","scene_threshold":27.0} |
| ] |
| return templates.TemplateResponse("index.html", {"request": request, "presets": json.dumps(presets, ensure_ascii=False)}) |
|
|
| @app.post("/create", response_class=HTMLResponse) |
| async def create(request: Request, |
| preset_json: str = Form(...), |
| duration: str = Form("00:00:20"), |
| aspects: str = Form("9:16"), |
| crossfade: float = Form(0.1), |
| intro_text: str = Form(""), |
| scene_threshold: float = Form(27.0), |
| music: UploadFile = File(...), |
| footage: list[UploadFile] = File(...), |
| logo: UploadFile | None = File(None)): |
| job_id = str(uuid.uuid4())[:8] |
| job_dir = os.path.join(JOBS_DIR, job_id) |
| os.makedirs(job_dir, exist_ok=True) |
| os.makedirs(os.path.join(job_dir, "footage"), exist_ok=True) |
|
|
| |
| try: |
| preset = json.loads(preset_json) if preset_json else {} |
| except: |
| preset = {} |
| cfg = { |
| "duration": preset.get("duration", duration), |
| "aspects": preset.get("aspects", aspects.split(",")), |
| "crossfade": float(preset.get("crossfade", crossfade)), |
| "intro_text": preset.get("intro_text", intro_text), |
| "scene_threshold": float(preset.get("scene_threshold", scene_threshold)), |
| } |
| with open(os.path.join(job_dir, "config.json"), "w", encoding="utf-8") as f: |
| json.dump(cfg, f, ensure_ascii=False, indent=2) |
|
|
| |
| mext = os.path.splitext(music.filename or "")[1] or ".mp3" |
| with open(os.path.join(job_dir, "music"+mext), "wb") as f: |
| f.write(await music.read()) |
|
|
| |
| for uf in footage: |
| name = uf.filename or f"clip_{uuid.uuid4().hex[:6]}.mp4" |
| with open(os.path.join(job_dir, "footage", name), "wb") as f: |
| f.write(await uf.read()) |
|
|
| |
| if logo is not None: |
| with open(os.path.join(job_dir, "logo.png"), "wb") as f: |
| f.write(await logo.read()) |
|
|
| STAT[job_id] = {"status":"queued","message":"در صف انتظار","paths":[]} |
| t = threading.Thread(target=_worker, args=(job_id, job_dir), daemon=True) |
| t.start() |
|
|
| return RedirectResponse(f"/job/{job_id}", status_code=303) |
|
|
| @app.get("/job/{job_id}", response_class=HTMLResponse) |
| def job_status(job_id: str, request: Request): |
| info = STAT.get(job_id, {"status":"unknown","message":"not found","paths":[]}) |
| return templates.TemplateResponse("status.html", {"request": request, "job_id": job_id, "info": info}) |
|
|
| @app.get("/download/{job_id}/{filename}") |
| def download(job_id: str, filename: str): |
| path = os.path.join(JOBS_DIR, job_id, filename) |
| if not os.path.exists(path): |
| return HTMLResponse("<h3>File not found.</h3>", status_code=404) |
| return FileResponse(path, filename=filename, media_type="video/mp4") |
|
|