from __future__ import annotations import importlib import os import sys import tempfile import unittest from pathlib import Path from unittest.mock import patch from fastapi.testclient import TestClient import numpy as np BACKEND_DIR = Path(__file__).resolve().parents[1] if str(BACKEND_DIR) not in sys.path: sys.path.insert(0, str(BACKEND_DIR)) class AudioStudioApiTests(unittest.TestCase): def _load_client(self, runtime_mode: str) -> TestClient: self.temp_dir = tempfile.TemporaryDirectory() os.environ["OMNIVOICE_RUNTIME_MODE"] = runtime_mode os.environ["OMNIVOICE_DATA_ROOT"] = self.temp_dir.name sys.modules.pop("main", None) main = importlib.import_module("main") main = importlib.reload(main) self.addCleanup(self.temp_dir.cleanup) return TestClient(main.app) def tearDown(self) -> None: os.environ.pop("OMNIVOICE_RUNTIME_MODE", None) os.environ.pop("OMNIVOICE_DATA_ROOT", None) os.environ.pop("OMNIVOICE_ENABLED_FEATURES", None) def test_audio_projects_require_capability_in_production(self) -> None: client = self._load_client(runtime_mode="production") response = client.post("/api/audio/projects", json={"name": "Forbidden"}) self.assertEqual(response.status_code, 403) def test_audio_project_create_and_list_in_self_hosted(self) -> None: client = self._load_client(runtime_mode="self_hosted") create_response = client.post("/api/audio/projects", json={"name": "Narration Draft"}) self.assertEqual(create_response.status_code, 200) created = create_response.json() self.assertEqual(created["name"], "Narration Draft") self.assertGreaterEqual(len(created["lines"]), 1) self.assertIn("speaker_profiles", created) list_response = client.get("/api/audio/projects") self.assertEqual(list_response.status_code, 200) listed = list_response.json() self.assertEqual(len(listed), 1) self.assertEqual(listed[0]["id"], created["id"]) self.assertEqual(listed[0]["name"], "Narration Draft") def test_audio_render_and_mixdown_create_files(self) -> None: client = self._load_client(runtime_mode="self_hosted") create_response = client.post("/api/audio/projects", json={"name": "Render Pipeline"}) project = create_response.json() class FakeAudioGenerator: runtime_backend = "mock" runtime_device = "cpu" def generate(self, **_: object) -> list[np.ndarray]: return [np.zeros(24000, dtype=np.float32)] with patch("audio_projects.get_audio_generator", return_value=FakeAudioGenerator()): for line in project["lines"]: render_response = client.post( f"/api/audio/projects/{project['id']}/lines/{line['id']}/render", json={}, ) self.assertEqual(render_response.status_code, 200) rendered = render_response.json() self.assertEqual(rendered["render_filename"], f"{line['id']}.wav") self.assertGreater(rendered["render_duration_seconds"], 0) self.assertEqual(rendered["render_backend"], "mock") self.assertEqual(rendered["render_device"], "cpu") mixdown_response = client.post( f"/api/audio/projects/{project['id']}/mixdown", json={"format": "wav"}, ) self.assertEqual(mixdown_response.status_code, 200) mixdown = mixdown_response.json() self.assertEqual(mixdown["format"], "wav") file_response = client.get( f"/api/audio/projects/{project['id']}/files/mixdowns/{mixdown['filename']}" ) self.assertEqual(file_response.status_code, 200)