""" Minimal background job runner. GPU jobs use a shared counter so the Whisper model stays cached between back-to-back transcriptions and is only released when no more GPU work is queued. """ import threading import traceback from concurrent.futures import ThreadPoolExecutor from . import database as db from .errors import PipelineError from .logging_config import get_logger log = get_logger(__name__) _executor = ThreadPoolExecutor(max_workers=1) _gpu_jobs_queued = 0 _gpu_lock = threading.Lock() def _unload_gpu(): """Release the cached Whisper model from GPU memory.""" from .pipeline.transcribe import unload_models unload_models() def submit(job_id: str, fn, *args, requires_gpu=False, **kwargs): if requires_gpu: with _gpu_lock: global _gpu_jobs_queued _gpu_jobs_queued += 1 def _run(): log.info("Job %s starting", job_id) db.update_job(job_id, status="running", progress="Starting...") try: def progress_cb(msg): log.info("Job %s: %s", job_id, msg) db.update_job(job_id, progress=msg) fn(*args, progress_cb=progress_cb, **kwargs) db.update_job(job_id, status="done", progress="Complete") log.info("Job %s complete", job_id) except PipelineError as e: log.error("Job %s failed at stage '%s': %s", job_id, e.stage, e.message) db.update_job( job_id, status="error", error=e.message, error_stage=e.stage, error_detail=traceback.format_exc(), ) except Exception as e: log.exception("Job %s failed with unexpected error", job_id) db.update_job( job_id, status="error", error=f"Unexpected error: {e}", error_stage="unknown", error_detail=traceback.format_exc(), ) finally: if requires_gpu: with _gpu_lock: global _gpu_jobs_queued _gpu_jobs_queued -= 1 if _gpu_jobs_queued <= 0: log.info("No more GPU jobs queued — unloading Whisper model") _unload_gpu() _executor.submit(_run)