server.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. #!/usr/bin/env python3
  2. import asyncio
  3. import base64
  4. import json
  5. import argparse
  6. import signal
  7. import sys
  8. import os
  9. import io
  10. import wave
  11. import hashlib
  12. from concurrent.futures import ThreadPoolExecutor
  13. from functools import lru_cache
  14. from typing import Set, Optional, Tuple, List
  15. import numpy as np
  16. import websockets
  17. from websockets.server import WebSocketServerProtocol
  18. sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buffering=1)
  19. class AudioCache:
  20. def __init__(self, max_size: int = 100):
  21. self.cache = {}
  22. self.access_order = []
  23. self.max_size = max_size
  24. def _make_key(self, text: str, ref_audio_hash: str) -> str:
  25. return hashlib.md5(f"{ref_audio_hash}:{text}".encode()).hexdigest()
  26. def get(self, text: str, ref_audio_hash: str) -> Optional[bytes]:
  27. key = self._make_key(text, ref_audio_hash)
  28. if key in self.cache:
  29. self.access_order.remove(key)
  30. self.access_order.append(key)
  31. return self.cache[key]
  32. return None
  33. def put(self, text: str, ref_audio_hash: str, audio_data: bytes):
  34. key = self._make_key(text, ref_audio_hash)
  35. if key in self.cache:
  36. self.access_order.remove(key)
  37. elif len(self.cache) >= self.max_size:
  38. oldest = self.access_order.pop(0)
  39. del self.cache[oldest]
  40. self.cache[key] = audio_data
  41. self.access_order.append(key)
  42. class TTSEngine:
  43. def __init__(self, model_id: str = "Qwen/Qwen3-TTS-12Hz-0.6B-Base",
  44. ref_audio_path: str = None, chunk_size: int = 8):
  45. from faster_qwen3_tts import FasterQwen3TTS
  46. self.model_id = model_id
  47. self.sample_rate = 24000
  48. self.ref_audio_path = ref_audio_path
  49. self.ref_audio_hash = None
  50. self.chunk_size = chunk_size
  51. self.audio_cache = AudioCache(max_size=100)
  52. print(f"Loading FasterQwen3-TTS model: {model_id}")
  53. self.model = FasterQwen3TTS.from_pretrained(model_id)
  54. print("Model loaded successfully")
  55. if self.ref_audio_path and os.path.exists(self.ref_audio_path):
  56. self.prompt_items = self._create_prompt(self.ref_audio_path)
  57. self.ref_audio_hash = hashlib.md5(open(self.ref_audio_path, 'rb').read()).hexdigest()
  58. print(f"Reference audio loaded: {self.ref_audio_path}")
  59. else:
  60. self.prompt_items = None
  61. print("Warning: No reference audio provided")
  62. def _create_prompt(self, path: str):
  63. return self.model.model.create_voice_clone_prompt(
  64. ref_audio=path,
  65. ref_text="",
  66. x_vector_only_mode=True
  67. )
  68. def _audio_to_wav(self, audio_data, sample_rate: int = None) -> bytes:
  69. if sample_rate is None:
  70. sample_rate = self.sample_rate
  71. if isinstance(audio_data, list):
  72. audio_array = np.concatenate(audio_data) if len(audio_data) > 0 else np.array([])
  73. else:
  74. audio_array = audio_data
  75. if audio_array.dtype != np.int16:
  76. audio_array = (audio_array * 32767).astype(np.int16)
  77. wav_io = io.BytesIO()
  78. with wave.open(wav_io, 'wb') as wav_file:
  79. wav_file.setnchannels(1)
  80. wav_file.setsampwidth(2)
  81. wav_file.setframerate(sample_rate)
  82. wav_file.writeframes(audio_array.tobytes())
  83. return wav_io.getvalue()
  84. def synthesize_streaming(self, text: str, chunk_index: int,
  85. ref_audio_path: str = None) -> List[Tuple[int, bytes]]:
  86. if self.audio_cache.get(text, self.ref_audio_hash or ""):
  87. cached = self.audio_cache.get(text, self.ref_audio_hash or "")
  88. return [(chunk_index, cached)]
  89. if ref_audio_path:
  90. prompt = self._create_prompt(ref_audio_path)
  91. elif self.prompt_items is not None:
  92. prompt = self.prompt_items
  93. else:
  94. raise ValueError("Reference audio required for Base model")
  95. audio_chunks = []
  96. for audio_chunk, sr, timing in self.model.generate_voice_clone_streaming(
  97. text=text,
  98. language="Chinese",
  99. voice_clone_prompt=prompt,
  100. chunk_size=self.chunk_size,
  101. x_vector_only_mode=True,
  102. ):
  103. wav_data = self._audio_to_wav(audio_chunk, sr)
  104. audio_chunks.append((chunk_index, wav_data))
  105. if audio_chunks:
  106. full_audio = self._audio_to_wav(np.concatenate([
  107. np.frombuffer(c[1][44:], dtype=np.int16) for c in audio_chunks
  108. ]) if len(audio_chunks) > 1 else np.frombuffer(audio_chunks[0][1][44:], dtype=np.int16),
  109. self.sample_rate)
  110. self.audio_cache.put(text, self.ref_audio_hash or "", full_audio)
  111. return audio_chunks
  112. def synthesize(self, text: str, ref_audio_path: str = None) -> bytes:
  113. audio_chunks = self.synthesize_streaming(text, 0, ref_audio_path)
  114. if audio_chunks:
  115. return audio_chunks[0][1]
  116. return b''
  117. def get_model_info(self) -> dict:
  118. return {
  119. 'model_id': self.model_id,
  120. 'sample_rate': self.sample_rate,
  121. 'has_ref_audio': self.prompt_items is not None,
  122. 'cache_size': len(self.audio_cache.cache),
  123. 'streaming': True
  124. }
  125. class TTSServer:
  126. def __init__(
  127. self,
  128. model_id: str = "Qwen/Qwen3-TTS-12Hz-0.6B-Base",
  129. port: int = 8766,
  130. host: str = "localhost",
  131. ref_audio_path: str = None,
  132. max_workers: int = 4,
  133. chunk_size: int = 8
  134. ):
  135. self.port = port
  136. self.host = host
  137. self.ref_audio_path = ref_audio_path
  138. self.clients: Set[WebSocketServerProtocol] = set()
  139. self.is_running = True
  140. self.executor = ThreadPoolExecutor(max_workers=max_workers)
  141. self.chunk_size = chunk_size
  142. print(f"Initializing TTS engine...")
  143. self.engine = TTSEngine(model_id, ref_audio_path, chunk_size)
  144. print(f"TTS Engine ready (max_workers={max_workers}, chunk_size={chunk_size})")
  145. async def register(self, websocket: WebSocketServerProtocol):
  146. self.clients.add(websocket)
  147. print(f"Client connected: {websocket.remote_address}")
  148. await self.send_message(websocket, {
  149. "type": "connected",
  150. "model": self.engine.get_model_info()
  151. })
  152. async def unregister(self, websocket: WebSocketServerProtocol):
  153. self.clients.discard(websocket)
  154. print(f"Client disconnected: {websocket.remote_address}")
  155. async def send_message(self, websocket: WebSocketServerProtocol, message: dict):
  156. if websocket in self.clients:
  157. try:
  158. await websocket.send(json.dumps(message, ensure_ascii=False))
  159. except websockets.exceptions.ConnectionClosed:
  160. await self.unregister(websocket)
  161. async def send_audio_chunk(self, websocket: WebSocketServerProtocol,
  162. audio_data: bytes, chunk_index: int, is_first: bool, is_last: bool):
  163. audio_base64 = base64.b64encode(audio_data).decode('utf-8')
  164. await self.send_message(websocket, {
  165. "type": "audio",
  166. "data": audio_base64,
  167. "format": "wav",
  168. "sample_rate": self.engine.sample_rate,
  169. "chunk_index": chunk_index,
  170. "is_first": is_first,
  171. "is_last": is_last
  172. })
  173. async def handle_synthesize(self, websocket: WebSocketServerProtocol,
  174. text: str, ref_audio: str = None, chunk_index: int = -1):
  175. loop = asyncio.get_event_loop()
  176. try:
  177. print(f"Starting streaming synthesis for chunk {chunk_index}: {text[:30]}...")
  178. audio_chunks = await loop.run_in_executor(
  179. self.executor,
  180. self.engine.synthesize_streaming,
  181. text,
  182. chunk_index,
  183. ref_audio
  184. )
  185. for i, (_, wav_data) in enumerate(audio_chunks):
  186. await self.send_audio_chunk(
  187. websocket, wav_data, chunk_index,
  188. is_first=(i == 0), is_last=(i == len(audio_chunks) - 1)
  189. )
  190. print(f"Streamed audio chunk {chunk_index}.{i}")
  191. print(f"Finished streaming for chunk {chunk_index}")
  192. except Exception as e:
  193. print(f"Synthesis error for chunk {chunk_index}: {e}")
  194. await self.send_message(websocket, {
  195. "type": "error",
  196. "message": str(e),
  197. "chunk_index": chunk_index
  198. })
  199. async def handle_message(self, websocket: WebSocketServerProtocol, message: dict):
  200. msg_type = message.get("type")
  201. if msg_type == "synthesize":
  202. text = message.get("text", "")
  203. ref_audio = message.get("ref_audio")
  204. chunk_index = message.get("chunk_index", -1)
  205. if not text:
  206. await self.send_message(websocket, {
  207. "type": "error",
  208. "message": "No text provided"
  209. })
  210. return
  211. asyncio.create_task(self.handle_synthesize(websocket, text, ref_audio, chunk_index))
  212. elif msg_type == "voices":
  213. await self.send_message(websocket, {
  214. "type": "voices",
  215. "voices": ["default"]
  216. })
  217. elif msg_type == "cache_stats":
  218. await self.send_message(websocket, {
  219. "type": "cache_stats",
  220. "cache_size": len(self.engine.audio_cache.cache),
  221. "max_size": self.engine.audio_cache.max_size
  222. })
  223. async def handler(self, websocket: WebSocketServerProtocol):
  224. await self.register(websocket)
  225. try:
  226. async for raw_message in websocket:
  227. try:
  228. if isinstance(raw_message, str):
  229. message = json.loads(raw_message)
  230. await self.handle_message(websocket, message)
  231. except json.JSONDecodeError:
  232. print(f"Invalid JSON from {websocket.remote_address}")
  233. except Exception as e:
  234. print(f"Error handling message: {e}")
  235. except websockets.exceptions.ConnectionClosed:
  236. pass
  237. finally:
  238. await self.unregister(websocket)
  239. async def start(self):
  240. print(f"Starting TTS server on {self.host}:{self.port}")
  241. async with websockets.serve(self.handler, self.host, self.port):
  242. await asyncio.Future()
  243. def run(self):
  244. asyncio.run(self.start())
  245. def main():
  246. parser = argparse.ArgumentParser(description="TTS WebSocket Server (Streaming)")
  247. parser.add_argument("--model", "-m", default="Qwen/Qwen3-TTS-12Hz-0.6B-Base")
  248. parser.add_argument("--port", "-p", type=int, default=8766)
  249. parser.add_argument("--host", default="localhost")
  250. parser.add_argument("--ref-audio", "-r", default=None)
  251. parser.add_argument("--workers", "-w", type=int, default=4)
  252. parser.add_argument("--chunk-size", "-c", type=int, default=8,
  253. help="Streaming chunk size (steps). Smaller = lower latency but more overhead")
  254. args = parser.parse_args()
  255. server = TTSServer(
  256. model_id=args.model,
  257. port=args.port,
  258. host=args.host,
  259. ref_audio_path=args.ref_audio,
  260. max_workers=args.workers,
  261. chunk_size=args.chunk_size
  262. )
  263. def signal_handler(sig, frame):
  264. print("\nShutting down server...")
  265. server.is_running = False
  266. sys.exit(0)
  267. signal.signal(signal.SIGINT, signal_handler)
  268. signal.signal(signal.SIGTERM, signal_handler)
  269. server.run()
  270. if __name__ == "__main__":
  271. main()