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:
2025-12-09 14:17:58 +00:00
parent a8636bcf71
commit 69eb0b3507
4 changed files with 298 additions and 22 deletions

View File

@@ -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):
"""