chore(format): run black on dev (#915)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
github-actions[bot]
2025-03-14 12:34:46 +09:00
committed by GitHub
parent d582fd5d70
commit 4090ff2665
3 changed files with 109 additions and 63 deletions
+84 -43
View File
@@ -12,11 +12,12 @@ Main features and improvements:
- Use asyncio.Lock to manage model access, improving concurrency performance
- Load and manage speaker embedding files to support personalized speech synthesis
"""
import io
import os
import sys
import asyncio
import time
import time
from typing import Optional, Dict
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse, JSONResponse
@@ -57,48 +58,72 @@ VOICE_MAP = {
# Allowed audio formats
ALLOWED_FORMATS = {"mp3", "wav", "ogg"}
@app.on_event("startup")
async def startup_event():
"""Load ChatTTS model and default speaker embedding when the application starts"""
# Initialize ChatTTS and async lock
app.state.chat = ChatTTS.Chat(get_logger("ChatTTS"))
app.state.model_lock = asyncio.Lock() # Use async lock instead of thread lock
# Register text normalizers
app.state.chat.normalizer.register("en", normalizer_en_nemo_text())
app.state.chat.normalizer.register("zh", normalizer_zh_tn())
logger.info("Initializing ChatTTS...")
if app.state.chat.load(source="huggingface"):
logger.info("Model loaded successfully.")
else:
logger.error("Model loading failed, exiting application.")
raise RuntimeError("Failed to load ChatTTS model")
# Load default speaker embedding
# Preload all supported speaker embeddings into memory at startup to avoid repeated loading during runtime
app.state.spk_emb_map = {}
for voice, spk_path in VOICE_MAP.items():
if os.path.exists(spk_path):
app.state.spk_emb_map[voice] = torch.load(spk_path, map_location=torch.device("cpu"))
app.state.spk_emb_map[voice] = torch.load(
spk_path, map_location=torch.device("cpu")
)
logger.info(f"Preloading speaker embedding: {voice} -> {spk_path}")
else:
logger.warning(f"Speaker embedding not found: {spk_path}, skipping preload")
app.state.spk_emb = app.state.spk_emb_map.get("default") # Default embedding
# Request parameter whitelist
ALLOWED_PARAMS = {"model", "input", "voice", "response_format", "speed", "stream", "output_format"}
ALLOWED_PARAMS = {
"model",
"input",
"voice",
"response_format",
"speed",
"stream",
"output_format",
}
class OpenAITTSRequest(BaseModel):
"""OpenAI TTS request data model"""
model: str = Field(..., description="Speech synthesis model, fixed as 'tts-1'")
input: str = Field(..., description="Text content to synthesize", max_length=2048) # Length limit
voice: Optional[str] = Field("default", description="Voice selection, supports: default, alloy, echo")
response_format: Optional[str] = Field("mp3", description="Audio format: mp3, wav, ogg")
speed: Optional[float] = Field(1.0, ge=0.5, le=2.0, description="Speed, range 0.5-2.0")
input: str = Field(
..., description="Text content to synthesize", max_length=2048
) # Length limit
voice: Optional[str] = Field(
"default", description="Voice selection, supports: default, alloy, echo"
)
response_format: Optional[str] = Field(
"mp3", description="Audio format: mp3, wav, ogg"
)
speed: Optional[float] = Field(
1.0, ge=0.5, le=2.0, description="Speed, range 0.5-2.0"
)
stream: Optional[bool] = Field(False, description="Whether to stream")
output_format: Optional[str] = "mp3" # Optional formats: mp3, wav, ogg
extra_params: Dict[str, Optional[str]] = Field(default_factory=dict, description="Unsupported extra parameters")
extra_params: Dict[str, Optional[str]] = Field(
default_factory=dict, description="Unsupported extra parameters"
)
@classmethod
def validate_request(cls, request_data: Dict):
@@ -109,6 +134,7 @@ class OpenAITTSRequest(BaseModel):
logger.warning(f"Ignoring unsupported parameters: {unsupported_params}")
return {key: request_data[key] for key in ALLOWED_PARAMS if key in request_data}
# Unified error response
@app.exception_handler(Exception)
async def custom_exception_handler(request, exc):
@@ -116,24 +142,30 @@ async def custom_exception_handler(request, exc):
logger.error(f"Error: {str(exc)}")
return JSONResponse(
status_code=getattr(exc, "status_code", 500),
content={"error": {"message": str(exc), "type": exc.__class__.__name__}}
content={"error": {"message": str(exc), "type": exc.__class__.__name__}},
)
@app.post("/v1/audio/speech")
async def generate_voice(request_data: Dict):
"""Handle speech synthesis request"""
request_data = OpenAITTSRequest.validate_request(request_data)
request = OpenAITTSRequest(**request_data)
logger.info(f"Received request: text={request.input}..., voice={request.voice}, stream={request.stream}")
logger.info(
f"Received request: text={request.input}..., voice={request.voice}, stream={request.stream}"
)
# Validate audio format
if request.response_format not in ALLOWED_FORMATS:
raise HTTPException(400, detail=f"Unsupported audio format: {request.response_format}, supported formats: {', '.join(ALLOWED_FORMATS)}")
raise HTTPException(
400,
detail=f"Unsupported audio format: {request.response_format}, supported formats: {', '.join(ALLOWED_FORMATS)}",
)
# Load speaker embedding for the specified voice
spk_emb = app.state.spk_emb_map.get(request.voice, app.state.spk_emb)
# Inference parameters
params_infer_main = {
"text": [request.input],
@@ -145,13 +177,13 @@ async def generate_voice(request_data: Dict):
"audio_seed": 12345678,
# "text_seed": 87654321, # Random seed for text processing, used to control text refinement
"do_text_normalization": True, # Perform text normalization
"do_homophone_replacement": True, # Perform homophone replacement
"do_homophone_replacement": True, # Perform homophone replacement
}
# Inference code parameters
params_infer_code = app.state.chat.InferCodeParams(
#prompt=f"[speed_{int(request.speed * 10)}]", # Convert to format supported by ChatTTS
prompt="[speed_5]",
# prompt=f"[speed_{int(request.speed * 10)}]", # Convert to format supported by ChatTTS
prompt="[speed_5]",
top_P=0.5,
top_K=10,
temperature=0.1,
@@ -166,21 +198,21 @@ async def generate_voice(request_data: Dict):
txt_smp=None,
stream_batch=24,
stream_speed=12000,
pass_first_n_batches=2
pass_first_n_batches=2,
)
try:
async with app.state.model_lock:
wavs = app.state.chat.infer(
text = params_infer_main["text"],
stream = params_infer_main["stream"],
lang = params_infer_main["lang"],
skip_refine_text = params_infer_main["skip_refine_text"],
use_decoder = params_infer_main["use_decoder"],
do_text_normalization = params_infer_main["do_text_normalization"],
do_homophone_replacement = params_infer_main['do_homophone_replacement'],
# params_refine_text = params_refine_text,
params_infer_code=params_infer_code,
text=params_infer_main["text"],
stream=params_infer_main["stream"],
lang=params_infer_main["lang"],
skip_refine_text=params_infer_main["skip_refine_text"],
use_decoder=params_infer_main["use_decoder"],
do_text_normalization=params_infer_main["do_text_normalization"],
do_homophone_replacement=params_infer_main["do_homophone_replacement"],
# params_refine_text = params_refine_text,
params_infer_code=params_infer_code,
)
except Exception as e:
raise HTTPException(500, detail=f"Speech synthesis failed: {str(e)}")
@@ -189,7 +221,7 @@ async def generate_voice(request_data: Dict):
"""Generate WAV file header (without data length)"""
header = bytearray()
header.extend(b"RIFF")
header.extend(b"\xFF\xFF\xFF\xFF") # File size unknown
header.extend(b"\xff\xff\xff\xff") # File size unknown
header.extend(b"WAVEfmt ")
header.extend((16).to_bytes(4, "little")) # fmt chunk size
header.extend((1).to_bytes(2, "little")) # PCM format
@@ -201,7 +233,7 @@ async def generate_voice(request_data: Dict):
header.extend((block_align).to_bytes(2, "little")) # Block align
header.extend((bits_per_sample).to_bytes(2, "little")) # Bits per sample
header.extend(b"data")
header.extend(b"\xFF\xFF\xFF\xFF") # Data size unknown
header.extend(b"\xff\xff\xff\xff") # Data size unknown
return bytes(header)
# Handle audio output format
@@ -210,14 +242,17 @@ async def generate_voice(request_data: Dict):
if format == "mp3":
return pcm_arr_to_mp3_view(wav)
elif format == "wav":
return pcm_arr_to_wav_view(wav, include_header=False) # No header in streaming
return pcm_arr_to_wav_view(
wav, include_header=False
) # No header in streaming
elif format == "ogg":
return pcm_arr_to_ogg_view(wav)
return pcm_arr_to_mp3_view(wav)
return pcm_arr_to_mp3_view(wav)
# Return streaming audio data
if request.stream:
first_chunk = True
async def audio_stream():
nonlocal first_chunk
for wav in wavs:
@@ -225,20 +260,26 @@ async def generate_voice(request_data: Dict):
yield generate_wav_header() # Send WAV header
first_chunk = False
yield convert_audio(wav, request.response_format)
media_type = "audio/wav" if request.response_format == "wav" else "audio/mpeg"
return StreamingResponse(audio_stream(), media_type=media_type)
# Return audio file directly
if request.response_format == 'wav':
if request.response_format == "wav":
music_data = pcm_arr_to_wav_view(wavs[0])
else:
music_data = convert_audio(wavs[0], request.response_format)
return StreamingResponse(io.BytesIO(music_data), media_type="audio/mpeg", headers={
"Content-Disposition": f"attachment; filename=output.{request.response_format}"
})
return StreamingResponse(
io.BytesIO(music_data),
media_type="audio/mpeg",
headers={
"Content-Disposition": f"attachment; filename=output.{request.response_format}"
},
)
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "healthy", "model_loaded": bool(app.state.chat)}
return {"status": "healthy", "model_loaded": bool(app.state.chat)}
+13 -14
View File
@@ -29,23 +29,20 @@
"from IPython.display import Audio, display\n",
"\n",
"# Initialize the client\n",
"client = OpenAI(\n",
" api_key=\"dummy-key\",\n",
" base_url=\"http://localhost:8000/v1\"\n",
")\n",
"client = OpenAI(api_key=\"dummy-key\", base_url=\"http://localhost:8000/v1\")\n",
"\n",
"# Generate audio\n",
"response = client.audio.speech.create(\n",
" model=\"tts-1\",\n",
" voice=\"echo\",\n",
" input= \"\"\"\n",
" input=\"\"\"\n",
" 以下是一些中英文对照的话语。 \n",
" 1. 早上好!希望你有美好的一天。Good morning! Wish you a wonderful day. \n",
" 2. 你好呀,最近怎么样?Hello there, how have you been recently? \n",
" 3. 别放弃,你能做到的!Don't give up, you can do it! \n",
" 4. 继续努力,你的付出会有回报的。Keep up the good work, your efforts will pay off.\n",
" \"\"\",\n",
" response_format=\"wav\"\n",
" response_format=\"wav\",\n",
")\n",
"\n",
"# Get audio binary data\n",
@@ -101,19 +98,21 @@
" 4. 继续努力,你的付出会有回报的。Keep up the good work, your efforts will pay off.\n",
" \"\"\",\n",
" \"voice\": \"echo\",\n",
" \"response_format\": \"wav\", \n",
" \"stream\": True\n",
" \"response_format\": \"wav\",\n",
" \"stream\": True,\n",
"}\n",
"\n",
"try:\n",
" response = requests.post(\"http://localhost:8000/v1/audio/speech\", json=payload, stream=True)\n",
" response = requests.post(\n",
" \"http://localhost:8000/v1/audio/speech\", json=payload, stream=True\n",
" )\n",
" response.raise_for_status() # Check the status code\n",
" \n",
"\n",
" audio_buffer = io.BytesIO()\n",
" for chunk in response.iter_content(chunk_size=8192):\n",
" if chunk:\n",
" audio_buffer.write(chunk)\n",
" \n",
"\n",
" audio_buffer.seek(0)\n",
" display(Audio(audio_buffer.getvalue(), autoplay=False))\n",
" print(\"Audio has been loaded into the Notebook and can be played manually\")\n",
@@ -462,7 +461,7 @@
" 'curl -X POST \"http://localhost:8000/v1/audio/speech\" '\n",
" '-H \"Content-Type: application/json\" '\n",
" '-d \\'{\"model\": \"tts-1\", \"input\": \"以下是一些中英文对照的话语。 1. 早上好!希望你有美好的一天。Good morning! Wish you a wonderful day. 2. 你好呀,最近怎么样?Hello there, how have you been recently? 3. 别放弃,你能做到的!Dont give up, you can do it! 4. 继续努力,你的付出会有回报的。Keep up the good work, your efforts will pay off.\", \"voice\": \"echo\", \"response_format\": \"wav\", \"stream\": true}\\' '\n",
" '-s | mpv --no-video -'\n",
" \"-s | mpv --no-video -\"\n",
")\n",
"subprocess.run(cmd, shell=True, check=True)"
]
@@ -1125,7 +1124,7 @@
" 'curl -X POST \"http://localhost:8000/v1/audio/speech\" '\n",
" '-H \"Content-Type: application/json\" '\n",
" '-d \\'{\"model\": \"tts-1\", \"input\": \"以下是一些中英文对照的话语。 1. 早上好!希望你有美好的一天。Good morning! Wish you a wonderful day. 2. 你好呀,最近怎么样?Hello there, how have you been recently? 3. 别放弃,你能做到的!Dont give up, you can do it! 4. 继续努力,你的付出会有回报的。Keep up the good work, your efforts will pay off.\", \"voice\": \"echo\", \"response_format\": \"mp3\", \"stream\": true}\\' '\n",
" '-s | mpv --no-video -'\n",
" \"-s | mpv --no-video -\"\n",
")\n",
"subprocess.run(cmd, shell=True, check=True)"
]
@@ -1690,7 +1689,7 @@
" 'curl -X POST \"http://localhost:8000/v1/audio/speech\" '\n",
" '-H \"Content-Type: application/json\" '\n",
" '-d \\'{\"model\": \"tts-1\", \"input\": \"以下是一些中英文对照的话语。 1. 早上好!希望你有美好的一天。Good morning! Wish you a wonderful day. 2. 你好呀,最近怎么样?Hello there, how have you been recently? 3. 别放弃,你能做到的!Dont give up, you can do it! 4. 继续努力,你的付出会有回报的。Keep up the good work, your efforts will pay off.\", \"voice\": \"echo\", \"response_format\": \"ogg\", \"stream\": true}\\' '\n",
" '-s | mpv --no-video -'\n",
" \"-s | mpv --no-video -\"\n",
")\n",
"subprocess.run(cmd, shell=True, check=True)"
]
+12 -6
View File
@@ -4,6 +4,7 @@ import numpy as np
from .np import float_to_int16
from .av import wav2
def _pcm_to_wav_buffer(wav: np.ndarray, sample_rate: int = 24000) -> BytesIO:
"""
Convert PCM audio data to a WAV format byte stream (internal utility function).
@@ -14,7 +15,7 @@ def _pcm_to_wav_buffer(wav: np.ndarray, sample_rate: int = 24000) -> BytesIO:
"""
# Create an in-memory byte stream buffer
buf = BytesIO()
# Open a WAV file stream in write mode
with wave.open(buf, "wb") as wf:
# Set number of channels to 1 (mono)
@@ -25,11 +26,12 @@ def _pcm_to_wav_buffer(wav: np.ndarray, sample_rate: int = 24000) -> BytesIO:
wf.setframerate(sample_rate)
# Convert PCM to 16-bit integer and write
wf.writeframes(float_to_int16(wav))
# Reset buffer pointer to the beginning
buf.seek(0, 0)
return buf
def pcm_arr_to_mp3_view(wav: np.ndarray, sample_rate: int = 24000) -> memoryview:
"""
Convert PCM audio data to MP3 format.
@@ -40,7 +42,7 @@ def pcm_arr_to_mp3_view(wav: np.ndarray, sample_rate: int = 24000) -> memoryview
"""
# Get WAV format byte stream
buf = _pcm_to_wav_buffer(wav, sample_rate)
# Create output buffer
buf2 = BytesIO()
# Convert WAV data to MP3
@@ -48,6 +50,7 @@ def pcm_arr_to_mp3_view(wav: np.ndarray, sample_rate: int = 24000) -> memoryview
# Return MP3 data
return buf2.getbuffer()
def pcm_arr_to_ogg_view(wav: np.ndarray, sample_rate: int = 24000) -> memoryview:
"""
Convert PCM audio data to OGG format (using Vorbis encoding).
@@ -58,7 +61,7 @@ def pcm_arr_to_ogg_view(wav: np.ndarray, sample_rate: int = 24000) -> memoryview
"""
# Get WAV format byte stream
buf = _pcm_to_wav_buffer(wav, sample_rate)
# Create output buffer
buf2 = BytesIO()
# Convert WAV data to OGG
@@ -66,7 +69,10 @@ def pcm_arr_to_ogg_view(wav: np.ndarray, sample_rate: int = 24000) -> memoryview
# Return OGG data
return buf2.getbuffer()
def pcm_arr_to_wav_view(wav: np.ndarray, sample_rate: int = 24000, include_header: bool = True) -> memoryview:
def pcm_arr_to_wav_view(
wav: np.ndarray, sample_rate: int = 24000, include_header: bool = True
) -> memoryview:
"""
Convert PCM audio data to WAV format, with an option to include header.
@@ -82,4 +88,4 @@ def pcm_arr_to_wav_view(wav: np.ndarray, sample_rate: int = 24000, include_heade
else:
# Return only converted 16-bit PCM data
pcm_data = float_to_int16(wav)
return memoryview(pcm_data.tobytes())
return memoryview(pcm_data.tobytes())