ssulbox/backend/app/jobs.py

102 lines
3.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# -*- coding: utf-8 -*-
"""썰박스 생성 잡 매니저 (subprocess → 내장 generator 엔진).
편당 2~4분·과금이 크므로 동시 실행 제한. stdout 진행 마커([1/4]…, "완성: ...mp4")를 파싱.
DB 작업은 하지 않는다 — 완료 후 처리(content 삽입·크레딧 차감)는 잡 상태 조회 엔드포인트가 1회 수행.
"""
import os
import re
import subprocess
import sys
import threading
import uuid
from collections import deque
from datetime import datetime
from .config import GENERATOR_DIR, SCENARIO_ENGINE, GEMINI_API_KEY, settings
_jobs: dict[str, dict] = {}
_queue: deque[str] = deque()
_lock = threading.Lock()
_running = 0
_STEP_RE = re.compile(r"\[(\d)\s*/\s*4\]")
_DONE_RE = re.compile(r"완성[:]\s*(.+\.mp4)")
STEP_NAMES = ["대본 생성", "스토리보드", "이미지·음성", "영상 합성"]
def create_job(scenario: str, input_text: str, scenes: int, seconds: int, owner_uuid: str | None) -> dict:
jid = uuid.uuid4().hex[:12]
_jobs[jid] = {
"id": jid, "scenario": scenario, "input": input_text,
"scenes": scenes, "seconds": seconds, "owner_uuid": owner_uuid,
"status": "queued", "step": 0, "log": [], "output": None, "error": None,
"finalized": False, "created": datetime.now().isoformat(timespec="seconds"),
}
with _lock:
_queue.append(jid)
_pump()
return _jobs[jid]
def get_job(jid: str) -> dict | None:
return _jobs.get(jid)
def _pump():
global _running
with _lock:
while _queue and _running < settings.MAX_CONCURRENT_JOBS:
jid = _queue.popleft()
_running += 1
threading.Thread(target=_run, args=(jid,), daemon=True).start()
def _run(jid: str):
global _running
job = _jobs[jid]
try:
engine = SCENARIO_ENGINE[job["scenario"]]
engine_dir = GENERATOR_DIR / engine
main_py = engine_dir / "main.py"
if not main_py.exists():
raise FileNotFoundError(f"generator not found: {main_py}")
cmd = [sys.executable, "-u", str(main_py), job["input"],
"--scenes", str(job["scenes"]), "--seconds", str(job["seconds"])]
env = dict(os.environ)
env["PYTHONIOENCODING"] = "utf-8"
if GEMINI_API_KEY:
env["GEMINI_API_KEY"] = GEMINI_API_KEY
job["status"] = "running"
p = subprocess.Popen(
cmd, cwd=str(engine_dir), env=env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, encoding="utf-8", errors="replace", bufsize=1,
)
for line in p.stdout:
line = line.rstrip()
if not line:
continue
job["log"].append(line)
if len(job["log"]) > 300:
del job["log"][:-300]
m = _STEP_RE.search(line)
if m:
job["step"] = int(m.group(1))
d = _DONE_RE.search(line)
if d:
job["output"] = d.group(1).strip()
code = p.wait()
if code == 0:
job["status"] = "done"; job["step"] = 4
else:
job["status"] = "error"; job["error"] = f"생성 프로세스 종료코드 {code}"
except Exception as e:
job["status"] = "error"; job["error"] = f"{type(e).__name__}: {e}"
finally:
with _lock:
_running -= 1
_pump()