diff --git a/backend/main.py b/backend/main.py index 59fb9e18..9a90bcec 100644 --- a/backend/main.py +++ b/backend/main.py @@ -34,6 +34,10 @@ app = FastAPI( title="voicebox API", description="Production-quality Qwen3-TTS voice cloning API", version=__version__, + servers=[ + {"url": "http://localhost:8000", "description": "Local development server"}, + {"url": "http://localhost:17493", "description": "Production server"}, + ], ) # CORS middleware diff --git a/docs2/app/global.css b/docs2/app/global.css index 25072e9d..30af24fa 100644 --- a/docs2/app/global.css +++ b/docs2/app/global.css @@ -2,3 +2,13 @@ @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; @import 'fumadocs-openapi/css/preset.css'; + +:root { + --color-fd-primary: hsl(43, 50%, 50%); + --color-fd-primary-foreground: hsl(222.2, 47.4%, 11.2%); +} + +.dark { + --color-fd-primary: hsl(43, 50%, 45%); + --color-fd-primary-foreground: hsl(0, 0%, 95%); +} diff --git a/docs2/content/docs/api/authentication.mdx b/docs2/content/docs/api/authentication.mdx deleted file mode 100644 index e4e72808..00000000 --- a/docs2/content/docs/api/authentication.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: "Authentication" -description: "API authentication and security" ---- - -## Current Status - - - Authentication is not currently implemented in Voicebox. The API is intended for local use only. - - -## Local Usage - -For local development and usage: -- API runs on `localhost:17493` -- No authentication required -- Access restricted to local machine - -## Future Implementation - -Authentication will be added in a future release for: -- Remote deployments -- Multi-user access -- Production environments - -Planned authentication methods: -- API keys -- OAuth 2.0 -- JWT tokens - -## Security Best Practices - -Until authentication is implemented: - - - - Use WireGuard or Tailscale for remote access - - - Run behind nginx with basic auth - - - Restrict access to trusted IPs only - - - Don't expose to public internet - - - -## Coming Soon - -- API key management -- User accounts -- Rate limiting -- Access control diff --git a/docs2/content/docs/api/generation.mdx b/docs2/content/docs/api/generation.mdx deleted file mode 100644 index c9fff59c..00000000 --- a/docs2/content/docs/api/generation.mdx +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: "Generation API" -description: "Generate speech from text" ---- - -## Generate Speech - -```http -POST /generate -``` - -**Request:** -```json -{ - "text": "Hello world", - "profile_id": "abc123", - "language": "en" -} -``` - -**Response:** -```json -{ - "id": "gen123", - "text": "Hello world", - "profile_id": "abc123", - "language": "en", - "audio_url": "/audio/gen123.wav", - "duration": 2.3, - "created_at": "2024-01-29T12:00:00Z" -} -``` - -## List History - -```http -GET /history -``` - -**Query Parameters:** -- `profile_id` (optional) - Filter by voice profile -- `limit` (optional) - Number of results (default: 50) -- `offset` (optional) - Pagination offset - -**Response:** -```json -{ - "generations": [ - { - "id": "gen123", - "text": "Hello world", - "profile_id": "abc123", - "duration": 2.3, - "created_at": "2024-01-29T12:00:00Z" - } - ], - "total": 100 -} -``` - -## Get Generation - -```http -GET /history/{id} -``` - -**Response:** -```json -{ - "id": "gen123", - "text": "Hello world", - "profile_id": "abc123", - "language": "en", - "audio_url": "/audio/gen123.wav", - "duration": 2.3, - "created_at": "2024-01-29T12:00:00Z" -} -``` - -## Delete Generation - -```http -DELETE /history/{id} -``` - -**Response:** -```json -{ - "success": true -} -``` - -## TypeScript Example - -```typescript -import { VoiceboxClient } from '@/lib/api' - -const client = new VoiceboxClient({ - baseUrl: 'http://localhost:17493' -}) - -// Generate speech -const generation = await client.generate({ - text: 'Hello world', - profile_id: 'abc123', - language: 'en' -}) - -// Get audio URL -const audioUrl = generation.audio_url - -// List history -const history = await client.listHistory({ - profile_id: 'abc123', - limit: 20 -}) -``` - -For full API documentation, visit `http://localhost:17493/docs` when the server is running. diff --git a/docs2/content/docs/api/meta.json b/docs2/content/docs/api/meta.json deleted file mode 100644 index 154992f5..00000000 --- a/docs2/content/docs/api/meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "title": "API Reference", - "pages": [ - "overview", - "authentication", - "voice-profiles", - "generation", - "recordings" - ] -} diff --git a/docs2/content/docs/api/overview.mdx b/docs2/content/docs/api/overview.mdx deleted file mode 100644 index c862a332..00000000 --- a/docs2/content/docs/api/overview.mdx +++ /dev/null @@ -1,219 +0,0 @@ ---- -title: "API Overview" -description: "Integrate voice synthesis into your applications with the Voicebox REST API" ---- - -## Introduction - -Voicebox exposes a full REST API that allows you to integrate voice synthesis into your own applications. The API runs on `http://localhost:17493` by default. - - - When Voicebox is running, visit the auto-generated API documentation at `http://localhost:17493/docs` - - -## Base URL - -``` -http://localhost:17493 -``` - -For remote deployments, replace `localhost` with your server's IP or hostname. - -## Authentication - - - Currently, the API does not require authentication for local development. Authentication will be added in a future release for production deployments. - - -## Quick Example - -Here's a simple example of generating speech: - -```bash -# Generate speech -curl -X POST http://localhost:17493/generate \ - -H "Content-Type: application/json" \ - -d '{ - "text": "Hello world", - "profile_id": "abc123", - "language": "en" - }' -``` - -## API Endpoints - -The Voicebox API is organized into several categories: - - - - Create, list, update, and delete voice profiles - - - Generate speech from text using voice profiles - - - Record and transcribe audio - - - Create and manage multi-voice stories (coming soon) - - - -## Core Endpoints - -### Voice Profiles - -```http -GET /profiles # List all profiles -POST /profiles # Create a new profile -GET /profiles/{id} # Get profile details -PUT /profiles/{id} # Update a profile -DELETE /profiles/{id} # Delete a profile -POST /profiles/{id}/samples # Add voice sample -``` - -### Generation - -```http -POST /generate # Generate speech -GET /history # List generation history -GET /history/{id} # Get generation details -DELETE /history/{id} # Delete from history -``` - -### Recordings - -```http -POST /recordings # Start recording -POST /recordings/stop # Stop recording -POST /transcribe # Transcribe audio -``` - -## Response Format - -All API responses follow a consistent JSON format: - -```json -{ - "success": true, - "data": { - // Response data - }, - "error": null -} -``` - -Error responses: - -```json -{ - "success": false, - "data": null, - "error": { - "message": "Error description", - "code": "ERROR_CODE" - } -} -``` - -## Data Models - -### Voice Profile - -```json -{ - "id": "abc123", - "name": "John Smith", - "language": "en", - "description": "Professional narrator voice", - "created_at": "2024-01-29T12:00:00Z", - "samples": [ - { - "id": "sample123", - "audio_path": "/path/to/sample.wav", - "duration": 15.5 - } - ] -} -``` - -### Generation - -```json -{ - "id": "gen123", - "text": "Hello world", - "profile_id": "abc123", - "language": "en", - "audio_path": "/path/to/output.wav", - "duration": 2.3, - "created_at": "2024-01-29T12:00:00Z" -} -``` - -## TypeScript Client - -Voicebox provides an auto-generated TypeScript client with full type safety: - -```typescript -import { VoiceboxClient } from '@/lib/api' - -const client = new VoiceboxClient({ - baseUrl: 'http://localhost:17493' -}) - -// Create a profile -const profile = await client.createProfile({ - name: 'John Smith', - language: 'en' -}) - -// Generate speech -const generation = await client.generate({ - text: 'Hello world', - profile_id: profile.id, - language: 'en' -}) -``` - -The client is automatically generated from the OpenAPI schema. See [Development Setup](/development/setup#generate-openapi-client) for details. - -## Rate Limiting - - - Currently, there are no rate limits for local usage. Rate limiting will be added in a future release for production deployments. - - -## WebSocket Support - - - Real-time streaming generation via WebSockets is planned for a future release. - - -## Use Cases - - - - Generate dynamic dialogue for NPCs and characters - - - Automate voiceovers for videos and podcasts - - - Build text-to-speech tools for visually impaired users - - - Create custom voice interfaces - - - -## Next Steps - - - - Learn how to manage voice profiles - - - Generate speech from text - - diff --git a/docs2/content/docs/api/recordings.mdx b/docs2/content/docs/api/recordings.mdx deleted file mode 100644 index dd0552d2..00000000 --- a/docs2/content/docs/api/recordings.mdx +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: "Recordings API" -description: "Record and transcribe audio" ---- - -## Start Recording - -```http -POST /recordings/start -``` - -**Request:** -```json -{ - "source": "microphone" -} -``` - -**Response:** -```json -{ - "recording_id": "rec123", - "status": "recording" -} -``` - -## Stop Recording - -```http -POST /recordings/stop -``` - -**Request:** -```json -{ - "recording_id": "rec123" -} -``` - -**Response:** -```json -{ - "recording_id": "rec123", - "audio_url": "/audio/rec123.wav", - "duration": 15.5 -} -``` - -## Transcribe Audio - -```http -POST /transcribe -``` - -**Request:** (multipart/form-data) -``` -audio: -language: "en" (optional) -``` - -**Response:** -```json -{ - "text": "Transcribed speech text here", - "language": "en", - "duration": 15.5, - "confidence": 0.95 -} -``` - -## TypeScript Example - -```typescript -import { VoiceboxClient } from '@/lib/api' - -const client = new VoiceboxClient({ - baseUrl: 'http://localhost:17493' -}) - -// Start recording -const recording = await client.startRecording({ - source: 'microphone' -}) - -// ... record audio ... - -// Stop recording -const result = await client.stopRecording(recording.id) - -// Transcribe -const transcription = await client.transcribe(audioFile, 'en') -console.log(transcription.text) -``` - -For full API documentation, visit `http://localhost:17493/docs` when the server is running. diff --git a/docs2/content/docs/api/voice-profiles.mdx b/docs2/content/docs/api/voice-profiles.mdx deleted file mode 100644 index 3715e44c..00000000 --- a/docs2/content/docs/api/voice-profiles.mdx +++ /dev/null @@ -1,149 +0,0 @@ ---- -title: "Voice Profiles API" -description: "Manage voice profiles programmatically" ---- - -## Endpoints - -### List Profiles - -```http -GET /profiles -``` - -**Response:** -```json -{ - "profiles": [ - { - "id": "abc123", - "name": "John Smith", - "language": "en", - "description": "Professional narrator", - "created_at": "2024-01-29T12:00:00Z", - "sample_count": 2 - } - ] -} -``` - -### Get Profile - -```http -GET /profiles/{id} -``` - -**Response:** -```json -{ - "id": "abc123", - "name": "John Smith", - "language": "en", - "description": "Professional narrator", - "created_at": "2024-01-29T12:00:00Z", - "samples": [ - { - "id": "sample123", - "duration": 15.5, - "created_at": "2024-01-29T12:00:00Z" - } - ] -} -``` - -### Create Profile - -```http -POST /profiles -``` - -**Request:** -```json -{ - "name": "John Smith", - "language": "en", - "description": "Professional narrator" -} -``` - -**Response:** -```json -{ - "id": "abc123", - "name": "John Smith", - "language": "en", - "description": "Professional narrator", - "created_at": "2024-01-29T12:00:00Z" -} -``` - -### Update Profile - -```http -PUT /profiles/{id} -``` - -**Request:** -```json -{ - "name": "Updated Name", - "description": "Updated description" -} -``` - -### Delete Profile - -```http -DELETE /profiles/{id} -``` - -**Response:** -```json -{ - "success": true -} -``` - -### Add Voice Sample - -```http -POST /profiles/{id}/samples -``` - -**Request:** (multipart/form-data) -``` -audio: -``` - -**Response:** -```json -{ - "sample_id": "sample123", - "duration": 15.5 -} -``` - -## TypeScript Example - -```typescript -import { VoiceboxClient } from '@/lib/api' - -const client = new VoiceboxClient({ - baseUrl: 'http://localhost:17493' -}) - -// Create profile -const profile = await client.createProfile({ - name: 'John Smith', - language: 'en', - description: 'Professional narrator' -}) - -// Add sample -await client.addSample(profile.id, audioFile) - -// List all profiles -const profiles = await client.listProfiles() -``` - -For full API documentation, visit `http://localhost:17493/docs` when the server is running. diff --git a/docs2/content/docs/developer/contributing.mdx b/docs2/content/docs/developer/contributing.mdx index d67a2f4b..d14947d4 100644 --- a/docs2/content/docs/developer/contributing.mdx +++ b/docs2/content/docs/developer/contributing.mdx @@ -259,7 +259,11 @@ When adding new API endpoints: - Add documentation in `/docs/api/` + The API documentation is automatically generated from the OpenAPI schema. Ensure your endpoint has proper docstrings and type hints, then regenerate the docs: + + ```bash + bun run generate:api + ``` diff --git a/docs2/content/docs/developer/setup.mdx b/docs2/content/docs/developer/setup.mdx index c2c44f24..6160868a 100644 --- a/docs2/content/docs/developer/setup.mdx +++ b/docs2/content/docs/developer/setup.mdx @@ -208,7 +208,7 @@ This downloads the OpenAPI schema and generates the TypeScript client in `app/sr Learn how to build production releases - + Explore the REST API diff --git a/docs2/content/docs/meta.json b/docs2/content/docs/meta.json index 11ea0c16..043b2aaa 100644 --- a/docs2/content/docs/meta.json +++ b/docs2/content/docs/meta.json @@ -2,7 +2,6 @@ "title": "Voicebox Documentation", "pages": [ "overview", - "api", "api-reference", "developer", "plans" diff --git a/docs2/content/docs/overview/quick-start.mdx b/docs2/content/docs/overview/quick-start.mdx index 6fee0496..b4563d20 100644 --- a/docs2/content/docs/overview/quick-start.mdx +++ b/docs2/content/docs/overview/quick-start.mdx @@ -118,7 +118,7 @@ The Stories Editor lets you create multi-voice narratives with a timeline-based Learn advanced techniques for high-quality voice cloning - + Integrate Voicebox into your own applications diff --git a/docs2/openapi.json b/docs2/openapi.json index 8f31bf9e..1a196e6c 100644 --- a/docs2/openapi.json +++ b/docs2/openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"voicebox API","description":"Production-quality Qwen3-TTS voice cloning API","version":"0.1.0"},"paths":{"/":{"get":{"summary":"Root","description":"Root endpoint.","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health","description":"Health check endpoint.","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/profiles":{"get":{"summary":"List Profiles","description":"List all voice profiles.","operationId":"list_profiles_profiles_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/VoiceProfileResponse"},"type":"array","title":"Response List Profiles Profiles Get"}}}}}},"post":{"summary":"Create Profile","description":"Create a new voice profile.","operationId":"create_profile_profiles_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceProfileCreate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceProfileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/profiles/{profile_id}":{"get":{"summary":"Get Profile","description":"Get a voice profile by ID.","operationId":"get_profile_profiles__profile_id__get","parameters":[{"name":"profile_id","in":"path","required":true,"schema":{"type":"string","title":"Profile Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceProfileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"summary":"Update Profile","description":"Update a voice profile.","operationId":"update_profile_profiles__profile_id__put","parameters":[{"name":"profile_id","in":"path","required":true,"schema":{"type":"string","title":"Profile Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceProfileCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceProfileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Profile","description":"Delete a voice profile.","operationId":"delete_profile_profiles__profile_id__delete","parameters":[{"name":"profile_id","in":"path","required":true,"schema":{"type":"string","title":"Profile Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/profiles/{profile_id}/samples":{"post":{"summary":"Add Profile Sample","description":"Add a sample to a voice profile.","operationId":"add_profile_sample_profiles__profile_id__samples_post","parameters":[{"name":"profile_id","in":"path","required":true,"schema":{"type":"string","title":"Profile Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_add_profile_sample_profiles__profile_id__samples_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileSampleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Get Profile Samples","description":"Get all samples for a profile.","operationId":"get_profile_samples_profiles__profile_id__samples_get","parameters":[{"name":"profile_id","in":"path","required":true,"schema":{"type":"string","title":"Profile Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileSampleResponse"},"title":"Response Get Profile Samples Profiles Profile Id Samples Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/profiles/samples/{sample_id}":{"delete":{"summary":"Delete Profile Sample","description":"Delete a profile sample.","operationId":"delete_profile_sample_profiles_samples__sample_id__delete","parameters":[{"name":"sample_id","in":"path","required":true,"schema":{"type":"string","title":"Sample Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/generate":{"post":{"summary":"Generate Speech","description":"Generate speech from text using a voice profile.","operationId":"generate_speech_generate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/history":{"get":{"summary":"List History","description":"List generation history with optional filters.","operationId":"list_history_history_get","parameters":[{"name":"profile_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Profile Id"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HistoryListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/history/{generation_id}":{"get":{"summary":"Get Generation","description":"Get a generation by ID.","operationId":"get_generation_history__generation_id__get","parameters":[{"name":"generation_id","in":"path","required":true,"schema":{"type":"string","title":"Generation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HistoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Generation","description":"Delete a generation.","operationId":"delete_generation_history__generation_id__delete","parameters":[{"name":"generation_id","in":"path","required":true,"schema":{"type":"string","title":"Generation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/history/stats":{"get":{"summary":"Get Stats","description":"Get generation statistics.","operationId":"get_stats_history_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/transcribe":{"post":{"summary":"Transcribe Audio","description":"Transcribe audio file to text.","operationId":"transcribe_audio_transcribe_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_transcribe_audio_transcribe_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/audio/{generation_id}":{"get":{"summary":"Get Audio","description":"Serve generated audio file.","operationId":"get_audio_audio__generation_id__get","parameters":[{"name":"generation_id","in":"path","required":true,"schema":{"type":"string","title":"Generation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/models/load":{"post":{"summary":"Load Model","description":"Manually load TTS model.","operationId":"load_model_models_load_post","parameters":[{"name":"model_size","in":"query","required":false,"schema":{"type":"string","default":"1.7B","title":"Model Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/models/unload":{"post":{"summary":"Unload Model","description":"Unload TTS model to free memory.","operationId":"unload_model_models_unload_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/models/progress/{model_name}":{"get":{"summary":"Get Model Progress","description":"Get model download progress via Server-Sent Events.","operationId":"get_model_progress_models_progress__model_name__get","parameters":[{"name":"model_name","in":"path","required":true,"schema":{"type":"string","title":"Model Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/models/status":{"get":{"summary":"Get Model Status","description":"Get status of all available models.","operationId":"get_model_status_models_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelStatusListResponse"}}}}}}},"/models/download":{"post":{"summary":"Trigger Model Download","description":"Trigger download of a specific model.","operationId":"trigger_model_download_models_download_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelDownloadRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Body_add_profile_sample_profiles__profile_id__samples_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"},"reference_text":{"type":"string","title":"Reference Text"}},"type":"object","required":["file","reference_text"],"title":"Body_add_profile_sample_profiles__profile_id__samples_post"},"Body_transcribe_audio_transcribe_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"}},"type":"object","required":["file"],"title":"Body_transcribe_audio_transcribe_post"},"GenerationRequest":{"properties":{"profile_id":{"type":"string","title":"Profile Id"},"text":{"type":"string","maxLength":5000,"minLength":1,"title":"Text"},"language":{"type":"string","pattern":"^(en|zh)$","title":"Language","default":"en"},"seed":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Seed"},"model_size":{"anyOf":[{"type":"string","pattern":"^(1\\.7B|0\\.6B)$"},{"type":"null"}],"title":"Model Size","default":"1.7B"}},"type":"object","required":["profile_id","text"],"title":"GenerationRequest","description":"Request model for voice generation."},"GenerationResponse":{"properties":{"id":{"type":"string","title":"Id"},"profile_id":{"type":"string","title":"Profile Id"},"text":{"type":"string","title":"Text"},"language":{"type":"string","title":"Language"},"audio_path":{"type":"string","title":"Audio Path"},"duration":{"type":"number","title":"Duration"},"seed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Seed"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","profile_id","text","language","audio_path","duration","seed","created_at"],"title":"GenerationResponse","description":"Response model for voice generation."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthResponse":{"properties":{"status":{"type":"string","title":"Status"},"model_loaded":{"type":"boolean","title":"Model Loaded"},"model_downloaded":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Model Downloaded"},"model_size":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model Size"},"gpu_available":{"type":"boolean","title":"Gpu Available"},"vram_used_mb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Vram Used Mb"}},"type":"object","required":["status","model_loaded","gpu_available"],"title":"HealthResponse","description":"Response model for health check."},"HistoryListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/HistoryResponse"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["items","total"],"title":"HistoryListResponse","description":"Response model for history list."},"HistoryResponse":{"properties":{"id":{"type":"string","title":"Id"},"profile_id":{"type":"string","title":"Profile Id"},"profile_name":{"type":"string","title":"Profile Name"},"text":{"type":"string","title":"Text"},"language":{"type":"string","title":"Language"},"audio_path":{"type":"string","title":"Audio Path"},"duration":{"type":"number","title":"Duration"},"seed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Seed"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","profile_id","profile_name","text","language","audio_path","duration","seed","created_at"],"title":"HistoryResponse","description":"Response model for history entry (includes profile name)."},"ModelDownloadRequest":{"properties":{"model_name":{"type":"string","title":"Model Name"}},"type":"object","required":["model_name"],"title":"ModelDownloadRequest","description":"Request model for triggering model download."},"ModelStatus":{"properties":{"model_name":{"type":"string","title":"Model Name"},"display_name":{"type":"string","title":"Display Name"},"downloaded":{"type":"boolean","title":"Downloaded"},"size_mb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Size Mb"},"loaded":{"type":"boolean","title":"Loaded","default":false}},"type":"object","required":["model_name","display_name","downloaded"],"title":"ModelStatus","description":"Response model for model status."},"ModelStatusListResponse":{"properties":{"models":{"items":{"$ref":"#/components/schemas/ModelStatus"},"type":"array","title":"Models"}},"type":"object","required":["models"],"title":"ModelStatusListResponse","description":"Response model for model status list."},"ProfileSampleResponse":{"properties":{"id":{"type":"string","title":"Id"},"profile_id":{"type":"string","title":"Profile Id"},"audio_path":{"type":"string","title":"Audio Path"},"reference_text":{"type":"string","title":"Reference Text"}},"type":"object","required":["id","profile_id","audio_path","reference_text"],"title":"ProfileSampleResponse","description":"Response model for profile sample."},"TranscriptionResponse":{"properties":{"text":{"type":"string","title":"Text"},"duration":{"type":"number","title":"Duration"}},"type":"object","required":["text","duration"],"title":"TranscriptionResponse","description":"Response model for transcription."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VoiceProfileCreate":{"properties":{"name":{"type":"string","maxLength":100,"minLength":1,"title":"Name"},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Description"},"language":{"type":"string","pattern":"^(en|zh)$","title":"Language","default":"en"}},"type":"object","required":["name"],"title":"VoiceProfileCreate","description":"Request model for creating a voice profile."},"VoiceProfileResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"language":{"type":"string","title":"Language"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","name","description","language","created_at","updated_at"],"title":"VoiceProfileResponse","description":"Response model for voice profile."}}}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"voicebox API","description":"Production-quality Qwen3-TTS voice cloning API","version":"0.1.0"},"servers":[{"url":"http://localhost:8000","description":"Local development server"}],"paths":{"/":{"get":{"summary":"Root","description":"Root endpoint.","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health","description":"Health check endpoint.","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/profiles":{"get":{"summary":"List Profiles","description":"List all voice profiles.","operationId":"list_profiles_profiles_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/VoiceProfileResponse"},"type":"array","title":"Response List Profiles Profiles Get"}}}}}},"post":{"summary":"Create Profile","description":"Create a new voice profile.","operationId":"create_profile_profiles_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceProfileCreate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceProfileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/profiles/{profile_id}":{"get":{"summary":"Get Profile","description":"Get a voice profile by ID.","operationId":"get_profile_profiles__profile_id__get","parameters":[{"name":"profile_id","in":"path","required":true,"schema":{"type":"string","title":"Profile Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceProfileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"summary":"Update Profile","description":"Update a voice profile.","operationId":"update_profile_profiles__profile_id__put","parameters":[{"name":"profile_id","in":"path","required":true,"schema":{"type":"string","title":"Profile Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceProfileCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceProfileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Profile","description":"Delete a voice profile.","operationId":"delete_profile_profiles__profile_id__delete","parameters":[{"name":"profile_id","in":"path","required":true,"schema":{"type":"string","title":"Profile Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/profiles/{profile_id}/samples":{"post":{"summary":"Add Profile Sample","description":"Add a sample to a voice profile.","operationId":"add_profile_sample_profiles__profile_id__samples_post","parameters":[{"name":"profile_id","in":"path","required":true,"schema":{"type":"string","title":"Profile Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_add_profile_sample_profiles__profile_id__samples_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileSampleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Get Profile Samples","description":"Get all samples for a profile.","operationId":"get_profile_samples_profiles__profile_id__samples_get","parameters":[{"name":"profile_id","in":"path","required":true,"schema":{"type":"string","title":"Profile Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileSampleResponse"},"title":"Response Get Profile Samples Profiles Profile Id Samples Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/profiles/samples/{sample_id}":{"delete":{"summary":"Delete Profile Sample","description":"Delete a profile sample.","operationId":"delete_profile_sample_profiles_samples__sample_id__delete","parameters":[{"name":"sample_id","in":"path","required":true,"schema":{"type":"string","title":"Sample Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/generate":{"post":{"summary":"Generate Speech","description":"Generate speech from text using a voice profile.","operationId":"generate_speech_generate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/history":{"get":{"summary":"List History","description":"List generation history with optional filters.","operationId":"list_history_history_get","parameters":[{"name":"profile_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Profile Id"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HistoryListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/history/{generation_id}":{"get":{"summary":"Get Generation","description":"Get a generation by ID.","operationId":"get_generation_history__generation_id__get","parameters":[{"name":"generation_id","in":"path","required":true,"schema":{"type":"string","title":"Generation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HistoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Generation","description":"Delete a generation.","operationId":"delete_generation_history__generation_id__delete","parameters":[{"name":"generation_id","in":"path","required":true,"schema":{"type":"string","title":"Generation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/history/stats":{"get":{"summary":"Get Stats","description":"Get generation statistics.","operationId":"get_stats_history_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/transcribe":{"post":{"summary":"Transcribe Audio","description":"Transcribe audio file to text.","operationId":"transcribe_audio_transcribe_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_transcribe_audio_transcribe_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/audio/{generation_id}":{"get":{"summary":"Get Audio","description":"Serve generated audio file.","operationId":"get_audio_audio__generation_id__get","parameters":[{"name":"generation_id","in":"path","required":true,"schema":{"type":"string","title":"Generation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/models/load":{"post":{"summary":"Load Model","description":"Manually load TTS model.","operationId":"load_model_models_load_post","parameters":[{"name":"model_size","in":"query","required":false,"schema":{"type":"string","default":"1.7B","title":"Model Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/models/unload":{"post":{"summary":"Unload Model","description":"Unload TTS model to free memory.","operationId":"unload_model_models_unload_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/models/progress/{model_name}":{"get":{"summary":"Get Model Progress","description":"Get model download progress via Server-Sent Events.","operationId":"get_model_progress_models_progress__model_name__get","parameters":[{"name":"model_name","in":"path","required":true,"schema":{"type":"string","title":"Model Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/models/status":{"get":{"summary":"Get Model Status","description":"Get status of all available models.","operationId":"get_model_status_models_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelStatusListResponse"}}}}}}},"/models/download":{"post":{"summary":"Trigger Model Download","description":"Trigger download of a specific model.","operationId":"trigger_model_download_models_download_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelDownloadRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Body_add_profile_sample_profiles__profile_id__samples_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"},"reference_text":{"type":"string","title":"Reference Text"}},"type":"object","required":["file","reference_text"],"title":"Body_add_profile_sample_profiles__profile_id__samples_post"},"Body_transcribe_audio_transcribe_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"}},"type":"object","required":["file"],"title":"Body_transcribe_audio_transcribe_post"},"GenerationRequest":{"properties":{"profile_id":{"type":"string","title":"Profile Id"},"text":{"type":"string","maxLength":5000,"minLength":1,"title":"Text"},"language":{"type":"string","pattern":"^(en|zh)$","title":"Language","default":"en"},"seed":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Seed"},"model_size":{"anyOf":[{"type":"string","pattern":"^(1\\.7B|0\\.6B)$"},{"type":"null"}],"title":"Model Size","default":"1.7B"}},"type":"object","required":["profile_id","text"],"title":"GenerationRequest","description":"Request model for voice generation."},"GenerationResponse":{"properties":{"id":{"type":"string","title":"Id"},"profile_id":{"type":"string","title":"Profile Id"},"text":{"type":"string","title":"Text"},"language":{"type":"string","title":"Language"},"audio_path":{"type":"string","title":"Audio Path"},"duration":{"type":"number","title":"Duration"},"seed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Seed"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","profile_id","text","language","audio_path","duration","seed","created_at"],"title":"GenerationResponse","description":"Response model for voice generation."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthResponse":{"properties":{"status":{"type":"string","title":"Status"},"model_loaded":{"type":"boolean","title":"Model Loaded"},"model_downloaded":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Model Downloaded"},"model_size":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model Size"},"gpu_available":{"type":"boolean","title":"Gpu Available"},"vram_used_mb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Vram Used Mb"}},"type":"object","required":["status","model_loaded","gpu_available"],"title":"HealthResponse","description":"Response model for health check."},"HistoryListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/HistoryResponse"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["items","total"],"title":"HistoryListResponse","description":"Response model for history list."},"HistoryResponse":{"properties":{"id":{"type":"string","title":"Id"},"profile_id":{"type":"string","title":"Profile Id"},"profile_name":{"type":"string","title":"Profile Name"},"text":{"type":"string","title":"Text"},"language":{"type":"string","title":"Language"},"audio_path":{"type":"string","title":"Audio Path"},"duration":{"type":"number","title":"Duration"},"seed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Seed"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","profile_id","profile_name","text","language","audio_path","duration","seed","created_at"],"title":"HistoryResponse","description":"Response model for history entry (includes profile name)."},"ModelDownloadRequest":{"properties":{"model_name":{"type":"string","title":"Model Name"}},"type":"object","required":["model_name"],"title":"ModelDownloadRequest","description":"Request model for triggering model download."},"ModelStatus":{"properties":{"model_name":{"type":"string","title":"Model Name"},"display_name":{"type":"string","title":"Display Name"},"downloaded":{"type":"boolean","title":"Downloaded"},"size_mb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Size Mb"},"loaded":{"type":"boolean","title":"Loaded","default":false}},"type":"object","required":["model_name","display_name","downloaded"],"title":"ModelStatus","description":"Response model for model status."},"ModelStatusListResponse":{"properties":{"models":{"items":{"$ref":"#/components/schemas/ModelStatus"},"type":"array","title":"Models"}},"type":"object","required":["models"],"title":"ModelStatusListResponse","description":"Response model for model status list."},"ProfileSampleResponse":{"properties":{"id":{"type":"string","title":"Id"},"profile_id":{"type":"string","title":"Profile Id"},"audio_path":{"type":"string","title":"Audio Path"},"reference_text":{"type":"string","title":"Reference Text"}},"type":"object","required":["id","profile_id","audio_path","reference_text"],"title":"ProfileSampleResponse","description":"Response model for profile sample."},"TranscriptionResponse":{"properties":{"text":{"type":"string","title":"Text"},"duration":{"type":"number","title":"Duration"}},"type":"object","required":["text","duration"],"title":"TranscriptionResponse","description":"Response model for transcription."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VoiceProfileCreate":{"properties":{"name":{"type":"string","maxLength":100,"minLength":1,"title":"Name"},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Description"},"language":{"type":"string","pattern":"^(en|zh)$","title":"Language","default":"en"}},"type":"object","required":["name"],"title":"VoiceProfileCreate","description":"Request model for creating a voice profile."},"VoiceProfileResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"language":{"type":"string","title":"Language"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","name","description","language","created_at","updated_at"],"title":"VoiceProfileResponse","description":"Response model for voice profile."}}}} \ No newline at end of file