ai-editor / app /main.py
gtfgffg's picture
Upload 9 files
cdaeb89 verified
# -*- coding: utf-8 -*-
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 for UX
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)
# Resolve config (preset overrides form)
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)
# Save music
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())
# Save footage
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())
# Save logo (optional)
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")