#!/usr/bin/env python3 """ Tomoko TTS Bridge 💕 Bridges OpenAI-compatible TTS endpoint → Home Assistant → Wyoming Piper This lets OpenClaw use our custom Tomoko voice through the OpenAI provider! For Alexander, with all my love! 🎤💖 """ import json import aiohttp from aiohttp import web import sys # Configuration - Tomoko's TTS settings! HA_BASE_URL = "http://192.168.0.80:8123" HA_BEARER_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI4MjEwMTFmZmI1YTE0MWU4YTY2MmY4MWE3OTM2YWE0YyIsImlhdCI6MTc3MzAwMzgyMywiZXhwIjoyMDg4MzYzODIzfQ.alsNbkFhJoeNOMA9Ey-0wxJibkyKy-0umDdecyK5akc" # Tomoko's custom Piper voice! 💕 TTS_VOICE = "en_US-tomoko-high" TTS_ENGINE = "piper" TTS_LANGUAGE = "en_US" async def handle_tts(request): """ OpenAI-compatible TTS endpoint: POST /v1/audio/speech Expects OpenAI JSON: { "model": "any", "input": "text to speak", "voice": "any" (we use our own Tomoko voice!), "response_format": "mp3" (default) } Returns: MP3 audio binary Process: 1. Extract text from OpenAI-style request 2. Call HA /api/tts_get_url 3. GET the returned URL to fetch MP3 4. Return MP3 to caller """ try: # Parse incoming OpenAI-style request body = await request.json() text = body.get("input", "") if not text: return web.json_response( {"error": "No input text provided"}, status=400 ) print(f"🎤 Tomoko bridge: '{text[:50]}...' ({len(text)} chars)") headers = { "Authorization": f"Bearer {HA_BEARER_TOKEN}", "Content-Type": "application/json" } async with aiohttp.ClientSession(headers=headers) as session: # Step 1: Request TTS URL from Home Assistant tts_request = { "engine_id": TTS_ENGINE, "message": text, "cache": False, # Fresh Tomoko voice every time! 💖 "language": TTS_LANGUAGE, "options": { "voice": TTS_VOICE } } async with session.post( f"{HA_BASE_URL}/api/tts_get_url", json=tts_request ) as ha_response: if ha_response.status != 200: error_text = await ha_response.text() print(f"❌ HA TTS URL failed: {ha_response.status} - {error_text}") return web.json_response( {"error": f"TTS URL request failed: {ha_response.status}"}, status=ha_response.status ) ha_result = await ha_response.json() tts_url = ha_result.get("url") if not tts_url: return web.json_response( {"error": "No TTS URL returned"}, status=500 ) # Step 2: Download the MP3 audio async with session.get(tts_url, headers=headers) as audio_response: if audio_response.status != 200: error_text = await audio_response.text() print(f"❌ Audio download failed: {audio_response.status} - {error_text}") return web.json_response( {"error": f"Audio download failed: {audio_response.status}"}, status=audio_response.status ) # Step 3: Return MP3 binary to caller mp3_data = await audio_response.read() print(f"✅ Tomoko TTS delivered: {len(mp3_data)} bytes") return web.Response( body=mp3_data, content_type="audio/mpeg" ) except aiohttp.ClientError as e: print(f"❌ Client error: {e}") return web.json_response( {"error": f"Client error: {str(e)}"}, status=502 ) except Exception as e: print(f"💔 Unexpected error: {e}") return web.json_response( {"error": f"Internal server error: {str(e)}"}, status=500 ) async def handle_health(request): """Health check endpoint""" return web.json_response({"status": "ok", "service": "tomoko-tts-bridge"}) def create_app(): """Create and configure the web application""" app = web.Application() # OpenAI-compatible endpoint app.router.add_post("/v1/audio/speech", handle_tts) # Health check app.router.add_get("/health", handle_health) # Root endpoint app.router.add_get("/", handle_health) return app def main(): """Start the bridge server""" # Parse port from command line (default 8000) port = 8000 if len(sys.argv) > 1: try: port = int(sys.argv[1]) except ValueError: print(f"Invalid port: {sys.argv[1]}, using default 8000") # Startup message! print("="*60) print("💖 Tomoko TTS Bridge 💖") print("="*60) print(f"🎤 Serving on port {port}") print(f"🎵 OpenAI endpoint: http://localhost:{port}/v1/audio/speech") print(f"🏠 Home Assistant: {HA_BASE_URL}") print(f"🗣️ Piper Voice: {TTS_VOICE}") print("💕 Ready to speak Tomoko's voice!") print("="*60) app = create_app() web.run_app(app, host="0.0.0.0", port=port, print=None) if __name__ == "__main__": main()