Spaces:
Running on Zero
Running on Zero
GitHub Actions
fix: 0 test failures; FileService; real RagService; emergency probe; chat return
4aaae80 | """TtsService β registers tts.synthesize@1.0 on the bus.""" | |
| from __future__ import annotations | |
| import base64 | |
| from typing import Any | |
| from hearthnet.constants import TRANSLATION_MAX_CHARS | |
| class TtsService: | |
| name = "tts" | |
| version = "1.0" | |
| def __init__( | |
| self, | |
| backends: list[Any] | None = None, | |
| bus: Any = None, | |
| ) -> None: | |
| if backends is not None: | |
| self._backends = backends | |
| else: | |
| self._backends = self._discover_backends() | |
| if bus is not None: | |
| self.register(bus) | |
| # ββ Backend discovery βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _discover_backends(self) -> list[Any]: | |
| backends: list[Any] = [] | |
| try: | |
| from hearthnet.services.speech.backends.edge_tts import EdgeTtsBackend | |
| b = EdgeTtsBackend() | |
| if b.health().get("status") == "ok": | |
| backends.append(b) | |
| except Exception: | |
| pass | |
| return backends | |
| def _select_backend(self, preferred: str | None = None) -> Any | None: | |
| for backend in self._backends: | |
| if preferred and backend.name != preferred: | |
| continue | |
| if backend.health().get("status") == "ok": | |
| return backend | |
| for backend in self._backends: | |
| if backend.health().get("status") == "ok": | |
| return backend | |
| return None | |
| # ββ Capability registration βββββββββββββββββββββββββββββββββββββββββββββββ | |
| def register(self, bus: Any) -> None: | |
| from hearthnet.bus.capability import CapabilityDescriptor | |
| desc = CapabilityDescriptor( | |
| name="tts.synthesize", | |
| version=(1, 0), | |
| stability="stable", | |
| params={"backends": [b.name for b in self._backends]}, | |
| max_concurrent=4, | |
| trust_required="member", | |
| timeout_seconds=60, | |
| idempotent=True, | |
| ) | |
| bus.register_capability(desc, self._handle_synthesize, self.params_compatible) | |
| def params_compatible(self, offered: dict, requested: dict) -> bool: | |
| req_backend = requested.get("backend") | |
| if not req_backend: | |
| return True | |
| return req_backend in offered.get("backends", []) | |
| # ββ Handler βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def _handle_synthesize(self, req: Any) -> dict: | |
| body = req.body if hasattr(req, "body") else req | |
| inp = body.get("input", body) | |
| text: str = inp.get("text", "") | |
| voice: str | None = inp.get("voice") | |
| language: str = inp.get("language", "de") | |
| fmt: str = inp.get("format", "ogg_vorbis") | |
| preferred: str | None = inp.get("backend") | |
| if not text: | |
| return {"error": "bad_request", "reason": "text is required"} | |
| if len(text) > TRANSLATION_MAX_CHARS: | |
| return { | |
| "error": "bad_request", | |
| "reason": f"Text too long: {len(text)} > {TRANSLATION_MAX_CHARS} chars", | |
| } | |
| backend = self._select_backend(preferred) | |
| if backend is None: | |
| return { | |
| "error": "backend_unavailable", | |
| "reason": "No healthy TTS backend available", | |
| } | |
| try: | |
| result = await backend.synthesize(text, voice=voice, language=language, format=fmt) | |
| except Exception as exc: | |
| return {"error": "internal_error", "reason": str(exc)} | |
| return { | |
| "audio_b64": base64.b64encode(result.audio_bytes).decode(), | |
| "audio_format": result.audio_format, | |
| "duration_seconds": result.duration_seconds, | |
| "backend": result.backend, | |
| "ms": result.ms, | |
| } | |