Scriptorium / backend /render_pipeline.py
mattkevan's picture
add modal endpoint and magpietts
cd0ff97
Raw
History Blame Contribute Delete
4.72 kB
from pathlib import Path
from typing import Dict, Iterable, List, Optional
from backend.types import RenderArtifact, RenderJob, VoiceConfig
class RenderPipeline:
def __init__(self, session_root: Path, synthesizer=None) -> None:
self.session_root = Path(session_root)
self.session_root.mkdir(parents=True, exist_ok=True)
self.synthesizer = synthesizer
self._jobs: Dict[str, RenderJob] = {}
self._cancelled: set[str] = set()
self._paused: set[str] = set()
def get_job(self, session_id: str) -> RenderJob:
return self._jobs.setdefault(session_id, RenderJob(session_id=session_id))
def cancel(self, session_id: str) -> None:
self._cancelled.add(session_id)
self.get_job(session_id).status = "cancelled"
def pause(self, session_id: str) -> Dict[str, object]:
self._paused.add(session_id)
self.get_job(session_id).status = "paused"
return {"type": "paused", "session_id": session_id}
def resume(self, session_id: str) -> Dict[str, object]:
self._paused.discard(session_id)
self.get_job(session_id).status = "running"
return {"type": "resumed", "session_id": session_id}
def render(
self,
*,
session_id: str,
book: Dict[str, object],
chapters: List[Dict[str, object]],
voice_config: Dict[str, object],
diffusion_steps: int,
speed: float,
synthesizer=None,
) -> Iterable[Dict[str, object]]:
selected = [chapter for chapter in chapters if chapter.get("included", True)]
job = self.get_job(session_id)
job.status = "running"
job.outputs.clear()
synth = synthesizer or self.synthesizer
if synth is None:
raise ValueError("No synthesizer provided for render pipeline")
yield {
"type": "started",
"session_id": session_id,
"total_chapters": len(selected),
"book_title": book.get("title"),
"backend": "local",
"model": VoiceConfig.from_dict(voice_config).model,
}
voice = VoiceConfig.from_dict(voice_config)
render_dir = self.session_root / session_id / "renders"
render_dir.mkdir(parents=True, exist_ok=True)
for index, chapter in enumerate(selected):
if session_id in self._cancelled:
yield {"type": "cancelled", "session_id": session_id, "backend": "local", "model": voice.model}
self._cancelled.discard(session_id)
job.status = "cancelled"
return
chapter_id = str(chapter["id"])
job.current_chapter_id = chapter_id
yield {
"type": "chapter_started",
"session_id": session_id,
"chapter_id": chapter_id,
"chapter_title": chapter["title"],
"chapter_index": index,
"overall_progress": index / max(1, len(selected)),
"backend": "local",
"model": voice.model,
}
output_path = render_dir / f"{index + 1:03d}-{chapter_id}.wav"
result = synth.synthesize(
text=str(chapter["text"]),
output_path=output_path,
voice_config=voice,
diffusion_steps=diffusion_steps,
speed=speed,
)
yield {
"type": "chapter_progress",
"session_id": session_id,
"chapter_id": chapter_id,
"percent": 100,
"backend": result.get("backend", "local"),
"model": result.get("model", voice.model),
}
artifact = RenderArtifact(
path=output_path,
duration_seconds=int(result.get("duration_seconds", 0)),
chapter_id=chapter_id,
)
job.outputs.append(artifact)
yield {
"type": "chapter_done",
"session_id": session_id,
"chapter_id": chapter_id,
"duration_seconds": artifact.duration_seconds,
"overall_progress": (index + 1) / max(1, len(selected)),
"output_path": str(output_path),
"backend": result.get("backend", "local"),
"model": result.get("model", voice.model),
}
job.status = "completed"
yield {
"type": "completed",
"session_id": session_id,
"outputs": [str(artifact.path) for artifact in job.outputs],
"backend": "local",
"model": voice.model,
}