Download app.py from build-small-hackathon/Scriptorium: direct link, hf CLI and curl.
- Browser
- Download file 10 kB
-
https://huggingface.co/spaces/build-small-hackathon/Scriptorium/resolve/main/app.py
- Command line
-
hf download hf://spaces/build-small-hackathon/Scriptorium/app.py
-
curl -L -o app.py https://huggingface.co/spaces/build-small-hackathon/Scriptorium/resolve/main/app.py
10 kB
| import os | |
| import json | |
| import shutil | |
| import socket | |
| from pathlib import Path | |
| from typing import Any, Dict, Generator, List, Optional | |
| from fastapi.responses import FileResponse, HTMLResponse, JSONResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from gradio import Blocks, Server | |
| from gradio.events import api as gradio_api | |
| from backend.config import ( | |
| APP_TITLE, | |
| MAX_PREVIEW_CHARACTERS, | |
| MAX_REFERENCE_AUDIO_SECONDS, | |
| SESSION_TTL_SECONDS, | |
| TEMP_ROOT, | |
| ) | |
| from backend.epub import EpubConfig, parse_epub | |
| from backend.export import export_audiobook | |
| from backend.input_files import resolve_uploaded_name, resolve_uploaded_path | |
| from backend.session_store import SessionStore | |
| from backend.synthesis_catalog import MAGPIE_OPTIONS, SYNTHESIS_BACKENDS, SYNTHESIS_MODELS | |
| from backend.synthesis_service import SynthesisService | |
| from backend.types import VoiceConfig | |
| from backend.voice_design import VOICE_DESIGN_OPTIONS | |
| from backend.voice_presets import VOICE_PRESETS | |
| ROOT = Path(__file__).parent | |
| FRONTEND_DIR = ROOT / "frontend" | |
| TEMP_ROOT.mkdir(parents=True, exist_ok=True) | |
| app = Server() | |
| app.title = APP_TITLE | |
| store = SessionStore(root=TEMP_ROOT, ttl_seconds=SESSION_TTL_SECONDS) | |
| synthesis_service = SynthesisService(session_root=TEMP_ROOT) | |
| def _warn_about_modal_configuration() -> None: | |
| client = synthesis_service.modal_client | |
| warning = client.configuration_warning() | |
| if warning: | |
| print(f"Modal configuration warning: {warning}") | |
| return | |
| if client.is_configured(): | |
| print( | |
| "Modal backend configured: " | |
| f"base_url={client.base_url} timeout={client.timeout_seconds}s poll_interval={client.poll_interval_seconds}s" | |
| ) | |
| _warn_about_modal_configuration() | |
| def _session_root(session_id: str) -> Path: | |
| return store.ensure_session(session_id).root | |
| def _book_payload(session_id: str) -> Dict[str, Any]: | |
| return store.load_json(session_id, "book.json") | |
| def _selected_book(session_id: str, selected_ids: List[str]) -> Dict[str, Any]: | |
| book = _book_payload(session_id) | |
| selected = set(selected_ids) | |
| chapters = [] | |
| for chapter in book["chapters"]: | |
| chapter = dict(chapter) | |
| chapter["included"] = chapter["id"] in selected | |
| chapters.append(chapter) | |
| book["chapters"] = chapters | |
| return book | |
| def _voice_config_for_backend(voice_config: Dict[str, Any], session_id: str) -> Dict[str, Any]: | |
| config = dict(voice_config) | |
| sample_path = config.get("samplePath") or config.get("sample_path") | |
| if sample_path: | |
| sample_file = resolve_uploaded_path(sample_path) | |
| target = _session_root(session_id) / "uploads" / sample_file.name | |
| shutil.copyfile(sample_file, target) | |
| config["samplePath"] = str(target) | |
| return config | |
| def parse_epub_api(session_id: str, epub_file: str) -> Dict[str, Any]: | |
| store.cleanup_expired() | |
| session = store.ensure_session(session_id) | |
| source = resolve_uploaded_path(epub_file) | |
| upload_name = resolve_uploaded_name(epub_file) | |
| if not source.exists(): | |
| raise ValueError("Uploaded EPUB file was not found") | |
| target = session.root / "uploads" / upload_name | |
| shutil.copyfile(source, target) | |
| payload = parse_epub(target, config=EpubConfig()) | |
| store.save_json(session_id, "book.json", payload) | |
| return payload | |
| def generate_preview_api( | |
| session_id: str, | |
| chapter_id: str, | |
| voice_config: Dict[str, Any], | |
| diffusion_steps: int = 32, | |
| speed: float = 1.0, | |
| ) -> Dict[str, Any]: | |
| book = _book_payload(session_id) | |
| chapter = next((item for item in book["chapters"] if item["id"] == chapter_id), None) | |
| if chapter is None: | |
| raise ValueError("Chapter not found for preview") | |
| text = str(chapter["text"])[:MAX_PREVIEW_CHARACTERS] | |
| preview_path = _session_root(session_id) / "previews" / f"{chapter_id}.wav" | |
| voice = _voice_config_for_backend(voice_config, session_id) | |
| result = synthesis_service.generate_preview( | |
| text=text, | |
| output_path=preview_path, | |
| voice_config=VoiceConfig.from_dict(voice), | |
| diffusion_steps=diffusion_steps, | |
| speed=speed, | |
| ) | |
| return { | |
| "url": f"/files/{session_id}/previews/{preview_path.name}", | |
| "duration_seconds": result["duration_seconds"], | |
| "backend": result["backend"], | |
| "model": result["model"], | |
| } | |
| def start_render_api( | |
| session_id: str, | |
| selected_chapter_ids: List[str], | |
| voice_config: Dict[str, Any], | |
| diffusion_steps: int = 32, | |
| speed: float = 1.0, | |
| ) -> Generator[Dict[str, Any], None, None]: | |
| job = synthesis_service.get_job(session_id) | |
| if job.status == "running": | |
| raise ValueError("A render is already active for this session") | |
| book = _selected_book(session_id, selected_chapter_ids) | |
| voice = _voice_config_for_backend(voice_config, session_id) | |
| captured: List[Dict[str, Any]] = [] | |
| for event in synthesis_service.render( | |
| session_id=session_id, | |
| book=book, | |
| chapters=book["chapters"], | |
| voice_config=voice, | |
| diffusion_steps=diffusion_steps, | |
| speed=speed, | |
| ): | |
| if event.get("type") == "chapter_done" and event.get("output_path"): | |
| filename = Path(str(event["output_path"])).name | |
| event = { | |
| **event, | |
| "url": f"/files/{session_id}/renders/{filename}", | |
| } | |
| captured.append(event) | |
| yield event | |
| if captured and captured[-1]["type"] == "completed": | |
| durations = { | |
| event["chapter_id"]: event["duration_seconds"] | |
| for event in captured | |
| if event["type"] == "chapter_done" | |
| } | |
| manifest = { | |
| "book": book, | |
| "chapters": [ | |
| { | |
| **chapter, | |
| "duration_seconds": durations.get(chapter["id"], chapter.get("duration_seconds", 0)), | |
| } | |
| for chapter in book["chapters"] | |
| if chapter.get("included") | |
| ], | |
| "render_result": captured[-1], | |
| } | |
| store.save_json(session_id, "render_manifest.json", manifest) | |
| def pause_render_api(session_id: str) -> Dict[str, Any]: | |
| return synthesis_service.pause(session_id) | |
| def resume_render_api(session_id: str) -> Dict[str, Any]: | |
| return synthesis_service.resume(session_id) | |
| def cancel_render_api(session_id: str) -> Dict[str, Any]: | |
| return synthesis_service.cancel(session_id) | |
| def export_audiobook_api( | |
| session_id: str, | |
| format: str = "m4a", | |
| metadata: Dict[str, str] | None = None, | |
| embed_markers: bool = True, | |
| ) -> Dict[str, Any]: | |
| manifest = store.load_json(session_id, "render_manifest.json") | |
| export = export_audiobook( | |
| session_root=_session_root(session_id), | |
| chapters=manifest["chapters"], | |
| output_format=format, | |
| metadata=metadata or {}, | |
| embed_markers=embed_markers, | |
| cover_path=None, | |
| ) | |
| export["url"] = f"/files/{session_id}/exports/{Path(export['file']).name}" | |
| return export | |
| async def homepage() -> HTMLResponse: | |
| html = (FRONTEND_DIR / "index.html").read_text(encoding="utf-8") | |
| preset_script = ( | |
| "<script>" | |
| f"window.__VOICE_PRESETS__ = {json.dumps(VOICE_PRESETS)};" | |
| f"window.__VOICE_DESIGN_OPTIONS__ = {json.dumps(VOICE_DESIGN_OPTIONS)};" | |
| f"window.__MAGPIE_OPTIONS__ = {json.dumps(MAGPIE_OPTIONS)};" | |
| f"window.__SYNTHESIS_MODELS__ = {json.dumps(SYNTHESIS_MODELS)};" | |
| f"window.__SYNTHESIS_BACKENDS__ = {json.dumps(SYNTHESIS_BACKENDS)};" | |
| "</script>" | |
| ) | |
| return HTMLResponse(html.replace("</body>", f" {preset_script}\n </body>")) | |
| async def health() -> Dict[str, str]: | |
| return {"status": "ok"} | |
| async def serve_generated_file(session_id: str, kind: str, filename: str, download: bool = False): | |
| path = _session_root(session_id) / kind / filename | |
| if not path.exists(): | |
| return JSONResponse({"error": "file not found"}, status_code=404) | |
| if download: | |
| return FileResponse(path, filename=filename) | |
| return FileResponse(path, content_disposition_type="inline") | |
| app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="frontend-static") | |
| def _build_demo() -> Blocks: | |
| # Expose the launched Blocks at module scope so Gradio hot reload can find it. | |
| with Blocks(mode="server") as demo: | |
| for fn, api_kwargs in app._deferred_apis: | |
| gradio_api(fn=fn, **api_kwargs) | |
| return demo | |
| demo = _build_demo() | |
| def _port_is_available(port: int, host: str = "127.0.0.1") -> bool: | |
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: | |
| sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
| return sock.connect_ex((host, port)) != 0 | |
| def _choose_server_port(preferred: int = 7860, attempts: int = 10) -> int: | |
| env_port = os.getenv("GRADIO_SERVER_PORT") | |
| if env_port: | |
| return int(env_port) | |
| for port in range(preferred, preferred + attempts): | |
| if _port_is_available(port): | |
| return port | |
| raise OSError( | |
| f"Cannot find empty port in range: {preferred}-{preferred + attempts - 1}. " | |
| "Set GRADIO_SERVER_PORT to override." | |
| ) | |
| def _launch_app(server_port: int) -> None: | |
| demo.launch( | |
| _app=app, | |
| server_name="0.0.0.0", | |
| server_port=server_port, | |
| # This app serves its own frontend and uses Gradio for the API layer only. | |
| # SSR adds a Node proxy that is unnecessary here and has been noisy in Spaces. | |
| ssr_mode=False, | |
| ) | |
| if __name__ == "__main__": | |
| server_port = _choose_server_port() | |
| print(f"Starting {APP_TITLE} on port {server_port}") | |
| _launch_app(server_port) | |