Add voice preview and age category modifiers
Features: - Voice preview button to test voices before generation - Age category selection (young, teen, adult, old) - Age categories adjust voice speed automatically: - Young: 1.15x (faster, energetic) - Teen: 1.08x (slightly faster) - Adult: 1.0x (normal) - Old: 0.85x (slower, measured) - Preview caching to avoid regenerating same voice - Backend /api/tts/preview endpoint for voice samples - Frontend previewVoice API function This allows users to hear how each voice sounds and adjust character ages to make them sound younger or older.
This commit is contained in:
@@ -62,6 +62,13 @@ class TTSRequest(BaseModel):
|
||||
roles: List[RoleVoiceMapping] = Field(..., description="Role to voice mappings")
|
||||
speed: Optional[float] = Field(1.0, description="Playback speed (0.5-1.5)")
|
||||
|
||||
class VoicePreviewRequest(BaseModel):
|
||||
"""Request model for voice preview endpoint."""
|
||||
voice: str = Field(..., description="Kokoro voice ID to preview")
|
||||
lang: str = Field(..., description="Language code (a, b, f, j)")
|
||||
speed: Optional[float] = Field(1.0, description="Playback speed (0.5-1.5)")
|
||||
text: Optional[str] = Field(None, description="Custom text to preview (uses default if not provided)")
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""Standard error response model."""
|
||||
error: str = Field(..., description="Error type")
|
||||
@@ -234,6 +241,90 @@ async def generate_script(request: ScriptRequest):
|
||||
}
|
||||
)
|
||||
|
||||
@app.post("/api/tts/preview")
|
||||
async def preview_voice(request: VoicePreviewRequest):
|
||||
"""
|
||||
Generates a short audio preview of a voice.
|
||||
|
||||
Args:
|
||||
request: VoicePreviewRequest containing voice ID, language, speed, and optional text
|
||||
|
||||
Returns:
|
||||
StreamingResponse with audio/wav content
|
||||
|
||||
Raises:
|
||||
400: If validation fails
|
||||
500: If audio generation fails
|
||||
"""
|
||||
logger.info(f"Voice preview requested: voice={request.voice}, lang={request.lang}, speed={request.speed}")
|
||||
|
||||
# Validate language code
|
||||
supported_langs = ['a', 'b', 'e', 'f', 'h', 'i', 'j', 'p', 'z']
|
||||
if request.lang not in supported_langs:
|
||||
logger.error(f"Invalid language code: {request.lang}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={
|
||||
"error": "ValidationError",
|
||||
"message": f"Invalid language code: {request.lang}",
|
||||
"details": {"field": "lang", "supported": supported_langs}
|
||||
}
|
||||
)
|
||||
|
||||
# Validate speed range
|
||||
if request.speed < 0.5 or request.speed > 1.5:
|
||||
logger.error(f"Invalid speed value: {request.speed}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={
|
||||
"error": "ValidationError",
|
||||
"message": "Speed must be between 0.5 and 1.5",
|
||||
"details": {"field": "speed", "value": request.speed, "range": [0.5, 1.5]}
|
||||
}
|
||||
)
|
||||
|
||||
# Use provided text or default preview text
|
||||
preview_text = request.text or "Hello! This is a preview of this voice. How does it sound?"
|
||||
|
||||
try:
|
||||
# Get pipeline for language
|
||||
pipeline = tts_service.get_pipeline(request.lang)
|
||||
|
||||
# Generate preview audio
|
||||
audio = tts_service.synthesize_segment(
|
||||
pipeline=pipeline,
|
||||
text=preview_text,
|
||||
voice=request.voice,
|
||||
speed=request.speed
|
||||
)
|
||||
|
||||
# Convert to WAV bytes
|
||||
import io
|
||||
import soundfile as sf
|
||||
buffer = io.BytesIO()
|
||||
sf.write(buffer, audio, tts_service.SAMPLE_RATE, format='WAV')
|
||||
buffer.seek(0)
|
||||
audio_bytes = buffer.read()
|
||||
|
||||
logger.info(f"Generated voice preview: {len(audio_bytes)} bytes")
|
||||
|
||||
return StreamingResponse(
|
||||
iter([audio_bytes]),
|
||||
media_type="audio/wav",
|
||||
headers={"Content-Disposition": "inline; filename=preview.wav"}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during voice preview: {str(e)}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content={
|
||||
"error": "PreviewGenerationError",
|
||||
"message": "Failed to generate voice preview",
|
||||
"details": {"error": str(e)}
|
||||
}
|
||||
)
|
||||
|
||||
@app.post("/api/tts")
|
||||
async def generate_audiobook(request: TTSRequest):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user