File size: 4,786 Bytes
35bb6f4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | from __future__ import annotations
from typing import AsyncGenerator
import numpy as np
from loguru import logger
from api.src.core.config import settings
from api.src.core.model_config import BackendType, get_backbone_info
from api.src.inference.model_manager import ModelManager
from api.src.inference.text_chunker import chunk_text
from api.src.inference.voice_manager import VoiceManager
from api.src.services.streaming_audio_writer import (
AudioFormat,
StreamingAudioWriter,
encode_audio_complete,
)
from api.src.structures.schemas import OpenAISpeechRequest
def _apply_speed(wav: np.ndarray, speed: float) -> np.ndarray:
"""Adjust audio playback speed via resampling."""
if speed == 1.0:
return wav
# Resample: fewer samples = faster, more samples = slower
new_length = int(len(wav) / speed)
indices = np.linspace(0, len(wav) - 1, new_length)
return np.interp(indices, np.arange(len(wav)), wav).astype(np.float32)
class TTSService:
_instance: TTSService | None = None
def __init__(self) -> None:
self._model_manager = ModelManager.get_instance()
self._voice_manager = VoiceManager.get_instance()
@classmethod
def get_instance(cls) -> TTSService:
if cls._instance is None:
cls._instance = cls()
return cls._instance
async def generate_speech(self, request: OpenAISpeechRequest) -> bytes:
"""Generate complete audio from text."""
model_id = request.model
voice = request.voice
fmt: AudioFormat = request.response_format
# Ensure model is loaded
if not self._model_manager.is_loaded(model_id):
raise ValueError(f"Model '{model_id}' is not loaded")
loaded = self._model_manager.loaded_models[model_id]
# Get voice reference
ref_codes = await self._voice_manager.get_or_encode_ref_codes(
voice, loaded.codec_id, self._model_manager, model_id
)
ref_text = self._voice_manager.get_ref_text(voice)
# For short text, single inference
text = request.input.strip()
info = get_backbone_info(model_id)
if len(text) <= 500 or info is None:
wav = await self._model_manager.infer(model_id, text, ref_codes, ref_text)
wav = _apply_speed(wav, request.speed)
return encode_audio_complete(wav, fmt, settings.sample_rate)
# For long text, chunk and concatenate
chunks = chunk_text(text)
wav_parts: list[np.ndarray] = []
for chunk in chunks:
wav = await self._model_manager.infer(model_id, chunk, ref_codes, ref_text)
wav_parts.append(wav)
full_wav = np.concatenate(wav_parts)
full_wav = _apply_speed(full_wav, request.speed)
return encode_audio_complete(full_wav, fmt, settings.sample_rate)
async def stream_speech(
self, request: OpenAISpeechRequest
) -> AsyncGenerator[bytes, None]:
"""Stream audio chunks as they are generated."""
model_id = request.model
voice = request.voice
fmt: AudioFormat = request.response_format
if not self._model_manager.is_loaded(model_id):
raise ValueError(f"Model '{model_id}' is not loaded")
loaded = self._model_manager.loaded_models[model_id]
info = get_backbone_info(model_id)
ref_codes = await self._voice_manager.get_or_encode_ref_codes(
voice, loaded.codec_id, self._model_manager, model_id
)
ref_text = self._voice_manager.get_ref_text(voice)
text = request.input.strip()
writer = StreamingAudioWriter(fmt, settings.sample_rate)
speed = request.speed
try:
if info and info.supports_streaming:
# Real streaming with GGUF models
async for chunk in self._model_manager.infer_stream(
model_id, text, ref_codes, ref_text
):
chunk = _apply_speed(chunk, speed)
encoded = writer.write_chunk(chunk)
if encoded:
yield encoded
else:
# Pseudo-streaming: chunk text, infer per chunk
chunks = chunk_text(text)
for chunk in chunks:
wav = await self._model_manager.infer(
model_id, chunk, ref_codes, ref_text
)
wav = _apply_speed(wav, speed)
encoded = writer.write_chunk(wav)
if encoded:
yield encoded
# Finalize
final = writer.finalize()
if final:
yield final
finally:
writer.close()
|