Files
VoiceStudio/backend/core/tasks.py
T
debpalashandClaude Opus 4.7 67328d04fe refactor: split backend into api/core/services/schemas, harden security + fd pressure, add searchable language picker, fix segment fragmentation
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>
2026-04-18 17:20:19 +05:30

104 lines
3.5 KiB
Python

import asyncio
import time
import json
class TaskManager:
def __init__(self):
self.queue = None
self.active_tasks = {}
def _init_queue(self):
if self.queue is None:
self.queue = asyncio.Queue()
async def add_task(self, task_id, task_type, func, *args, **kwargs):
self._init_queue()
task_obj = {
"status": "pending",
"type": task_type,
"created_at": time.time(),
"history": [],
"listeners": [],
"listeners_lock": asyncio.Lock(),
"error": None,
"cancelled": False,
}
self.active_tasks[task_id] = task_obj
await self.queue.put((task_id, func, args, kwargs))
def cancel_task(self, task_id):
if task_id in self.active_tasks:
self.active_tasks[task_id]["cancelled"] = True
return True
return False
def is_cancelled(self, task_id):
t = self.active_tasks.get(task_id)
return t["cancelled"] if t else False
async def add_listener(self, task_id, q):
t = self.active_tasks.get(task_id)
if not t:
return False
async with t["listeners_lock"]:
t["listeners"].append(q)
return True
async def remove_listener(self, task_id, q):
t = self.active_tasks.get(task_id)
if not t:
return
async with t["listeners_lock"]:
if q in t["listeners"]:
t["listeners"].remove(q)
async def _push_event(self, task_id, event_str):
t = self.active_tasks.get(task_id)
if t is None:
return
if event_str is not None:
t["history"].append(event_str)
# Snapshot listeners under lock so concurrent add/remove can't mutate mid-iteration.
async with t["listeners_lock"]:
listeners = list(t["listeners"])
for q in listeners:
await q.put(event_str)
async def worker(self):
self._init_queue()
while True:
task_id, func, args, kwargs = await self.queue.get()
t = self.active_tasks.get(task_id)
if not t:
self.queue.task_done()
continue
t["status"] = "running"
try:
import inspect
res = func(*args, **kwargs)
if inspect.isasyncgen(res):
async for update in res:
if t.get("cancelled"):
await self._push_event(task_id, f"data: {json.dumps({'type': 'cancelled'})}\n\n")
t["status"] = "cancelled"
break
await self._push_event(task_id, update)
elif inspect.iscoroutine(res):
await res
t["status"] = "done"
except Exception as e:
import logging
logging.getLogger("omnivoice.tasks").exception("Task %s failed", task_id)
t["status"] = "failed"
t["error"] = str(e)
try:
await self._push_event(task_id, f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n")
except Exception as push_err:
logging.getLogger("omnivoice.tasks").warning("Failed to push error event for %s: %s", task_id, push_err)
finally:
await self._push_event(task_id, None) # EOF
self.queue.task_done()
task_manager = TaskManager()