| 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") |
|
|