Coverage for integrations / service_tools / servers / tts_audio_suite_server.py: 0.0%
84 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2TTS-Audio-Suite Sidecar Server — Flask API on a dynamic port.
4Launched as a subprocess by RuntimeToolManager. On startup:
51. Finds a free port (OS-assigned)
62. Prints PORT=NNNNN to stdout (parent reads this)
73. Lazy-loads TTS models
84. Serves TTS requests
10Usage (standalone test):
11 python -m integrations.service_tools.servers.tts_audio_suite_server
13Pattern from: ltx2_server.py
14"""
16import json
17import logging
18import os
19import socket
20import sys
21import uuid
22from pathlib import Path
23from threading import Lock
25from flask import Flask, request, jsonify, send_file
27try:
28 from integrations.service_tools.vram_manager import clear_cuda_cache
29except ImportError:
30 def clear_cuda_cache():
31 try:
32 import torch
33 if torch.cuda.is_available():
34 torch.cuda.empty_cache()
35 if hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
36 torch.mps.empty_cache()
37 except Exception:
38 pass
40logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
41logger = logging.getLogger('tts_audio_suite_server')
43app = Flask(__name__)
45# Global state
46_model = None
47_model_lock = Lock()
48_model_dir = None
49OUTPUT_DIR = os.path.join(Path.home(), '.hevolve', 'outputs', 'tts_audio_suite')
50os.makedirs(OUTPUT_DIR, exist_ok=True)
53def _get_model_dir():
54 global _model_dir
55 if _model_dir:
56 return _model_dir
57 _model_dir = os.environ.get(
58 'TTS_AUDIO_SUITE_MODEL_DIR',
59 str(Path.home() / '.hevolve' / 'models' / 'tts-audio-suite')
60 )
61 return _model_dir
64def _load_model():
65 """Lazy-load TTS-Audio-Suite model."""
66 global _model
67 if _model is not None:
68 return _model
70 with _model_lock:
71 if _model is not None:
72 return _model
74 model_dir = _get_model_dir()
75 logger.info(f"Loading TTS-Audio-Suite from {model_dir}...")
77 if model_dir not in sys.path:
78 sys.path.insert(0, model_dir)
80 try:
81 offload_mode = os.environ.get('TTS_OFFLOAD', 'gpu')
83 # TTS-Audio-Suite supports multiple TTS backends
84 # Actual loading depends on repo structure
85 _model = {
86 'loaded': True,
87 'model_dir': model_dir,
88 'offload_mode': offload_mode,
89 }
90 logger.info(f"TTS-Audio-Suite loaded (mode: {offload_mode})")
91 return _model
92 except Exception as e:
93 logger.error(f"Failed to load TTS-Audio-Suite: {e}")
94 _model = {'loaded': False, 'error': str(e)}
95 return _model
98@app.route('/health', methods=['GET'])
99def health():
100 """Health check with VRAM stats."""
101 status = {'status': 'ok', 'service': 'tts_audio_suite'}
102 try:
103 import torch
104 if torch.cuda.is_available():
105 status['gpu'] = torch.cuda.get_device_name(0)
106 status['vram_total_gb'] = round(torch.cuda.get_device_properties(0).total_memory / 1e9, 2)
107 status['vram_used_gb'] = round(torch.cuda.memory_allocated(0) / 1e9, 2)
108 except ImportError:
109 pass
110 return jsonify(status)
113@app.route('/synthesize', methods=['POST'])
114def synthesize():
115 """Synthesize speech from text."""
116 data = request.get_json() or {}
117 text = data.get('text', '')
118 model_name = data.get('model', 'default')
119 language = data.get('language', 'en')
121 if not text:
122 return jsonify({'error': 'text is required'}), 400
124 model = _load_model()
125 if not model.get('loaded'):
126 return jsonify({'error': f"Model not loaded: {model.get('error', 'unknown')}"}), 503
128 try:
129 output_id = str(uuid.uuid4())[:8]
130 output_path = os.path.join(OUTPUT_DIR, f"tts_{output_id}.wav")
132 # TODO: Replace with actual TTS-Audio-Suite inference call once repo is cloned
133 return jsonify({
134 'success': True,
135 'audio_url': f"/audio/{output_id}",
136 'text': text,
137 'model': model_name,
138 'language': language,
139 'message': 'TTS-Audio-Suite synthesis placeholder — model integration pending repo clone',
140 })
141 except Exception as e:
142 logger.error(f"Synthesis failed: {e}")
143 return jsonify({'error': str(e)}), 500
146@app.route('/models', methods=['GET'])
147def list_models():
148 """List available TTS models."""
149 return jsonify({
150 'models': [
151 {'name': 'default', 'description': 'Default TTS model', 'languages': ['en']},
152 ],
153 'message': 'Model list placeholder — populated after model download',
154 })
157@app.route('/audio/<filename>', methods=['GET'])
158def serve_audio(filename):
159 """Serve generated audio file."""
160 path = os.path.join(OUTPUT_DIR, f"tts_{filename}.wav")
161 if os.path.exists(path):
162 return send_file(path, mimetype='audio/wav')
163 return jsonify({'error': 'not found'}), 404
166@app.route('/unload', methods=['POST'])
167def unload():
168 """Unload model to free memory."""
169 global _model
170 _model = None
171 clear_cuda_cache()
172 return jsonify({'status': 'unloaded'})
175def _find_free_port() -> int:
176 """Find a free port using OS assignment."""
177 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
178 sock.bind(('127.0.0.1', 0))
179 port = sock.getsockname()[1]
180 sock.close()
181 return port
184if __name__ == '__main__':
185 port = _find_free_port()
186 print(f"PORT={port}", flush=True)
187 app.run(host='127.0.0.1', port=port, threaded=True)