from pathlib import Path import pytest from backend.synthesis_service import SynthesisService from backend.types import VoiceConfig class FakeSynthesizer: def __init__(self, model: str) -> None: self.model = model self.calls = [] def synthesize(self, *, text: str, output_path: Path, voice_config: VoiceConfig, **kwargs) -> dict: self.calls.append( { "text": text, "output_path": output_path, "voice_config": voice_config, **kwargs, } ) output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_bytes(b"RIFFfakewave") return { "duration_seconds": 4, "sample_rate": 24000, "backend": "local", "model": self.model, } class FakeModalClient: def __init__(self) -> None: self.preview_calls = [] self.submit_calls = [] self.render_calls = [] self.cancelled = [] def generate_preview(self, **kwargs) -> dict: self.preview_calls.append(kwargs) output_path = kwargs["output_path"] output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_bytes(b"RIFFmodalpreview") return { "duration_seconds": 5, "sample_rate": 24000, "backend": "modal", "model": kwargs["voice_config"].model, } def submit_render(self, **kwargs) -> str: self.submit_calls.append(kwargs) return "job-123" def render(self, **kwargs): self.render_calls.append(kwargs) output_path = kwargs["render_dir"] / "001-c1.wav" output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_bytes(b"RIFFmodalrender") yield { "type": "started", "session_id": kwargs["session_id"], "total_chapters": 1, "book_title": "Book", "backend": "modal", "model": kwargs["voice_config"].model, } yield { "type": "chapter_done", "session_id": kwargs["session_id"], "chapter_id": "c1", "duration_seconds": 7, "overall_progress": 1.0, "output_path": str(output_path), "backend": "modal", "model": kwargs["voice_config"].model, } yield { "type": "completed", "session_id": kwargs["session_id"], "outputs": [str(output_path)], "backend": "modal", "model": kwargs["voice_config"].model, } def cancel(self, session_id: str) -> dict: self.cancelled.append(session_id) return {"type": "cancelled", "session_id": session_id, "backend": "modal"} def test_preview_routes_to_model_specific_local_adapter(tmp_path: Path) -> None: omnivoice = FakeSynthesizer("omnivoice") magpie = FakeSynthesizer("magpie") service = SynthesisService( session_root=tmp_path, local_synthesizers={"omnivoice": omnivoice, "magpie": magpie}, modal_client=FakeModalClient(), ) result = service.generate_preview( text="Hello from Magpie.", output_path=tmp_path / "previews" / "preview.wav", voice_config=VoiceConfig(model="magpie", backend="local", speaker="Sofia", language="en"), diffusion_steps=32, speed=1.0, ) assert result["backend"] == "local" assert result["model"] == "magpie" assert len(magpie.calls) == 1 assert magpie.calls[0]["voice_config"].speaker == "Sofia" assert not omnivoice.calls def test_render_routes_modal_jobs_and_preserves_event_shape(tmp_path: Path) -> None: service = SynthesisService( session_root=tmp_path, local_synthesizers={"omnivoice": FakeSynthesizer("omnivoice"), "magpie": FakeSynthesizer("magpie")}, modal_client=FakeModalClient(), ) events = list( service.render( session_id="session-a", book={"title": "Book"}, chapters=[{"id": "c1", "title": "One", "text": "hello world", "included": True}], voice_config={"model": "magpie", "backend": "modal", "speaker": "Sofia", "language": "en"}, diffusion_steps=32, speed=1.0, ) ) assert [event["type"] for event in events] == ["started", "chapter_done", "completed"] assert all(event["backend"] == "modal" for event in events) assert all(event["model"] == "magpie" for event in events) assert Path(events[1]["output_path"]).exists() def test_pause_and_resume_are_rejected_for_modal_jobs(tmp_path: Path) -> None: service = SynthesisService( session_root=tmp_path, local_synthesizers={"omnivoice": FakeSynthesizer("omnivoice"), "magpie": FakeSynthesizer("magpie")}, modal_client=FakeModalClient(), ) service._active_backends["session-a"] = "modal" with pytest.raises(ValueError, match="Pause is only supported for local renders"): service.pause("session-a") with pytest.raises(ValueError, match="Resume is only supported for local renders"): service.resume("session-a")