Backend:
- Split monolithic main.py into backend/{api/routers,core,schemas,services}
- core/db.py: allowlist-gated migrations, db_conn context manager (kills SQL injection on ALTER)
- core/tasks.py: lock-guarded listener add/remove/push, snapshot-before-iterate
- services/ffmpeg_utils.py: run_ffmpeg helper with concurrency semaphore, EAGAIN retry, guaranteed reap
- services/segmentation.py: Bengali/CJK/Arabic punctuation, ultra-short tier, stitch_adjacent_shorts,
bounded-loop merge; public clean_up_segments API
- services/model_manager.py: robust lock.locked() handling
- api/routers/dub_core.py: job_id traversal guard, thread-safe _active_procs, timeouts on ffmpeg/demucs,
POST /dub/cleanup-segments endpoint
- api/routers/dub_export.py: guarded SSE listener remove, ffmpeg timeouts via run_ffmpeg
- api/routers/exports.py: destination_path validation, safe source resolver, subprocess list-form
- api/routers/generation.py: contextlib.suppress on tempfile cleanup, db_conn usage, safe output-path helper
- api/routers/system.py: try/finally tmp cleanup, subprocess timeouts
- schemas/requests.py: TranslateSegment.id int->str to match hex segment IDs
- main.py: threading.Lock around crash log writes
Frontend:
- components/SearchableSelect.jsx: popover combobox with search, keyboard nav, popular+recent pins, 200-item cap
- App.jsx: wire SearchableSelect for dub language / ISO code / voice-gen language; Clean Up segments button;
fix blob URL leak (object-shaped prev in setter, unmount cleanup via ref)
- components/WaveformTimeline.jsx: explicit <video> detach instead of innerHTML='' to release decoder
- index.css: ss-* combobox styles matching Gruvbox theme
Tests:
- tests/test_segmentation.py (26 cases), test_dub_transcribe.py, test_dub_export_unique.py, conftest.py
Chore:
- .gitignore: exclude omnivoice.zip, /research/ reference clones
- Remove tracked stray root test scripts + crash_log.txt
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
73 lines
2.5 KiB
Python
73 lines
2.5 KiB
Python
import uuid
|
|
import time
|
|
import json
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
from core.db import get_db
|
|
from schemas.requests import ProjectSaveRequest
|
|
|
|
router = APIRouter()
|
|
|
|
@router.get("/projects")
|
|
async def list_projects():
|
|
conn = get_db()
|
|
rows = conn.execute(
|
|
"SELECT id, name, video_path, duration, created_at, updated_at FROM studio_projects ORDER BY updated_at DESC"
|
|
).fetchall()
|
|
conn.close()
|
|
return [dict(r) for r in rows]
|
|
|
|
@router.get("/projects/{project_id}")
|
|
async def get_project(project_id: str):
|
|
conn = get_db()
|
|
row = conn.execute("SELECT * FROM studio_projects WHERE id=?", (project_id,)).fetchone()
|
|
conn.close()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
result = dict(row)
|
|
if result.get("state_json"):
|
|
try:
|
|
result["state"] = json.loads(result["state_json"])
|
|
except Exception:
|
|
result["state"] = {}
|
|
else:
|
|
result["state"] = {}
|
|
return result
|
|
|
|
@router.post("/projects")
|
|
async def create_project(req: ProjectSaveRequest):
|
|
project_id = str(uuid.uuid4())[:8]
|
|
now = time.time()
|
|
conn = get_db()
|
|
conn.execute(
|
|
"INSERT INTO studio_projects (id, name, video_path, audio_path, duration, state_json, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)",
|
|
(project_id, req.name, req.video_path, req.audio_path, req.duration, json.dumps(req.state), now, now),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
return {"id": project_id, "name": req.name, "created_at": now}
|
|
|
|
@router.put("/projects/{project_id}")
|
|
async def update_project(project_id: str, req: ProjectSaveRequest):
|
|
conn = get_db()
|
|
row = conn.execute("SELECT id FROM studio_projects WHERE id=?", (project_id,)).fetchone()
|
|
if not row:
|
|
conn.close()
|
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
now = time.time()
|
|
conn.execute(
|
|
"UPDATE studio_projects SET name=?, video_path=?, audio_path=?, duration=?, state_json=?, updated_at=? WHERE id=?",
|
|
(req.name, req.video_path, req.audio_path, req.duration, json.dumps(req.state), now, project_id),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
return {"id": project_id, "name": req.name, "updated_at": now}
|
|
|
|
@router.delete("/projects/{project_id}")
|
|
async def delete_project(project_id: str):
|
|
conn = get_db()
|
|
conn.execute("DELETE FROM studio_projects WHERE id=?", (project_id,))
|
|
conn.commit()
|
|
conn.close()
|
|
return {"deleted": project_id}
|