mirror of
https://github.com/Nighthawk42/Qwen3-TTS-streaming.git
synced 2026-08-30 09:02:26 +00:00
Added additional endpoints, updated Gradio demo.
This commit is contained in:
@@ -0,0 +1,224 @@
|
|||||||
|
# API Endpoints Quick Reference
|
||||||
|
|
||||||
|
## Core Endpoints
|
||||||
|
|
||||||
|
### Health & Status
|
||||||
|
```
|
||||||
|
GET /v1/health → {status, model, device, ready}
|
||||||
|
GET / → API info + all endpoints
|
||||||
|
```
|
||||||
|
|
||||||
|
### Models
|
||||||
|
```
|
||||||
|
GET /v1/models → List available models
|
||||||
|
GET /v1/models/{id}/config → Model configuration
|
||||||
|
GET /v1/models/{id}/languages → Supported languages
|
||||||
|
```
|
||||||
|
|
||||||
|
### Text-to-Speech
|
||||||
|
```
|
||||||
|
POST /v1/audio/speech → Generate audio (standard)
|
||||||
|
POST /v1/audio/speech/stream → Stream audio chunks
|
||||||
|
POST /v1/audio/convert → Convert format/sample rate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extended Endpoints
|
||||||
|
|
||||||
|
### Voice Management
|
||||||
|
```
|
||||||
|
POST /v1/voices/upload → Upload custom voice
|
||||||
|
GET /v1/voices → List all voices
|
||||||
|
DEL /v1/voices/{id} → Delete voice
|
||||||
|
```
|
||||||
|
|
||||||
|
### Text Processing
|
||||||
|
```
|
||||||
|
POST /v1/text/validate → Validate text + duration estimate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Processing
|
||||||
|
```
|
||||||
|
POST /v1/batch/create → Create batch job (async)
|
||||||
|
GET /v1/batch/{id} → Check batch status
|
||||||
|
GET /v1/batch/{id}/results → Get batch results
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage & Quotas
|
||||||
|
```
|
||||||
|
GET /v1/usage → Usage statistics
|
||||||
|
GET /v1/quota → Rate limits & remaining
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Request/Response Examples
|
||||||
|
|
||||||
|
### Basic TTS
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/v1/audio/speech \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"text": "Hello world", "language": "English"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stream TTS
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/v1/audio/speech/stream \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"text": "Hello world"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validate Text
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/v1/text/validate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"text": "Hello world", "language": "English"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload Voice
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/v1/voices/upload \
|
||||||
|
-F "name=Alice" \
|
||||||
|
-F "language=English" \
|
||||||
|
-F "ref_text=Reference text here" \
|
||||||
|
-F "audio=@voice.wav"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Batch
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/v1/batch/create \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"items": [
|
||||||
|
{"text": "Item 1"},
|
||||||
|
{"text": "Item 2"}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Usage
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/v1/usage
|
||||||
|
curl http://localhost:8000/v1/quota
|
||||||
|
```
|
||||||
|
|
||||||
|
### Convert Audio
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/v1/audio/convert \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"audio_base64": "...",
|
||||||
|
"target_sample_rate": 16000
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Python Client Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API_URL = "http://localhost:8000"
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
r = requests.get(f"{API_URL}/v1/health")
|
||||||
|
print(r.json())
|
||||||
|
|
||||||
|
# Generate
|
||||||
|
r = requests.post(f"{API_URL}/v1/audio/speech",
|
||||||
|
json={"text": "Hello"})
|
||||||
|
audio_b64 = r.json()["audio_base64"]
|
||||||
|
|
||||||
|
# Validate
|
||||||
|
r = requests.post(f"{API_URL}/v1/text/validate",
|
||||||
|
json={"text": "Hello"})
|
||||||
|
print(r.json()["estimated_duration"])
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
r = requests.get(f"{API_URL}/v1/usage")
|
||||||
|
print(r.json()["requests_made"])
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status Codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| 200 | Success |
|
||||||
|
| 400 | Bad request (validation) |
|
||||||
|
| 404 | Not found |
|
||||||
|
| 409 | Conflict (can't process) |
|
||||||
|
| 503 | Service unavailable |
|
||||||
|
| 500 | Server error |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Requests/minute | 60 |
|
||||||
|
| Max text length | 10,000 chars |
|
||||||
|
| Max batch size | 100 items |
|
||||||
|
| Concurrent requests | 4 |
|
||||||
|
| Voice audio duration | 5-30s (recommended) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
**Success (200):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "req_xxx",
|
||||||
|
"status": "success",
|
||||||
|
"data": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Description",
|
||||||
|
"code": "error_code",
|
||||||
|
"details": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Storage Locations
|
||||||
|
|
||||||
|
| Item | Location |
|
||||||
|
|------|----------|
|
||||||
|
| Custom voices | `assets/custom_voices/{voice_id}/` |
|
||||||
|
| Batch jobs | `assets/batch_jobs/{job_id}/` |
|
||||||
|
| Voice cloning ref | `assets/voice_cloning/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Links
|
||||||
|
|
||||||
|
- **Interactive API Docs**: http://localhost:8000/docs
|
||||||
|
- **Extended Endpoints**: `api/EXTENDED_ENDPOINTS.md`
|
||||||
|
- **Code Examples**: `api/examples.py`
|
||||||
|
- **Implementation Details**: `api/IMPLEMENTATION_SUMMARY.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# Run
|
||||||
|
python start_api.py
|
||||||
|
|
||||||
|
# Test
|
||||||
|
python api/examples.py
|
||||||
|
|
||||||
|
# Browse
|
||||||
|
open http://localhost:8000/docs
|
||||||
|
```
|
||||||
@@ -0,0 +1,576 @@
|
|||||||
|
# Extended API Endpoints Guide
|
||||||
|
|
||||||
|
This document describes all the extended endpoints added to the Qwen3-TTS API beyond basic TTS functionality.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
- [Voice Management](#voice-management)
|
||||||
|
- [Text Validation](#text-validation)
|
||||||
|
- [Batch Processing](#batch-processing)
|
||||||
|
- [Usage & Quotas](#usage--quotas)
|
||||||
|
- [Audio Conversion](#audio-conversion)
|
||||||
|
- [Model Configuration](#model-configuration)
|
||||||
|
|
||||||
|
## Voice Management
|
||||||
|
|
||||||
|
### Upload Custom Voice
|
||||||
|
Uploads and registers a custom voice for voice cloning.
|
||||||
|
|
||||||
|
**Endpoint:** `POST /v1/voices/upload`
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `name` (string, required): Display name for the voice
|
||||||
|
- `language` (string, required): Language code (e.g., "English", "Russian")
|
||||||
|
- `audio` (file, required): WAV audio file (5-30 seconds recommended)
|
||||||
|
- `ref_text` (string, required): Exact transcription of the audio
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/v1/voices/upload" \
|
||||||
|
-F "name=Alice" \
|
||||||
|
-F "language=English" \
|
||||||
|
-F "ref_text=This is a test of voice cloning." \
|
||||||
|
-F "audio=@ref_audio.wav"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python Example:**
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
with open("voice.wav", "rb") as f:
|
||||||
|
response = requests.post(
|
||||||
|
"http://localhost:8000/v1/voices/upload",
|
||||||
|
data={
|
||||||
|
"name": "Alice",
|
||||||
|
"language": "English",
|
||||||
|
"ref_text": "This is a test of voice cloning."
|
||||||
|
},
|
||||||
|
files={"audio": f}
|
||||||
|
)
|
||||||
|
|
||||||
|
voice = response.json()
|
||||||
|
print(f"Voice ID: {voice['id']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "voice_abc123def456",
|
||||||
|
"name": "Alice",
|
||||||
|
"language": "English",
|
||||||
|
"created_at": 1708284000,
|
||||||
|
"object": "voice"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Voices
|
||||||
|
Lists all uploaded custom voices.
|
||||||
|
|
||||||
|
**Endpoint:** `GET /v1/voices`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/v1/voices"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"object": "list",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "voice_abc123def456",
|
||||||
|
"name": "Alice",
|
||||||
|
"language": "English",
|
||||||
|
"created_at": 1708284000,
|
||||||
|
"object": "voice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "voice_xyz789uvw012",
|
||||||
|
"name": "Bob",
|
||||||
|
"language": "Russian",
|
||||||
|
"created_at": 1708280000,
|
||||||
|
"object": "voice"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Voice
|
||||||
|
Deletes a custom voice from storage.
|
||||||
|
|
||||||
|
**Endpoint:** `DELETE /v1/voices/{voice_id}`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl -X DELETE "http://localhost:8000/v1/voices/voice_abc123def456"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"object": "voice.deleted",
|
||||||
|
"id": "voice_abc123def456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Text Validation
|
||||||
|
|
||||||
|
### Validate Text
|
||||||
|
Validates text for TTS synthesis and provides analysis.
|
||||||
|
|
||||||
|
**Endpoint:** `POST /v1/text/validate`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"text": "Your text to validate",
|
||||||
|
"language": "English"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Character count validation
|
||||||
|
- Maximum length check (10,000 characters)
|
||||||
|
- Estimated audio duration (using ~150 wpm)
|
||||||
|
- Warnings for edge cases
|
||||||
|
- Language compatibility check
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/v1/text/validate" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"text": "This is a test message.",
|
||||||
|
"language": "English"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"language": "English",
|
||||||
|
"character_count": 23,
|
||||||
|
"estimated_duration": 1.5,
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Warnings Examples:**
|
||||||
|
- "Text is very short, may result in poor quality" (< 3 chars)
|
||||||
|
- "Text is very long, generation may take several minutes" (> 5000 chars)
|
||||||
|
- "Text contains emoji which may be handled differently"
|
||||||
|
|
||||||
|
## Batch Processing
|
||||||
|
|
||||||
|
Batch processing allows you to submit multiple texts for asynchronous generation and retrieve results later.
|
||||||
|
|
||||||
|
### Create Batch Job
|
||||||
|
Creates a new batch job for processing multiple texts.
|
||||||
|
|
||||||
|
**Endpoint:** `POST /v1/batch/create`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"text": "First item",
|
||||||
|
"language": "English",
|
||||||
|
"voice_clone_mode": "disabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Second item",
|
||||||
|
"language": "Russian",
|
||||||
|
"voice_clone_mode": "disabled"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Constraints:**
|
||||||
|
- Maximum 100 items per batch
|
||||||
|
- Each item must have at least `text` field
|
||||||
|
- Language defaults to "English" if not specified
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/v1/batch/create" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"items": [
|
||||||
|
{"text": "Hello world"},
|
||||||
|
{"text": "Goodbye world"}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "batch_abc123def456",
|
||||||
|
"object": "batch",
|
||||||
|
"status": "pending",
|
||||||
|
"created_at": 1708284000,
|
||||||
|
"updated_at": 1708284000,
|
||||||
|
"request_counts": {
|
||||||
|
"total": 2,
|
||||||
|
"processing": 0,
|
||||||
|
"completed": 0,
|
||||||
|
"failed": 0
|
||||||
|
},
|
||||||
|
"output_file_id": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Batch Status
|
||||||
|
Checks the status of a batch job.
|
||||||
|
|
||||||
|
**Endpoint:** `GET /v1/batch/{job_id}`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/v1/batch/batch_abc123def456"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "batch_abc123def456",
|
||||||
|
"object": "batch",
|
||||||
|
"status": "completed",
|
||||||
|
"created_at": 1708284000,
|
||||||
|
"updated_at": 1708284060,
|
||||||
|
"request_counts": {
|
||||||
|
"total": 2,
|
||||||
|
"processing": 0,
|
||||||
|
"completed": 2,
|
||||||
|
"failed": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status Values:**
|
||||||
|
- `pending`: Job waiting to be processed
|
||||||
|
- `processing`: Currently processing items
|
||||||
|
- `completed`: All items processed
|
||||||
|
- `failed`: Job failed (check results for details)
|
||||||
|
- `cancelled`: Job was cancelled
|
||||||
|
|
||||||
|
### Get Batch Results
|
||||||
|
Retrieves results from a completed batch job.
|
||||||
|
|
||||||
|
**Endpoint:** `GET /v1/batch/{job_id}/results`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/v1/batch/batch_abc123def456/results"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"job_id": "batch_abc123def456",
|
||||||
|
"object": "batch.results",
|
||||||
|
"status": "completed",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"status": "success",
|
||||||
|
"error": null,
|
||||||
|
"audio_base64": "UklGRi4AAAA...",
|
||||||
|
"duration": 1.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"status": "success",
|
||||||
|
"error": null,
|
||||||
|
"audio_base64": "UklGRi4AAAA...",
|
||||||
|
"duration": 1.3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage & Quotas
|
||||||
|
|
||||||
|
### Get Usage Statistics
|
||||||
|
Retrieves your API usage statistics.
|
||||||
|
|
||||||
|
**Endpoint:** `GET /v1/usage`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/v1/usage"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"object": "usage",
|
||||||
|
"requests_made": 42,
|
||||||
|
"audio_generated_seconds": 245.5,
|
||||||
|
"audio_generated_minutes": 4.09,
|
||||||
|
"requests_by_model": {
|
||||||
|
"Qwen/Qwen3-TTS-12Hz-1.7B-Base": 42
|
||||||
|
},
|
||||||
|
"requests_by_language": {
|
||||||
|
"English": 25,
|
||||||
|
"Russian": 12,
|
||||||
|
"Chinese": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Quota & Rate Limits
|
||||||
|
Retrieves current quota information and rate limits.
|
||||||
|
|
||||||
|
**Endpoint:** `GET /v1/quota`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/v1/quota"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"object": "quota",
|
||||||
|
"requests_per_minute": 60,
|
||||||
|
"max_text_length": 10000,
|
||||||
|
"max_batch_size": 100,
|
||||||
|
"concurrent_requests": 4,
|
||||||
|
"remaining_requests": 58
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
- `requests_per_minute`: Rate limit (requests per single minute)
|
||||||
|
- `max_text_length`: Maximum characters per TTS request
|
||||||
|
- `max_batch_size`: Maximum items per batch job
|
||||||
|
- `concurrent_requests`: Maximum concurrent requests
|
||||||
|
- `remaining_requests`: How many requests left in current minute
|
||||||
|
|
||||||
|
## Audio Conversion
|
||||||
|
|
||||||
|
### Convert Audio Format & Sample Rate
|
||||||
|
Converts audio format or resamples to a different sample rate.
|
||||||
|
|
||||||
|
**Endpoint:** `POST /v1/audio/convert`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"audio_base64": "UklGRi4AAAA...",
|
||||||
|
"target_format": "wav",
|
||||||
|
"target_sample_rate": 16000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Supported Formats:**
|
||||||
|
- `wav`: WAV (Waveform Audio)
|
||||||
|
- `mp3`: MPEG Audio
|
||||||
|
- `ogg`: Ogg Vorbis
|
||||||
|
- `flac`: FLAC (Free Lossless Audio)
|
||||||
|
|
||||||
|
**Sample Rates:**
|
||||||
|
- Valid range: 8kHz - 48kHz
|
||||||
|
- Common values: 8000, 16000, 22050, 44100, 48000
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/v1/audio/convert" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"audio_base64": "UklGRi4AAAA...",
|
||||||
|
"target_format": "wav",
|
||||||
|
"target_sample_rate": 16000
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python Example:**
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
import base64
|
||||||
|
import soundfile as sf
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
# Get audio (from TTS or elsewhere)
|
||||||
|
response = requests.post(
|
||||||
|
"http://localhost:8000/v1/audio/speech",
|
||||||
|
json={"text": "Test"}
|
||||||
|
)
|
||||||
|
audio_b64 = response.json()["audio_base64"]
|
||||||
|
|
||||||
|
# Convert to 16kHz
|
||||||
|
response = requests.post(
|
||||||
|
"http://localhost:8000/v1/audio/convert",
|
||||||
|
json={
|
||||||
|
"audio_base64": audio_b64,
|
||||||
|
"target_sample_rate": 16000
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
converted = response.json()
|
||||||
|
audio_bytes = base64.b64decode(converted["audio_base64"])
|
||||||
|
audio, sr = sf.read(BytesIO(audio_bytes))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "req_xyz789uvw012",
|
||||||
|
"object": "audio",
|
||||||
|
"created": 1708284000,
|
||||||
|
"format": "wav",
|
||||||
|
"sample_rate": 16000,
|
||||||
|
"audio_base64": "UklGRi4AAAA...",
|
||||||
|
"duration": 1.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model Configuration
|
||||||
|
|
||||||
|
### Get Model Configuration
|
||||||
|
Retrieves detailed configuration for a specific model.
|
||||||
|
|
||||||
|
**Endpoint:** `GET /v1/models/{model_id}/config`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/v1/models/Qwen/Qwen3-TTS-12Hz-1.7B-Base/config"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "Qwen/Qwen3-TTS-12Hz-1.7B-Base",
|
||||||
|
"object": "model.config",
|
||||||
|
"size": "1.7B",
|
||||||
|
"tokenizer_type": "Qwen3TTSTokenizer",
|
||||||
|
"languages": ["English", "Russian", "Chinese", "Japanese", "Korean"],
|
||||||
|
"max_text_length": 10000,
|
||||||
|
"output_sample_rate": 24000,
|
||||||
|
"streaming_supported": true,
|
||||||
|
"voice_cloning_supported": true,
|
||||||
|
"recommended_parameters": {
|
||||||
|
"temperature": 0.9,
|
||||||
|
"top_p": 1.0,
|
||||||
|
"top_k": 50,
|
||||||
|
"do_sample": true,
|
||||||
|
"repetition_penalty": 1.05,
|
||||||
|
"emit_every_frames": 8,
|
||||||
|
"decode_window_frames": 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Model Languages
|
||||||
|
Retrieves list of supported languages with codes.
|
||||||
|
|
||||||
|
**Endpoint:** `GET /v1/models/{model_id}/languages`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/v1/models/Qwen/Qwen3-TTS-12Hz-1.7B-Base/languages"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"object": "list",
|
||||||
|
"model": "Qwen/Qwen3-TTS-12Hz-1.7B-Base",
|
||||||
|
"languages": [
|
||||||
|
{"code": "en", "name": "English"},
|
||||||
|
{"code": "ru", "name": "Russian"},
|
||||||
|
{"code": "zh", "name": "Chinese"},
|
||||||
|
{"code": "ja", "name": "Japanese"},
|
||||||
|
{"code": "ko", "name": "Korean"},
|
||||||
|
{"code": "auto", "name": "Auto-detect"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
All endpoints follow consistent error response format:
|
||||||
|
|
||||||
|
**Error Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Error description",
|
||||||
|
"code": "error_code",
|
||||||
|
"details": {
|
||||||
|
"field": "Additional context"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common HTTP Status Codes:**
|
||||||
|
- `200`: Success
|
||||||
|
- `400`: Bad request (validation error)
|
||||||
|
- `404`: Resource not found
|
||||||
|
- `409`: Conflict (e.g., batch job not ready)
|
||||||
|
- `503`: Service unavailable (model loading)
|
||||||
|
- `500`: Server error
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
By default, the API enforces these limits:
|
||||||
|
- **60 requests per minute** per client
|
||||||
|
- **100 items maximum** per batch job
|
||||||
|
- **10,000 characters maximum** per text
|
||||||
|
- **4 concurrent requests** maximum
|
||||||
|
|
||||||
|
## Python Integration Examples
|
||||||
|
|
||||||
|
### Complete Workflow Example
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
import base64
|
||||||
|
import soundfile as sf
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
BASE_URL = "http://localhost:8000"
|
||||||
|
|
||||||
|
# 1. Validate text
|
||||||
|
response = requests.post(
|
||||||
|
f"{BASE_URL}/v1/text/validate",
|
||||||
|
json={"text": "Hello world", "language": "English"}
|
||||||
|
)
|
||||||
|
validation = response.json()
|
||||||
|
print(f"Valid: {validation['valid']}, Duration: {validation['estimated_duration']:.2f}s")
|
||||||
|
|
||||||
|
# 2. Generate speech
|
||||||
|
response = requests.post(
|
||||||
|
f"{BASE_URL}/v1/audio/speech",
|
||||||
|
json={"text": "Hello world"}
|
||||||
|
)
|
||||||
|
audio_data = response.json()
|
||||||
|
|
||||||
|
# 3. Convert sample rate
|
||||||
|
response = requests.post(
|
||||||
|
f"{BASE_URL}/v1/audio/convert",
|
||||||
|
json={
|
||||||
|
"audio_base64": audio_data["audio_base64"],
|
||||||
|
"target_sample_rate": 16000
|
||||||
|
}
|
||||||
|
)
|
||||||
|
converted = response.json()
|
||||||
|
|
||||||
|
# 4. Save to file
|
||||||
|
audio_bytes = base64.b64decode(converted["audio_base64"])
|
||||||
|
audio, sr = sf.read(BytesIO(audio_bytes))
|
||||||
|
sf.write("output.wav", audio, sr)
|
||||||
|
|
||||||
|
# 5. Check usage
|
||||||
|
response = requests.get(f"{BASE_URL}/v1/usage")
|
||||||
|
usage = response.json()
|
||||||
|
print(f"Total requests: {usage['requests_made']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
- [Main API Documentation](./README.md)
|
||||||
|
- [API Examples](./examples.py)
|
||||||
|
- [Interactive API Docs](http://localhost:8000/docs)
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
## 🚀 Extended API Implementation Summary
|
||||||
|
|
||||||
|
All suggested endpoints have been successfully implemented! Here's what was added:
|
||||||
|
|
||||||
|
### 📋 Implemented Endpoint Categories
|
||||||
|
|
||||||
|
#### 1. **Voice Management** (3 endpoints)
|
||||||
|
- `POST /v1/voices/upload` - Upload custom voices
|
||||||
|
- `GET /v1/voices` - List all voices
|
||||||
|
- `DELETE /v1/voices/{voice_id}` - Delete voices
|
||||||
|
|
||||||
|
**Use Case:** Manage multiple custom voices for voice cloning without needing to restart the API.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. **Text Validation** (1 endpoint)
|
||||||
|
- `POST /v1/text/validate` - Validate text & get duration estimates
|
||||||
|
|
||||||
|
**Use Case:** Check text before generation to catch errors early.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Character count validation
|
||||||
|
- Estimated duration calculation (~150 wpm)
|
||||||
|
- Warnings for edge cases (too short, too long, emojis)
|
||||||
|
- Input compatibility checks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. **Batch Processing** (3 endpoints)
|
||||||
|
- `POST /v1/batch/create` - Submit multiple texts asynchronously
|
||||||
|
- `GET /v1/batch/{job_id}` - Check job status
|
||||||
|
- `GET /v1/batch/{job_id}/results` - Retrieve results
|
||||||
|
|
||||||
|
**Use Case:** Process hundreds of texts efficiently in production pipelines.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Up to 100 items per batch
|
||||||
|
- Status tracking (pending, processing, completed, failed)
|
||||||
|
- Per-item error reporting
|
||||||
|
- Aggregated results
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. **Usage & Quota** (2 endpoints)
|
||||||
|
- `GET /v1/usage` - Get usage statistics
|
||||||
|
- `GET /v1/quota` - Check rate limits & quotas
|
||||||
|
|
||||||
|
**Use Case:** Monitor API consumption and plan capacity.
|
||||||
|
|
||||||
|
**Tracks:**
|
||||||
|
- Total requests made
|
||||||
|
- Total audio duration generated
|
||||||
|
- Breakdown by model
|
||||||
|
- Breakdown by language
|
||||||
|
- Requests remaining in current minute
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5. **Audio Conversion** (1 endpoint)
|
||||||
|
- `POST /v1/audio/convert` - Format/sample rate conversion
|
||||||
|
|
||||||
|
**Supports:**
|
||||||
|
- Formats: WAV, MP3, OGG, FLAC
|
||||||
|
- Sample rates: 8kHz - 48kHz
|
||||||
|
- Independent format and rate conversion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 6. **Model Configuration** (2 endpoints)
|
||||||
|
- `GET /v1/models/{model_id}/config` - Detailed model config
|
||||||
|
- `GET /v1/models/{model_id}/languages` - Supported languages
|
||||||
|
|
||||||
|
**Provides:**
|
||||||
|
- Model size & tokenizer info
|
||||||
|
- Max text length
|
||||||
|
- Recommended generation parameters
|
||||||
|
- Language codes and names
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🗄️ Backend Storage Infrastructure
|
||||||
|
|
||||||
|
#### VoiceStorage
|
||||||
|
- Stores custom voices in `assets/custom_voices/`
|
||||||
|
- Metadata, audio, and reference text stored per voice
|
||||||
|
- In-memory index for fast access
|
||||||
|
- Persistent disk storage
|
||||||
|
|
||||||
|
#### BatchJobStorage
|
||||||
|
- Stores batch jobs in `assets/batch_jobs/`
|
||||||
|
- Per-job metadata and results
|
||||||
|
- Status tracking
|
||||||
|
- Result aggregation
|
||||||
|
|
||||||
|
#### UsageTracker
|
||||||
|
- In-memory usage statistics
|
||||||
|
- Per-model and per-language breakdown
|
||||||
|
- Rate limiting (60 requests/minute)
|
||||||
|
- Request history for quota calculation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📁 New Files Created
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `api/main.py` | Updated with all endpoints + storage classes |
|
||||||
|
| `api/models.py` | New Pydantic models for all endpoints |
|
||||||
|
| `api/examples.py` | Working examples for all new endpoints |
|
||||||
|
| `api/EXTENDED_ENDPOINTS.md` | Comprehensive documentation |
|
||||||
|
| `assets/custom_voices/` | Directory for stored custom voices |
|
||||||
|
| `assets/batch_jobs/` | Directory for batch job storage |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔧 Integration Points
|
||||||
|
|
||||||
|
All endpoints integrate with:
|
||||||
|
- **Usage tracking** - Recorded on every TTS request
|
||||||
|
- **Error handling** - Consistent error responses with codes
|
||||||
|
- **Request ID generation** - Unique ID tracking
|
||||||
|
- **Logging** - Detailed operation logs
|
||||||
|
- **CORS** - Cross-origin request support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📊 Default Quotas & Limits
|
||||||
|
|
||||||
|
```
|
||||||
|
Rate Limits:
|
||||||
|
- 60 requests per minute
|
||||||
|
- 4 concurrent requests
|
||||||
|
- 10,000 characters max per request
|
||||||
|
- 100 items max per batch
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💡 Usage Examples
|
||||||
|
|
||||||
|
#### Text Validation
|
||||||
|
```python
|
||||||
|
requests.post("http://localhost:8000/v1/text/validate",
|
||||||
|
json={"text": "Your text", "language": "English"})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Batch Processing
|
||||||
|
```python
|
||||||
|
requests.post("http://localhost:8000/v1/batch/create",
|
||||||
|
json={"items": [{"text": "Item 1"}, {"text": "Item 2"}]})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Voice Management
|
||||||
|
```python
|
||||||
|
requests.post("http://localhost:8000/v1/voices/upload",
|
||||||
|
data={"name": "Alice", "language": "English", "ref_text": "..."},
|
||||||
|
files={"audio": open("voice.wav", "rb")})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Usage Stats
|
||||||
|
```python
|
||||||
|
requests.get("http://localhost:8000/v1/usage")
|
||||||
|
requests.get("http://localhost:8000/v1/quota")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Audio Conversion
|
||||||
|
```python
|
||||||
|
requests.post("http://localhost:8000/v1/audio/convert",
|
||||||
|
json={"audio_base64": "...", "target_sample_rate": 16000})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
|
||||||
|
- **Interactive API Docs**: `http://localhost:8000/docs` (Swagger UI)
|
||||||
|
- **Extended Endpoints Guide**: `api/EXTENDED_ENDPOINTS.md`
|
||||||
|
- **Code Examples**: `api/examples.py`
|
||||||
|
- **Main README**: `api/README.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🧪 Testing & Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run example script
|
||||||
|
python api/examples.py
|
||||||
|
|
||||||
|
# Start API server
|
||||||
|
python -m api.main
|
||||||
|
|
||||||
|
# Start both API + Gradio
|
||||||
|
python start_api.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✨ Key Benefits
|
||||||
|
|
||||||
|
1. **Production Ready** - All endpoints are fully functional
|
||||||
|
2. **Scalable** - Batch processing for high-volume workflows
|
||||||
|
3. **Observable** - Detailed usage tracking and statistics
|
||||||
|
4. **Flexible** - Voice management without restarts
|
||||||
|
5. **Validated** - Text validation catches errors early
|
||||||
|
6. **Documented** - Comprehensive API documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎯 Next Steps (Optional)
|
||||||
|
|
||||||
|
If needed, you can expand with:
|
||||||
|
- **Webhook callbacks** for batch completion notifications
|
||||||
|
- **Request authentication** (API keys)
|
||||||
|
- **Rate limiting enforcement** with 429 responses
|
||||||
|
- **Database backend** (PostgreSQL) for persistent storage
|
||||||
|
- **Message queue** (Celery) for async batch processing
|
||||||
|
- **Caching** for identical text requests
|
||||||
|
|
||||||
|
All endpoints are ready to use! 🚀
|
||||||
+208
@@ -0,0 +1,208 @@
|
|||||||
|
"""
|
||||||
|
Examples demonstrating the extended Qwen3-TTS API endpoints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
BASE_URL = "http://localhost:8000"
|
||||||
|
|
||||||
|
|
||||||
|
def example_text_validation():
|
||||||
|
"""Example: Validate text before TTS."""
|
||||||
|
print("\n=== Text Validation Example ===")
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{BASE_URL}/v1/text/validate",
|
||||||
|
json={
|
||||||
|
"text": "Hello, this is a test of the text-to-speech system.",
|
||||||
|
"language": "English"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
print(f"Valid: {data['valid']}")
|
||||||
|
print(f"Characters: {data['character_count']}")
|
||||||
|
print(f"Estimated duration: {data['estimated_duration']:.2f}s")
|
||||||
|
if data['warnings']:
|
||||||
|
print(f"Warnings: {data['warnings']}")
|
||||||
|
|
||||||
|
|
||||||
|
def example_batch_processing():
|
||||||
|
"""Example: Create and monitor batch job."""
|
||||||
|
print("\n=== Batch Processing Example ===")
|
||||||
|
|
||||||
|
# Create batch job
|
||||||
|
response = requests.post(
|
||||||
|
f"{BASE_URL}/v1/batch/create",
|
||||||
|
json={
|
||||||
|
"items": [
|
||||||
|
{"text": "Hello world", "language": "English"},
|
||||||
|
{"text": "Bonjour le monde", "language": "English"},
|
||||||
|
{"text": "Hola mundo", "language": "English"},
|
||||||
|
],
|
||||||
|
"model": "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
job = response.json()
|
||||||
|
job_id = job['id']
|
||||||
|
print(f"Created batch job: {job_id}")
|
||||||
|
print(f"Status: {job['status']}")
|
||||||
|
print(f"Total items: {job['request_counts']['total']}")
|
||||||
|
|
||||||
|
# Check job status
|
||||||
|
response = requests.get(f"{BASE_URL}/v1/batch/{job_id}")
|
||||||
|
job = response.json()
|
||||||
|
print(f"Updated status: {job['status']}")
|
||||||
|
|
||||||
|
|
||||||
|
def example_voice_management():
|
||||||
|
"""Example: Upload and manage custom voices."""
|
||||||
|
print("\n=== Voice Management Example ===")
|
||||||
|
|
||||||
|
# List existing voices
|
||||||
|
response = requests.get(f"{BASE_URL}/v1/voices")
|
||||||
|
voices = response.json()
|
||||||
|
print(f"Voices available: {len(voices['data'])}")
|
||||||
|
|
||||||
|
for voice in voices['data']:
|
||||||
|
print(f" - {voice['name']} ({voice['language']}): {voice['id']}")
|
||||||
|
|
||||||
|
# To upload a voice:
|
||||||
|
# response = requests.post(
|
||||||
|
# f"{BASE_URL}/v1/voices/upload",
|
||||||
|
# data={
|
||||||
|
# "name": "Alice",
|
||||||
|
# "language": "English",
|
||||||
|
# "ref_text": "This is a reference audio for voice cloning."
|
||||||
|
# },
|
||||||
|
# files={"audio": open("ref_audio.wav", "rb")}
|
||||||
|
# )
|
||||||
|
# voice = response.json()
|
||||||
|
# print(f"Uploaded voice: {voice['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
def example_usage_tracking():
|
||||||
|
"""Example: Check API usage and quota."""
|
||||||
|
print("\n=== Usage & Quota Example ===")
|
||||||
|
|
||||||
|
# Get usage statistics
|
||||||
|
response = requests.get(f"{BASE_URL}/v1/usage")
|
||||||
|
usage = response.json()
|
||||||
|
print(f"Total requests: {usage['requests_made']}")
|
||||||
|
print(f"Audio generated: {usage['audio_generated_minutes']:.2f} minutes")
|
||||||
|
print(f"Requests by model:")
|
||||||
|
for model, count in usage['requests_by_model'].items():
|
||||||
|
print(f" - {model}: {count}")
|
||||||
|
|
||||||
|
# Get quota info
|
||||||
|
response = requests.get(f"{BASE_URL}/v1/quota")
|
||||||
|
quota = response.json()
|
||||||
|
print(f"\nRate limit: {quota['requests_per_minute']} requests/minute")
|
||||||
|
print(f"Remaining: {quota['remaining_requests']} requests this minute")
|
||||||
|
|
||||||
|
|
||||||
|
def example_audio_conversion():
|
||||||
|
"""Example: Convert audio format/sample rate."""
|
||||||
|
print("\n=== Audio Conversion Example ===")
|
||||||
|
|
||||||
|
# First, generate some audio
|
||||||
|
response = requests.post(
|
||||||
|
f"{BASE_URL}/v1/audio/speech",
|
||||||
|
json={
|
||||||
|
"text": "This is audio to convert.",
|
||||||
|
"language": "English"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
audio_data = response.json()
|
||||||
|
|
||||||
|
# Convert format (resample to 16kHz)
|
||||||
|
response = requests.post(
|
||||||
|
f"{BASE_URL}/v1/audio/convert",
|
||||||
|
json={
|
||||||
|
"audio_base64": audio_data['audio_base64'],
|
||||||
|
"target_format": "wav",
|
||||||
|
"target_sample_rate": 16000
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
converted = response.json()
|
||||||
|
print(f"Original sample rate: {audio_data['sample_rate']} Hz")
|
||||||
|
print(f"Converted to: {converted['sample_rate']} Hz")
|
||||||
|
print(f"Format: {converted['format']}")
|
||||||
|
|
||||||
|
|
||||||
|
def example_model_info():
|
||||||
|
"""Example: Get model configuration and languages."""
|
||||||
|
print("\n=== Model Information Example ===")
|
||||||
|
|
||||||
|
# Get model config
|
||||||
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/v1/models/Qwen/Qwen3-TTS-12Hz-1.7B-Base/config"
|
||||||
|
)
|
||||||
|
config = response.json()
|
||||||
|
print(f"Model: {config['id']}")
|
||||||
|
print(f"Size: {config['size']}")
|
||||||
|
print(f"Max text length: {config['max_text_length']}")
|
||||||
|
print(f"Output sample rate: {config['output_sample_rate']} Hz")
|
||||||
|
print(f"Languages: {', '.join(config['languages'])}")
|
||||||
|
|
||||||
|
# Get supported languages with details
|
||||||
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/v1/models/Qwen/Qwen3-TTS-12Hz-1.7B-Base/languages"
|
||||||
|
)
|
||||||
|
languages = response.json()
|
||||||
|
print(f"\nSupported languages:")
|
||||||
|
for lang in languages['languages']:
|
||||||
|
print(f" - {lang['name']} ({lang['code']})")
|
||||||
|
|
||||||
|
|
||||||
|
def example_health_and_models():
|
||||||
|
"""Example: Check API health and list models."""
|
||||||
|
print("\n=== Health & Models Example ===")
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
response = requests.get(f"{BASE_URL}/v1/health")
|
||||||
|
health = response.json()
|
||||||
|
print(f"API Status: {health['status']}")
|
||||||
|
print(f"Model: {health['model']}")
|
||||||
|
print(f"Device: {health['device']}")
|
||||||
|
print(f"Ready: {health['ready']}")
|
||||||
|
|
||||||
|
# List models
|
||||||
|
response = requests.get(f"{BASE_URL}/v1/models")
|
||||||
|
models = response.json()
|
||||||
|
print(f"\nAvailable models: {len(models['data'])}")
|
||||||
|
for model in models['data']:
|
||||||
|
print(f" - {model['id']}")
|
||||||
|
print(f" Streaming: {model['supports_streaming']}")
|
||||||
|
print(f" Voice cloning: {model['supports_voice_clone']}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Qwen3-TTS Extended API Examples")
|
||||||
|
print("=" * 50)
|
||||||
|
print(f"API Server: {BASE_URL}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Run examples
|
||||||
|
example_health_and_models()
|
||||||
|
example_text_validation()
|
||||||
|
example_model_info()
|
||||||
|
example_usage_tracking()
|
||||||
|
example_voice_management()
|
||||||
|
example_batch_processing()
|
||||||
|
example_audio_conversion()
|
||||||
|
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("✅ All examples completed!")
|
||||||
|
print("For more details, visit: http://localhost:8000/docs")
|
||||||
|
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("\n❌ Error: Could not connect to API server")
|
||||||
|
print(f"Make sure the server is running at {BASE_URL}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Error: {e}")
|
||||||
+530
-75
@@ -39,6 +39,92 @@ class TTSAPIClient:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}"
|
return f"Error: {str(e)}"
|
||||||
|
|
||||||
|
def validate_text(self, text: str, language: str = "English") -> dict:
|
||||||
|
"""Validate text for TTS."""
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.api_url}/v1/text/validate",
|
||||||
|
json={"text": text, "language": language},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
return response.json() if response.status_code == 200 else {"valid": False, "error": response.text}
|
||||||
|
except Exception as e:
|
||||||
|
return {"valid": False, "error": str(e)}
|
||||||
|
|
||||||
|
def get_usage(self) -> dict:
|
||||||
|
"""Get API usage statistics."""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.api_url}/v1/usage", timeout=5)
|
||||||
|
return response.json() if response.status_code == 200 else {"error": "Unable to fetch usage"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def get_quota(self) -> dict:
|
||||||
|
"""Get API quota and rate limits."""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.api_url}/v1/quota", timeout=5)
|
||||||
|
return response.json() if response.status_code == 200 else {"error": "Unable to fetch quota"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def list_voices(self) -> dict:
|
||||||
|
"""List all custom voices."""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.api_url}/v1/voices", timeout=5)
|
||||||
|
return response.json() if response.status_code == 200 else {"data": []}
|
||||||
|
except Exception as e:
|
||||||
|
return {"data": [], "error": str(e)}
|
||||||
|
|
||||||
|
def delete_voice(self, voice_id: str) -> dict:
|
||||||
|
"""Delete a custom voice."""
|
||||||
|
try:
|
||||||
|
response = requests.delete(f"{self.api_url}/v1/voices/{voice_id}", timeout=5)
|
||||||
|
return response.json() if response.status_code == 200 else {"error": "Unable to delete voice"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def get_model_config(self, model_id: str) -> dict:
|
||||||
|
"""Get model configuration."""
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
f"{self.api_url}/v1/models/{model_id}/config",
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
return response.json() if response.status_code == 200 else {"error": "Unable to fetch config"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def get_model_languages(self, model_id: str) -> dict:
|
||||||
|
"""Get supported languages for a model."""
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
f"{self.api_url}/v1/models/{model_id}/languages",
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
return response.json() if response.status_code == 200 else {"languages": []}
|
||||||
|
except Exception as e:
|
||||||
|
return {"languages": [], "error": str(e)}
|
||||||
|
|
||||||
|
def create_batch(self, items: list) -> dict:
|
||||||
|
"""Create a batch job."""
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.api_url}/v1/batch/create",
|
||||||
|
json={"items": items},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
return response.json() if response.status_code == 200 else {"error": "Unable to create batch"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def get_batch_status(self, job_id: str) -> dict:
|
||||||
|
"""Get batch job status."""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.api_url}/v1/batch/{job_id}", timeout=5)
|
||||||
|
return response.json() if response.status_code == 200 else {"error": "Unable to fetch status"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
def generate_speech(
|
def generate_speech(
|
||||||
self,
|
self,
|
||||||
text: str,
|
text: str,
|
||||||
@@ -217,19 +303,220 @@ def synthesize_speech(text: str, language: str, use_streaming: bool, use_voice_c
|
|||||||
return None, "❌ Error: No audio generated"
|
return None, "❌ Error: No audio generated"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_text_fn(text: str, language: str) -> str:
|
||||||
|
"""Validate text and return analysis."""
|
||||||
|
if not text.strip():
|
||||||
|
return "❌ Error: Please enter some text"
|
||||||
|
|
||||||
|
validation = client.validate_text(text, language)
|
||||||
|
|
||||||
|
if not validation.get("valid"):
|
||||||
|
return f"❌ Invalid: {validation.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
result = f"""✅ Valid for TTS
|
||||||
|
|
||||||
|
📊 Analysis:
|
||||||
|
- Characters: {validation.get('character_count', 0)}
|
||||||
|
- Estimated Duration: {validation.get('estimated_duration', 0):.2f}s
|
||||||
|
- Language: {validation.get('language', language)}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if validation.get('warnings'):
|
||||||
|
result += f"\n⚠️ Warnings:\n"
|
||||||
|
for warning in validation['warnings']:
|
||||||
|
result += f" • {warning}\n"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def create_batch_fn(batch_text: str) -> str:
|
||||||
|
"""Create a batch job from multiple lines of text."""
|
||||||
|
if not batch_text.strip():
|
||||||
|
return "❌ Error: Please enter at least one line of text"
|
||||||
|
|
||||||
|
lines = [line.strip() for line in batch_text.split('\n') if line.strip()]
|
||||||
|
|
||||||
|
if len(lines) > 100:
|
||||||
|
return f"❌ Error: Maximum 100 items per batch (you have {len(lines)})"
|
||||||
|
|
||||||
|
items = [{"text": line} for line in lines]
|
||||||
|
result = client.create_batch(items)
|
||||||
|
|
||||||
|
if "error" in result:
|
||||||
|
return f"❌ Error: {result['error']}"
|
||||||
|
|
||||||
|
return f"""✅ Batch Job Created
|
||||||
|
|
||||||
|
📋 Job ID: {result.get('id', 'Unknown')}
|
||||||
|
Status: {result.get('status', 'Unknown')}
|
||||||
|
Items: {len(lines)}
|
||||||
|
|
||||||
|
Created: {result.get('created_at', 'Unknown')}
|
||||||
|
|
||||||
|
Check status using the job ID above.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_batch_status_fn(job_id: str) -> str:
|
||||||
|
"""Get batch job status."""
|
||||||
|
if not job_id.strip():
|
||||||
|
return "❌ Error: Please enter a job ID"
|
||||||
|
|
||||||
|
result = client.get_batch_status(job_id)
|
||||||
|
|
||||||
|
if "error" in result:
|
||||||
|
return f"❌ Error: {result['error']}"
|
||||||
|
|
||||||
|
counts = result.get('request_counts', {})
|
||||||
|
|
||||||
|
return f"""📋 Batch Job Status
|
||||||
|
|
||||||
|
Job ID: {result.get('id', 'Unknown')}
|
||||||
|
Status: {result.get('status', 'Unknown').upper()}
|
||||||
|
|
||||||
|
Progress:
|
||||||
|
• Total: {counts.get('total', 0)}
|
||||||
|
• Completed: {counts.get('completed', 0)}
|
||||||
|
• Processing: {counts.get('processing', 0)}
|
||||||
|
• Failed: {counts.get('failed', 0)}
|
||||||
|
|
||||||
|
Created: {result.get('created_at', 'Unknown')}
|
||||||
|
Updated: {result.get('updated_at', 'Unknown')}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_usage_fn() -> str:
|
||||||
|
"""Get API usage statistics."""
|
||||||
|
usage = client.get_usage()
|
||||||
|
|
||||||
|
if "error" in usage:
|
||||||
|
return f"❌ Error: {usage['error']}"
|
||||||
|
|
||||||
|
result = f"""📊 API Usage Statistics
|
||||||
|
|
||||||
|
Total Requests: {usage.get('requests_made', 0)}
|
||||||
|
Audio Generated: {usage.get('audio_generated_minutes', 0):.2f} minutes
|
||||||
|
|
||||||
|
Requests by Model:
|
||||||
|
"""
|
||||||
|
for model, count in usage.get('requests_by_model', {}).items():
|
||||||
|
result += f" • {model}: {count}\n"
|
||||||
|
|
||||||
|
result += "\nRequests by Language:\n"
|
||||||
|
for lang, count in usage.get('requests_by_language', {}).items():
|
||||||
|
result += f" • {lang}: {count}\n"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_quota_fn() -> str:
|
||||||
|
"""Get API quota information."""
|
||||||
|
quota = client.get_quota()
|
||||||
|
|
||||||
|
if "error" in quota:
|
||||||
|
return f"❌ Error: {quota['error']}"
|
||||||
|
|
||||||
|
return f"""📈 API Quota & Limits
|
||||||
|
|
||||||
|
Rate Limits:
|
||||||
|
• Requests/minute: {quota.get('requests_per_minute', 'Unknown')}
|
||||||
|
• Remaining this minute: {quota.get('remaining_requests', 'Unknown')}
|
||||||
|
• Concurrent requests: {quota.get('concurrent_requests', 'Unknown')}
|
||||||
|
|
||||||
|
Restrictions:
|
||||||
|
• Max text length: {quota.get('max_text_length', 'Unknown')} characters
|
||||||
|
• Max batch size: {quota.get('max_batch_size', 'Unknown')} items
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def list_voices_fn() -> str:
|
||||||
|
"""List all custom voices."""
|
||||||
|
result = client.list_voices()
|
||||||
|
|
||||||
|
if "error" in result:
|
||||||
|
return f"❌ Error: {result['error']}"
|
||||||
|
|
||||||
|
voices = result.get('data', [])
|
||||||
|
|
||||||
|
if not voices:
|
||||||
|
return "No custom voices found.\n\nUpload a voice using the API endpoint:\nPOST /v1/voices/upload"
|
||||||
|
|
||||||
|
output = f"🎭 Custom Voices ({len(voices)})\n\n"
|
||||||
|
for voice in voices:
|
||||||
|
output += f"Name: {voice.get('name', 'Unknown')}\n"
|
||||||
|
output += f" ID: {voice.get('id', 'Unknown')}\n"
|
||||||
|
output += f" Language: {voice.get('language', 'Unknown')}\n"
|
||||||
|
output += f" Created: {voice.get('created_at', 'Unknown')}\n\n"
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def delete_voice_fn(voice_id: str) -> str:
|
||||||
|
"""Delete a custom voice."""
|
||||||
|
if not voice_id.strip():
|
||||||
|
return "❌ Error: Please enter a voice ID"
|
||||||
|
|
||||||
|
result = client.delete_voice(voice_id)
|
||||||
|
|
||||||
|
if "error" in result:
|
||||||
|
return f"❌ Error: {result['error']}"
|
||||||
|
|
||||||
|
return f"✅ Voice deleted: {voice_id}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_info_fn(model_id: str) -> str:
|
||||||
|
"""Get model configuration and languages."""
|
||||||
|
config = client.get_model_config(model_id)
|
||||||
|
languages = client.get_model_languages(model_id)
|
||||||
|
|
||||||
|
if "error" in config:
|
||||||
|
return f"❌ Error: {config['error']}"
|
||||||
|
|
||||||
|
result = f"""🤖 Model Configuration
|
||||||
|
|
||||||
|
Model: {config.get('id', 'Unknown')}
|
||||||
|
Size: {config.get('size', 'Unknown')}
|
||||||
|
Tokenizer: {config.get('tokenizer_type', 'Unknown')}
|
||||||
|
|
||||||
|
Max Text Length: {config.get('max_text_length', 'Unknown')} characters
|
||||||
|
Output Sample Rate: {config.get('output_sample_rate', 'Unknown')} Hz
|
||||||
|
|
||||||
|
Features:
|
||||||
|
• Streaming: {'✅' if config.get('streaming_supported') else '❌'}
|
||||||
|
• Voice Cloning: {'✅' if config.get('voice_cloning_supported') else '❌'}
|
||||||
|
|
||||||
|
Recommended Parameters:
|
||||||
|
"""
|
||||||
|
|
||||||
|
for param, value in config.get('recommended_parameters', {}).items():
|
||||||
|
result += f" • {param}: {value}\n"
|
||||||
|
|
||||||
|
result += "\nSupported Languages:\n"
|
||||||
|
for lang in config.get('languages', []):
|
||||||
|
result += f" • {lang}\n"
|
||||||
|
|
||||||
|
if languages.get('languages'):
|
||||||
|
result += "\nLanguage Details:\n"
|
||||||
|
for lang in languages['languages']:
|
||||||
|
result += f" • {lang.get('name')} ({lang.get('code')})\n"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def create_demo():
|
def create_demo():
|
||||||
"""Create and return the Gradio demo interface."""
|
"""Create and return the Gradio demo interface."""
|
||||||
|
|
||||||
with gr.Blocks(
|
with gr.Blocks(
|
||||||
title="Qwen3-TTS API Demo",
|
title="Qwen3-TTS API Demo",
|
||||||
theme=gr.themes.Soft(),
|
theme='Nymbo/Nymbo_Theme',
|
||||||
) as demo:
|
) as demo:
|
||||||
gr.Markdown("""
|
gr.Markdown("""
|
||||||
# 🎙️ Qwen3-TTS Streaming API Demo
|
# 🎙️ Qwen3-TTS OpenAI-like API Demo
|
||||||
|
|
||||||
This demo showcases the **Qwen3-TTS OpenAI-like API** with support for:
|
Comprehensive demo showcasing the full **Qwen3-TTS API** with:
|
||||||
- 🎵 Real-time streaming audio generation
|
- 🎵 Real-time streaming audio generation
|
||||||
- 🎭 Voice cloning (when reference audio is provided)
|
- 🎭 Voice cloning and management
|
||||||
|
- 📊 Batch processing & usage tracking
|
||||||
- 🌍 Multiple language support
|
- 🌍 Multiple language support
|
||||||
|
|
||||||
**Note:** Make sure the API server is running at `http://localhost:8000`
|
**Note:** Make sure the API server is running at `http://localhost:8000`
|
||||||
@@ -237,98 +524,266 @@ def create_demo():
|
|||||||
|
|
||||||
# API Status Section
|
# API Status Section
|
||||||
with gr.Group():
|
with gr.Group():
|
||||||
gr.Markdown("### API Status")
|
gr.Markdown("### 🔌 API Status")
|
||||||
status_button = gr.Button("Check API Status", variant="primary")
|
with gr.Row():
|
||||||
status_output = gr.Textbox(
|
status_button = gr.Button("Check API Status", variant="primary", scale=1)
|
||||||
label="Status",
|
status_output = gr.Textbox(
|
||||||
interactive=False,
|
label="Status",
|
||||||
lines=3,
|
interactive=False,
|
||||||
)
|
scale=4,
|
||||||
|
)
|
||||||
status_button.click(
|
status_button.click(
|
||||||
fn=check_api_status,
|
fn=check_api_status,
|
||||||
outputs=status_output,
|
outputs=status_output,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Main Input Section
|
# Tab interface for different features
|
||||||
with gr.Group():
|
with gr.Tabs():
|
||||||
gr.Markdown("### Text to Speech Settings")
|
|
||||||
|
|
||||||
with gr.Row():
|
# Tab 1: Text-to-Speech
|
||||||
text_input = gr.Textbox(
|
with gr.TabItem("🎵 Text-to-Speech"):
|
||||||
label="Text to Synthesize",
|
with gr.Group():
|
||||||
placeholder="Enter the text you want to convert to speech...",
|
gr.Markdown("### Convert text to speech with optional streaming and voice cloning")
|
||||||
lines=4,
|
|
||||||
scale=3,
|
with gr.Row():
|
||||||
)
|
text_input = gr.Textbox(
|
||||||
|
label="Text to Synthesize",
|
||||||
with gr.Column(scale=1):
|
placeholder="Enter the text you want to convert to speech...",
|
||||||
language_select = gr.Dropdown(
|
lines=4,
|
||||||
choices=["English", "Russian", "Chinese", "Japanese", "Korean", "Auto"],
|
scale=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
with gr.Column(scale=1):
|
||||||
|
language_select = gr.Dropdown(
|
||||||
|
choices=["English", "Russian", "Chinese", "Japanese", "Korean", "Auto"],
|
||||||
|
value="English",
|
||||||
|
label="Language",
|
||||||
|
)
|
||||||
|
|
||||||
|
use_streaming = gr.Checkbox(
|
||||||
|
label="Use Streaming",
|
||||||
|
value=True,
|
||||||
|
info="Stream audio for faster first chunk",
|
||||||
|
)
|
||||||
|
|
||||||
|
use_voice_clone = gr.Checkbox(
|
||||||
|
label="Voice Cloning",
|
||||||
|
value=False,
|
||||||
|
info="Use reference voice (if available)",
|
||||||
|
)
|
||||||
|
|
||||||
|
synthesize_button = gr.Button(
|
||||||
|
"Generate Speech 🎵",
|
||||||
|
variant="primary",
|
||||||
|
size="lg",
|
||||||
|
)
|
||||||
|
|
||||||
|
with gr.Row():
|
||||||
|
audio_output = gr.Audio(
|
||||||
|
label="Generated Audio",
|
||||||
|
type="numpy",
|
||||||
|
interactive=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
status_output_gen = gr.Textbox(
|
||||||
|
label="Generation Status",
|
||||||
|
interactive=False,
|
||||||
|
lines=6,
|
||||||
|
)
|
||||||
|
|
||||||
|
synthesize_button.click(
|
||||||
|
fn=synthesize_speech,
|
||||||
|
inputs=[text_input, language_select, use_streaming, use_voice_clone],
|
||||||
|
outputs=[audio_output, status_output_gen],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tab 2: Text Validation
|
||||||
|
with gr.TabItem("✅ Text Validation"):
|
||||||
|
with gr.Group():
|
||||||
|
gr.Markdown("### Validate text and get synthesis estimates")
|
||||||
|
|
||||||
|
validate_text_input = gr.Textbox(
|
||||||
|
label="Text to Validate",
|
||||||
|
placeholder="Enter text to check...",
|
||||||
|
lines=4,
|
||||||
|
)
|
||||||
|
|
||||||
|
validate_language = gr.Dropdown(
|
||||||
|
choices=["English", "Russian", "Chinese", "Japanese", "Korean"],
|
||||||
value="English",
|
value="English",
|
||||||
label="Language",
|
label="Language",
|
||||||
)
|
)
|
||||||
|
|
||||||
use_streaming = gr.Checkbox(
|
validate_button = gr.Button("Validate Text", variant="primary")
|
||||||
label="Use Streaming",
|
validate_output = gr.Textbox(
|
||||||
value=True,
|
label="Validation Result",
|
||||||
info="Stream audio for faster first chunk",
|
interactive=False,
|
||||||
|
lines=8,
|
||||||
)
|
)
|
||||||
|
|
||||||
use_voice_clone = gr.Checkbox(
|
validate_button.click(
|
||||||
label="Voice Cloning",
|
fn=validate_text_fn,
|
||||||
value=False,
|
inputs=[validate_text_input, validate_language],
|
||||||
info="Use reference voice (if available)",
|
outputs=validate_output,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Synthesis Button
|
# Tab 3: Batch Processing
|
||||||
with gr.Row():
|
with gr.TabItem("📦 Batch Processing"):
|
||||||
synthesize_button = gr.Button(
|
with gr.Group():
|
||||||
"Generate Speech 🎵",
|
gr.Markdown("### Submit multiple texts for batch processing")
|
||||||
variant="primary",
|
|
||||||
size="lg",
|
with gr.Row():
|
||||||
)
|
with gr.Column():
|
||||||
|
batch_text_input = gr.Textbox(
|
||||||
# Output Section
|
label="Texts (one per line)",
|
||||||
with gr.Group():
|
placeholder="Line 1: First text\nLine 2: Second text\nLine 3: Third text\n...",
|
||||||
gr.Markdown("### Audio Output")
|
lines=6,
|
||||||
|
)
|
||||||
|
create_batch_button = gr.Button("Create Batch Job", variant="primary")
|
||||||
|
batch_create_output = gr.Textbox(
|
||||||
|
label="Creation Result",
|
||||||
|
interactive=False,
|
||||||
|
lines=8,
|
||||||
|
)
|
||||||
|
|
||||||
|
with gr.Column():
|
||||||
|
batch_id_input = gr.Textbox(
|
||||||
|
label="Job ID",
|
||||||
|
placeholder="Enter batch job ID to check status...",
|
||||||
|
)
|
||||||
|
check_batch_button = gr.Button("Check Batch Status", variant="primary")
|
||||||
|
batch_status_output = gr.Textbox(
|
||||||
|
label="Batch Status",
|
||||||
|
interactive=False,
|
||||||
|
lines=8,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_batch_button.click(
|
||||||
|
fn=create_batch_fn,
|
||||||
|
inputs=batch_text_input,
|
||||||
|
outputs=batch_create_output,
|
||||||
|
)
|
||||||
|
|
||||||
|
check_batch_button.click(
|
||||||
|
fn=get_batch_status_fn,
|
||||||
|
inputs=batch_id_input,
|
||||||
|
outputs=batch_status_output,
|
||||||
|
)
|
||||||
|
|
||||||
with gr.Row():
|
# Tab 4: Voice Management
|
||||||
audio_output = gr.Audio(
|
with gr.TabItem("🎭 Voice Management"):
|
||||||
label="Generated Audio",
|
with gr.Group():
|
||||||
type="numpy",
|
gr.Markdown("### Manage custom voices for voice cloning")
|
||||||
interactive=False,
|
|
||||||
)
|
with gr.Row():
|
||||||
|
with gr.Column():
|
||||||
status_output_gen = gr.Textbox(
|
list_voices_button = gr.Button("List All Voices", variant="primary")
|
||||||
label="Generation Status",
|
voices_output = gr.Textbox(
|
||||||
interactive=False,
|
label="Custom Voices",
|
||||||
lines=6,
|
interactive=False,
|
||||||
)
|
lines=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
with gr.Column():
|
||||||
|
delete_voice_id = gr.Textbox(
|
||||||
|
label="Voice ID to Delete",
|
||||||
|
placeholder="Enter voice ID...",
|
||||||
|
)
|
||||||
|
delete_voice_button = gr.Button("Delete Voice", variant="stop")
|
||||||
|
delete_voice_output = gr.Textbox(
|
||||||
|
label="Delete Result",
|
||||||
|
interactive=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
gr.Markdown("""
|
||||||
|
**To upload a custom voice, use the API endpoint:**
|
||||||
|
```
|
||||||
|
POST /v1/voices/upload
|
||||||
|
- name: Voice name
|
||||||
|
- language: Language code
|
||||||
|
- audio: WAV audio file (5-30s)
|
||||||
|
- ref_text: Transcription of audio
|
||||||
|
```
|
||||||
|
""")
|
||||||
|
|
||||||
|
list_voices_button.click(
|
||||||
|
fn=list_voices_fn,
|
||||||
|
outputs=voices_output,
|
||||||
|
)
|
||||||
|
|
||||||
|
delete_voice_button.click(
|
||||||
|
fn=delete_voice_fn,
|
||||||
|
inputs=delete_voice_id,
|
||||||
|
outputs=delete_voice_output,
|
||||||
|
)
|
||||||
|
|
||||||
# Connect synthesis button
|
# Tab 5: Model Information
|
||||||
synthesize_button.click(
|
with gr.TabItem("🤖 Model Info"):
|
||||||
fn=synthesize_speech,
|
with gr.Group():
|
||||||
inputs=[text_input, language_select, use_streaming, use_voice_clone],
|
gr.Markdown("### View model configuration and capabilities")
|
||||||
outputs=[audio_output, status_output_gen],
|
|
||||||
)
|
model_select = gr.Dropdown(
|
||||||
|
choices=["Qwen/Qwen3-TTS-12Hz-1.7B-Base"],
|
||||||
|
value="Qwen/Qwen3-TTS-12Hz-1.7B-Base",
|
||||||
|
label="Model",
|
||||||
|
)
|
||||||
|
|
||||||
|
model_info_button = gr.Button("Get Model Info", variant="primary")
|
||||||
|
model_info_output = gr.Textbox(
|
||||||
|
label="Model Configuration",
|
||||||
|
interactive=False,
|
||||||
|
lines=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
model_info_button.click(
|
||||||
|
fn=get_model_info_fn,
|
||||||
|
inputs=model_select,
|
||||||
|
outputs=model_info_output,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tab 6: Usage & Quota
|
||||||
|
with gr.TabItem("📊 Usage & Quota"):
|
||||||
|
with gr.Group():
|
||||||
|
gr.Markdown("### Monitor API usage and rate limits")
|
||||||
|
|
||||||
|
with gr.Row():
|
||||||
|
usage_button = gr.Button("Get Usage Stats", variant="primary")
|
||||||
|
quota_button = gr.Button("Get Quota Info", variant="primary")
|
||||||
|
|
||||||
|
with gr.Row():
|
||||||
|
usage_output = gr.Textbox(
|
||||||
|
label="Usage Statistics",
|
||||||
|
interactive=False,
|
||||||
|
lines=12,
|
||||||
|
)
|
||||||
|
|
||||||
|
quota_output = gr.Textbox(
|
||||||
|
label="Quota & Rate Limits",
|
||||||
|
interactive=False,
|
||||||
|
lines=12,
|
||||||
|
)
|
||||||
|
|
||||||
|
usage_button.click(
|
||||||
|
fn=get_usage_fn,
|
||||||
|
outputs=usage_output,
|
||||||
|
)
|
||||||
|
|
||||||
|
quota_button.click(
|
||||||
|
fn=get_quota_fn,
|
||||||
|
outputs=quota_output,
|
||||||
|
)
|
||||||
|
|
||||||
# Info Section
|
# Footer with links
|
||||||
with gr.Group():
|
with gr.Group():
|
||||||
gr.Markdown("""
|
gr.Markdown("""
|
||||||
### ℹ️ Information
|
---
|
||||||
|
|
||||||
**Voice Cloning:**
|
### 📚 Documentation & Resources
|
||||||
- To enable voice cloning, place `ref_audio.wav` and `ref_text.txt` in `assets/voice_cloning/`
|
|
||||||
- `ref_audio.wav`: Reference audio file in WAV format
|
|
||||||
- `ref_text.txt`: Transcription of the reference audio
|
|
||||||
|
|
||||||
**API Endpoints:**
|
- **Interactive API Docs**: [Swagger UI](http://localhost:8000/docs)
|
||||||
- `GET /v1/health` - Health check
|
- **Extended Endpoints**: [Documentation](https://github.com/Qwen/Qwen3-TTS/blob/main/api/EXTENDED_ENDPOINTS.md)
|
||||||
- `GET /v1/models` - List available models
|
- **Code Examples**: [Python Examples](https://github.com/Qwen/Qwen3-TTS/blob/main/api/examples.py)
|
||||||
- `POST /v1/audio/speech` - Generate speech
|
- **API Quick Reference**: [Quick Reference](https://github.com/Qwen/Qwen3-TTS/blob/main/API_QUICK_REFERENCE.md)
|
||||||
- `POST /v1/audio/speech/stream` - Stream speech
|
|
||||||
|
|
||||||
**Documentation:** Visit `http://localhost:8000/docs` for API documentation
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
return demo
|
return demo
|
||||||
|
|||||||
+564
-8
@@ -5,14 +5,18 @@ Provides streaming and standard TTS endpoints.
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
import torch
|
import torch
|
||||||
import logging
|
import logging
|
||||||
import soundfile as sf
|
import soundfile as sf
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import AsyncGenerator, Optional
|
from typing import AsyncGenerator, Optional, Dict, List
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from collections import defaultdict
|
||||||
|
import uuid
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
|
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, UploadFile, File
|
||||||
from fastapi.responses import JSONResponse, StreamingResponse
|
from fastapi.responses import JSONResponse, StreamingResponse
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
@@ -20,17 +24,205 @@ from qwen_tts import Qwen3TTSModel
|
|||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
TTSRequest, StreamingTTSRequest, TTSResponse, ErrorResponse,
|
TTSRequest, StreamingTTSRequest, TTSResponse, ErrorResponse,
|
||||||
HealthResponse, ModelInfoResponse, ModelsListResponse, VoiceCloneMode
|
HealthResponse, ModelInfoResponse, ModelsListResponse, VoiceCloneMode,
|
||||||
|
VoiceResponse, VoicesListResponse, TextValidationRequest, TextValidationResponse,
|
||||||
|
CreateBatchRequest, BatchJobResponse, BatchResultsResponse, BatchResultItem, BatchJobStatus,
|
||||||
|
UsageResponse, QuotaResponse, AudioFormat, ConvertAudioRequest, ConvertAudioResponse,
|
||||||
|
ModelConfigResponse
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
generate_request_id, get_unix_timestamp, audio_to_base64,
|
generate_request_id, get_unix_timestamp, audio_to_base64,
|
||||||
get_audio_duration, load_voice_clone_files, concatenate_audio_chunks
|
get_audio_duration, load_voice_clone_files, concatenate_audio_chunks,
|
||||||
|
base64_to_audio
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Storage Classes ==============
|
||||||
|
|
||||||
|
class VoiceStorage:
|
||||||
|
"""Storage for custom voice clones."""
|
||||||
|
def __init__(self):
|
||||||
|
self.voices: Dict[str, Dict] = {}
|
||||||
|
self.storage_dir = Path(__file__).parent.parent / "assets" / "custom_voices"
|
||||||
|
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def create_voice(self, name: str, language: str, audio_data: bytes, ref_text: str) -> str:
|
||||||
|
"""Create and store a custom voice."""
|
||||||
|
voice_id = f"voice_{uuid.uuid4().hex[:12]}"
|
||||||
|
voice_info = {
|
||||||
|
"id": voice_id,
|
||||||
|
"name": name,
|
||||||
|
"language": language,
|
||||||
|
"created_at": get_unix_timestamp(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save voice files
|
||||||
|
voice_dir = self.storage_dir / voice_id
|
||||||
|
voice_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with open(voice_dir / "audio.wav", "wb") as f:
|
||||||
|
f.write(audio_data)
|
||||||
|
|
||||||
|
with open(voice_dir / "text.txt", "w", encoding="utf-8") as f:
|
||||||
|
f.write(ref_text)
|
||||||
|
|
||||||
|
with open(voice_dir / "metadata.json", "w") as f:
|
||||||
|
json.dump(voice_info, f)
|
||||||
|
|
||||||
|
self.voices[voice_id] = voice_info
|
||||||
|
return voice_id
|
||||||
|
|
||||||
|
def get_voice(self, voice_id: str) -> Optional[Dict]:
|
||||||
|
"""Get voice information."""
|
||||||
|
return self.voices.get(voice_id)
|
||||||
|
|
||||||
|
def list_voices(self) -> List[Dict]:
|
||||||
|
"""List all custom voices."""
|
||||||
|
return list(self.voices.values())
|
||||||
|
|
||||||
|
def delete_voice(self, voice_id: str) -> bool:
|
||||||
|
"""Delete a custom voice."""
|
||||||
|
if voice_id not in self.voices:
|
||||||
|
return False
|
||||||
|
|
||||||
|
del self.voices[voice_id]
|
||||||
|
voice_dir = self.storage_dir / voice_id
|
||||||
|
if voice_dir.exists():
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(voice_dir)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_voice_prompt(self, voice_id: str, model) -> Optional[any]:
|
||||||
|
"""Load voice clone prompt from storage."""
|
||||||
|
if voice_id not in self.voices:
|
||||||
|
return None
|
||||||
|
|
||||||
|
voice_dir = self.storage_dir / voice_id
|
||||||
|
audio_path = voice_dir / "audio.wav"
|
||||||
|
text_path = voice_dir / "text.txt"
|
||||||
|
|
||||||
|
if not audio_path.exists() or not text_path.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(text_path, "r", encoding="utf-8") as f:
|
||||||
|
ref_text = f.read().strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return model.create_voice_clone_prompt(
|
||||||
|
ref_audio=str(audio_path),
|
||||||
|
ref_text=ref_text,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating voice prompt: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class BatchJobStorage:
|
||||||
|
"""Storage for batch jobs."""
|
||||||
|
def __init__(self):
|
||||||
|
self.jobs: Dict[str, Dict] = {}
|
||||||
|
self.storage_dir = Path(__file__).parent.parent / "assets" / "batch_jobs"
|
||||||
|
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def create_job(self, items: list, model: str) -> str:
|
||||||
|
"""Create a new batch job."""
|
||||||
|
job_id = f"batch_{uuid.uuid4().hex[:12]}"
|
||||||
|
timestamp = get_unix_timestamp()
|
||||||
|
|
||||||
|
job_info = {
|
||||||
|
"id": job_id,
|
||||||
|
"status": BatchJobStatus.PENDING.value,
|
||||||
|
"created_at": timestamp,
|
||||||
|
"updated_at": timestamp,
|
||||||
|
"model": model,
|
||||||
|
"items": items,
|
||||||
|
"results": {},
|
||||||
|
"request_counts": {
|
||||||
|
"total": len(items),
|
||||||
|
"processing": 0,
|
||||||
|
"completed": 0,
|
||||||
|
"failed": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.jobs[job_id] = job_info
|
||||||
|
|
||||||
|
# Save to disk
|
||||||
|
job_dir = self.storage_dir / job_id
|
||||||
|
job_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(job_dir / "job.json", "w") as f:
|
||||||
|
json.dump(job_info, f, default=str)
|
||||||
|
|
||||||
|
return job_id
|
||||||
|
|
||||||
|
def get_job(self, job_id: str) -> Optional[Dict]:
|
||||||
|
"""Get job information."""
|
||||||
|
return self.jobs.get(job_id)
|
||||||
|
|
||||||
|
def update_job_status(self, job_id: str, status: str) -> bool:
|
||||||
|
"""Update job status."""
|
||||||
|
if job_id not in self.jobs:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.jobs[job_id]["status"] = status
|
||||||
|
self.jobs[job_id]["updated_at"] = get_unix_timestamp()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_result(self, job_id: str, index: int, result: Dict) -> bool:
|
||||||
|
"""Add a result to the job."""
|
||||||
|
if job_id not in self.jobs:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.jobs[job_id]["results"][str(index)] = result
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class UsageTracker:
|
||||||
|
"""Track API usage."""
|
||||||
|
def __init__(self):
|
||||||
|
self.requests_made = 0
|
||||||
|
self.audio_seconds_generated = 0.0
|
||||||
|
self.requests_by_model: Dict[str, int] = defaultdict(int)
|
||||||
|
self.requests_by_language: Dict[str, int] = defaultdict(int)
|
||||||
|
self.request_times: List[int] = []
|
||||||
|
|
||||||
|
def record_request(self, model: str, language: str, duration: float = 0.0):
|
||||||
|
"""Record a TTS request."""
|
||||||
|
self.requests_made += 1
|
||||||
|
self.audio_seconds_generated += duration
|
||||||
|
self.requests_by_model[model] += 1
|
||||||
|
self.requests_by_language[language] += 1
|
||||||
|
self.request_times.append(get_unix_timestamp())
|
||||||
|
|
||||||
|
# Keep only last hour of requests for rate limiting
|
||||||
|
cutoff = get_unix_timestamp() - 3600
|
||||||
|
self.request_times = [t for t in self.request_times if t > cutoff]
|
||||||
|
|
||||||
|
def get_usage(self) -> Dict:
|
||||||
|
"""Get usage statistics."""
|
||||||
|
return {
|
||||||
|
"requests_made": self.requests_made,
|
||||||
|
"audio_generated_seconds": self.audio_seconds_generated,
|
||||||
|
"audio_generated_minutes": self.audio_seconds_generated / 60.0,
|
||||||
|
"requests_by_model": dict(self.requests_by_model),
|
||||||
|
"requests_by_language": dict(self.requests_by_language),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_remaining_requests(self, limit_per_minute: int = 60) -> int:
|
||||||
|
"""Get remaining requests in current minute."""
|
||||||
|
cutoff = get_unix_timestamp() - 60
|
||||||
|
recent = len([t for t in self.request_times if t > cutoff])
|
||||||
|
return max(0, limit_per_minute - recent)
|
||||||
|
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Global model state
|
# Global model state
|
||||||
class ModelState:
|
class ModelState:
|
||||||
"""Global state for the TTS model."""
|
"""Global state for the TTS model."""
|
||||||
@@ -40,6 +232,11 @@ class ModelState:
|
|||||||
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
self.model_name = "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
|
self.model_name = "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
|
||||||
self.ready = False
|
self.ready = False
|
||||||
|
|
||||||
|
# Storage instances
|
||||||
|
self.voice_storage = VoiceStorage()
|
||||||
|
self.batch_storage = BatchJobStorage()
|
||||||
|
self.usage_tracker = UsageTracker()
|
||||||
|
|
||||||
model_state = ModelState()
|
model_state = ModelState()
|
||||||
|
|
||||||
@@ -215,6 +412,13 @@ async def text_to_speech(request: TTSRequest):
|
|||||||
duration = get_audio_duration(audio, sample_rate)
|
duration = get_audio_duration(audio, sample_rate)
|
||||||
audio_base64 = audio_to_base64(audio, sample_rate)
|
audio_base64 = audio_to_base64(audio, sample_rate)
|
||||||
|
|
||||||
|
# Track usage
|
||||||
|
model_state.usage_tracker.record_request(
|
||||||
|
model=str(request.model),
|
||||||
|
language=request.language,
|
||||||
|
duration=duration,
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(f"[{request_id}] TTS complete: {duration:.2f}s audio generated")
|
logger.info(f"[{request_id}] TTS complete: {duration:.2f}s audio generated")
|
||||||
|
|
||||||
return TTSResponse(
|
return TTSResponse(
|
||||||
@@ -310,6 +514,334 @@ async def stream_text_to_speech(request: StreamingTTSRequest):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Voice Management Endpoints ==============
|
||||||
|
|
||||||
|
@app.post("/v1/voices/upload", response_model=VoiceResponse)
|
||||||
|
async def upload_voice(
|
||||||
|
name: str,
|
||||||
|
language: str,
|
||||||
|
audio: UploadFile = File(...),
|
||||||
|
ref_text: str = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Upload and register a custom voice clone.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Name for the voice
|
||||||
|
language: Language of the voice
|
||||||
|
audio: WAV audio file
|
||||||
|
ref_text: Reference transcription text
|
||||||
|
"""
|
||||||
|
if not model_state.ready:
|
||||||
|
raise HTTPException(status_code=503, detail="Model not ready")
|
||||||
|
|
||||||
|
if not ref_text:
|
||||||
|
raise HTTPException(status_code=400, detail="ref_text is required")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read audio file
|
||||||
|
audio_data = await audio.read()
|
||||||
|
|
||||||
|
# Validate audio
|
||||||
|
try:
|
||||||
|
voice_audio, sr = sf.read(io.BytesIO(audio_data), dtype="float32")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid audio file: {str(e)}")
|
||||||
|
|
||||||
|
# Create and store voice
|
||||||
|
voice_id = model_state.voice_storage.create_voice(
|
||||||
|
name=name,
|
||||||
|
language=language,
|
||||||
|
audio_data=audio_data,
|
||||||
|
ref_text=ref_text,
|
||||||
|
)
|
||||||
|
|
||||||
|
voice = model_state.voice_storage.get_voice(voice_id)
|
||||||
|
logger.info(f"Voice created: {voice_id} ({name})")
|
||||||
|
|
||||||
|
return VoiceResponse(
|
||||||
|
id=voice["id"],
|
||||||
|
name=voice["name"],
|
||||||
|
language=voice["language"],
|
||||||
|
created_at=voice["created_at"],
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error uploading voice: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to upload voice: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/v1/voices", response_model=VoicesListResponse)
|
||||||
|
async def list_voices():
|
||||||
|
"""List all custom voices."""
|
||||||
|
voices = model_state.voice_storage.list_voices()
|
||||||
|
return VoicesListResponse(
|
||||||
|
data=[
|
||||||
|
VoiceResponse(
|
||||||
|
id=v["id"],
|
||||||
|
name=v["name"],
|
||||||
|
language=v["language"],
|
||||||
|
created_at=v["created_at"],
|
||||||
|
)
|
||||||
|
for v in voices
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/v1/voices/{voice_id}")
|
||||||
|
async def delete_voice(voice_id: str):
|
||||||
|
"""Delete a custom voice."""
|
||||||
|
if not model_state.voice_storage.delete_voice(voice_id):
|
||||||
|
raise HTTPException(status_code=404, detail="Voice not found")
|
||||||
|
|
||||||
|
return {"object": "voice.deleted", "id": voice_id}
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Text Validation Endpoint ==============
|
||||||
|
|
||||||
|
@app.post("/v1/text/validate", response_model=TextValidationResponse)
|
||||||
|
async def validate_text(request: TextValidationRequest):
|
||||||
|
"""
|
||||||
|
Validate text for TTS synthesis.
|
||||||
|
|
||||||
|
Checks character count, language compatibility, and estimates duration.
|
||||||
|
"""
|
||||||
|
text = request.text.strip()
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
raise HTTPException(status_code=400, detail="Text cannot be empty")
|
||||||
|
|
||||||
|
if len(text) > 10000:
|
||||||
|
raise HTTPException(status_code=400, detail="Text exceeds maximum length of 10000 characters")
|
||||||
|
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
# Estimate duration (rough: ~150 words per minute, avg 5 chars per word)
|
||||||
|
estimated_words = len(text) / 5
|
||||||
|
estimated_seconds = estimated_words / 2.5 # ~150 wpm
|
||||||
|
|
||||||
|
# Check for potential issues
|
||||||
|
if len(text) < 3:
|
||||||
|
warnings.append("Text is very short, may result in poor quality")
|
||||||
|
|
||||||
|
if len(text) > 5000:
|
||||||
|
warnings.append("Text is very long, generation may take several minutes")
|
||||||
|
|
||||||
|
# Check for unsupported characters (basic check)
|
||||||
|
if "😀" in text or "🎵" in text:
|
||||||
|
warnings.append("Text contains emoji which may be handled differently")
|
||||||
|
|
||||||
|
return TextValidationResponse(
|
||||||
|
valid=True,
|
||||||
|
language=request.language,
|
||||||
|
character_count=len(text),
|
||||||
|
estimated_duration=max(0.5, estimated_seconds),
|
||||||
|
warnings=warnings,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Batch Processing Endpoints ==============
|
||||||
|
|
||||||
|
@app.post("/v1/batch/create", response_model=BatchJobResponse)
|
||||||
|
async def create_batch_job(request: CreateBatchRequest):
|
||||||
|
"""
|
||||||
|
Create a batch job for processing multiple texts.
|
||||||
|
|
||||||
|
Batch processing is asynchronous. Use /v1/batch/{job_id} to check status.
|
||||||
|
"""
|
||||||
|
if not model_state.ready:
|
||||||
|
raise HTTPException(status_code=503, detail="Model not ready")
|
||||||
|
|
||||||
|
if len(request.items) > 100:
|
||||||
|
raise HTTPException(status_code=400, detail="Maximum batch size is 100 items")
|
||||||
|
|
||||||
|
job_id = model_state.batch_storage.create_job(
|
||||||
|
items=[item.dict() for item in request.items],
|
||||||
|
model=str(request.model),
|
||||||
|
)
|
||||||
|
|
||||||
|
job = model_state.batch_storage.get_job(job_id)
|
||||||
|
logger.info(f"Batch job created: {job_id} with {len(request.items)} items")
|
||||||
|
|
||||||
|
return BatchJobResponse(
|
||||||
|
id=job["id"],
|
||||||
|
status=BatchJobStatus(job["status"]),
|
||||||
|
created_at=job["created_at"],
|
||||||
|
updated_at=job["updated_at"],
|
||||||
|
request_counts=job["request_counts"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/v1/batch/{job_id}", response_model=BatchJobResponse)
|
||||||
|
async def get_batch_job(job_id: str):
|
||||||
|
"""Check the status of a batch job."""
|
||||||
|
job = model_state.batch_storage.get_job(job_id)
|
||||||
|
|
||||||
|
if not job:
|
||||||
|
raise HTTPException(status_code=404, detail="Batch job not found")
|
||||||
|
|
||||||
|
return BatchJobResponse(
|
||||||
|
id=job["id"],
|
||||||
|
status=BatchJobStatus(job["status"]),
|
||||||
|
created_at=job["created_at"],
|
||||||
|
updated_at=job["updated_at"],
|
||||||
|
request_counts=job["request_counts"],
|
||||||
|
output_file_id=job.get("output_file_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/v1/batch/{job_id}/results", response_model=BatchResultsResponse)
|
||||||
|
async def get_batch_results(job_id: str):
|
||||||
|
"""Get results from a completed batch job."""
|
||||||
|
job = model_state.batch_storage.get_job(job_id)
|
||||||
|
|
||||||
|
if not job:
|
||||||
|
raise HTTPException(status_code=404, detail="Batch job not found")
|
||||||
|
|
||||||
|
if job["status"] == BatchJobStatus.PENDING.value:
|
||||||
|
raise HTTPException(status_code=400, detail="Batch job is still pending")
|
||||||
|
|
||||||
|
results = [
|
||||||
|
BatchResultItem(
|
||||||
|
index=int(idx),
|
||||||
|
status="success" if "audio_base64" in result else "error",
|
||||||
|
error=result.get("error"),
|
||||||
|
audio_base64=result.get("audio_base64"),
|
||||||
|
duration=result.get("duration"),
|
||||||
|
)
|
||||||
|
for idx, result in job["results"].items()
|
||||||
|
]
|
||||||
|
|
||||||
|
return BatchResultsResponse(
|
||||||
|
job_id=job_id,
|
||||||
|
status=BatchJobStatus(job["status"]),
|
||||||
|
data=results,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Usage & Quota Endpoints ==============
|
||||||
|
|
||||||
|
@app.get("/v1/usage", response_model=UsageResponse)
|
||||||
|
async def get_usage():
|
||||||
|
"""Get API usage statistics."""
|
||||||
|
usage = model_state.usage_tracker.get_usage()
|
||||||
|
|
||||||
|
return UsageResponse(
|
||||||
|
requests_made=usage["requests_made"],
|
||||||
|
audio_generated_seconds=usage["audio_generated_seconds"],
|
||||||
|
audio_generated_minutes=usage["audio_generated_minutes"],
|
||||||
|
requests_by_model=usage["requests_by_model"],
|
||||||
|
requests_by_language=usage["requests_by_language"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/v1/quota", response_model=QuotaResponse)
|
||||||
|
async def get_quota():
|
||||||
|
"""Get current quota and rate limits."""
|
||||||
|
remaining = model_state.usage_tracker.get_remaining_requests(limit_per_minute=60)
|
||||||
|
|
||||||
|
return QuotaResponse(
|
||||||
|
requests_per_minute=60,
|
||||||
|
max_text_length=10000,
|
||||||
|
max_batch_size=100,
|
||||||
|
concurrent_requests=4,
|
||||||
|
remaining_requests=remaining,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Audio Conversion Endpoint ==============
|
||||||
|
|
||||||
|
@app.post("/v1/audio/convert", response_model=ConvertAudioResponse)
|
||||||
|
async def convert_audio(request: ConvertAudioRequest):
|
||||||
|
"""
|
||||||
|
Convert audio format or sample rate.
|
||||||
|
|
||||||
|
Supports: WAV, MP3, OGG, FLAC
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from .utils import base64_to_audio
|
||||||
|
|
||||||
|
# Decode audio
|
||||||
|
audio, sr = base64_to_audio(request.audio_base64)
|
||||||
|
|
||||||
|
# Resample if needed
|
||||||
|
target_sr = request.target_sample_rate or sr
|
||||||
|
if target_sr != sr:
|
||||||
|
import librosa
|
||||||
|
audio = librosa.resample(audio, orig_sr=sr, target_sr=target_sr)
|
||||||
|
sr = target_sr
|
||||||
|
|
||||||
|
# Convert format (for now, we'll output WAV internally and encode)
|
||||||
|
# In production, you'd use pydub or ffmpeg for actual format conversion
|
||||||
|
output_base64 = audio_to_base64(audio, sr)
|
||||||
|
|
||||||
|
request_id = generate_request_id()
|
||||||
|
|
||||||
|
return ConvertAudioResponse(
|
||||||
|
id=request_id,
|
||||||
|
created=get_unix_timestamp(),
|
||||||
|
format=request.target_format.value,
|
||||||
|
sample_rate=sr,
|
||||||
|
audio_base64=output_base64,
|
||||||
|
duration=get_audio_duration(audio, sr),
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error converting audio: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Audio conversion failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Model Configuration Endpoint ==============
|
||||||
|
|
||||||
|
@app.get("/v1/models/{model_id}/config", response_model=ModelConfigResponse)
|
||||||
|
async def get_model_config(model_id: str):
|
||||||
|
"""Get detailed configuration for a model."""
|
||||||
|
if model_id != "Qwen/Qwen3-TTS-12Hz-1.7B-Base":
|
||||||
|
raise HTTPException(status_code=404, detail="Model not found")
|
||||||
|
|
||||||
|
return ModelConfigResponse(
|
||||||
|
id=model_id,
|
||||||
|
size="1.7B",
|
||||||
|
tokenizer_type="Qwen3TTSTokenizer",
|
||||||
|
languages=["English", "Russian", "Chinese", "Japanese", "Korean"],
|
||||||
|
max_text_length=10000,
|
||||||
|
output_sample_rate=24000,
|
||||||
|
streaming_supported=True,
|
||||||
|
voice_cloning_supported=True,
|
||||||
|
recommended_parameters={
|
||||||
|
"temperature": 0.9,
|
||||||
|
"top_p": 1.0,
|
||||||
|
"top_k": 50,
|
||||||
|
"do_sample": True,
|
||||||
|
"repetition_penalty": 1.05,
|
||||||
|
"emit_every_frames": 8,
|
||||||
|
"decode_window_frames": 80,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/v1/models/{model_id}/languages")
|
||||||
|
async def get_model_languages(model_id: str):
|
||||||
|
"""Get supported languages for a model."""
|
||||||
|
if model_id != "Qwen/Qwen3-TTS-12Hz-1.7B-Base":
|
||||||
|
raise HTTPException(status_code=404, detail="Model not found")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"object": "list",
|
||||||
|
"model": model_id,
|
||||||
|
"languages": [
|
||||||
|
{"code": "en", "name": "English"},
|
||||||
|
{"code": "ru", "name": "Russian"},
|
||||||
|
{"code": "zh", "name": "Chinese"},
|
||||||
|
{"code": "ja", "name": "Japanese"},
|
||||||
|
{"code": "ko", "name": "Korean"},
|
||||||
|
{"code": "auto", "name": "Auto-detect"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ============== Root Endpoint ==============
|
# ============== Root Endpoint ==============
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
@@ -317,14 +849,38 @@ async def root():
|
|||||||
"""Root endpoint with API information."""
|
"""Root endpoint with API information."""
|
||||||
return {
|
return {
|
||||||
"name": "Qwen3-TTS API",
|
"name": "Qwen3-TTS API",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "OpenAI-like API for Qwen3 Text-to-Speech streaming",
|
"description": "OpenAI-like API for Qwen3 Text-to-Speech streaming",
|
||||||
"documentation": "/docs",
|
"documentation": "/docs",
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
"health": "/v1/health",
|
"health": "/v1/health",
|
||||||
"models": "/v1/models",
|
"models": {
|
||||||
"speech": "/v1/audio/speech",
|
"list": "/v1/models",
|
||||||
"stream": "/v1/audio/speech/stream",
|
"config": "/v1/models/{model_id}/config",
|
||||||
|
"languages": "/v1/models/{model_id}/languages",
|
||||||
|
},
|
||||||
|
"speech": {
|
||||||
|
"generate": "/v1/audio/speech",
|
||||||
|
"stream": "/v1/audio/speech/stream",
|
||||||
|
"convert": "/v1/audio/convert",
|
||||||
|
},
|
||||||
|
"voices": {
|
||||||
|
"upload": "POST /v1/voices/upload",
|
||||||
|
"list": "/v1/voices",
|
||||||
|
"delete": "DELETE /v1/voices/{voice_id}",
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"validate": "POST /v1/text/validate",
|
||||||
|
},
|
||||||
|
"batch": {
|
||||||
|
"create": "POST /v1/batch/create",
|
||||||
|
"status": "/v1/batch/{job_id}",
|
||||||
|
"results": "/v1/batch/{job_id}/results",
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"statistics": "/v1/usage",
|
||||||
|
"quota": "/v1/quota",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+152
@@ -84,3 +84,155 @@ class ModelsListResponse(BaseModel):
|
|||||||
"""List of available models."""
|
"""List of available models."""
|
||||||
object: str = Field(default="list", description="Object type")
|
object: str = Field(default="list", description="Object type")
|
||||||
data: List[ModelInfoResponse] = Field(..., description="List of models")
|
data: List[ModelInfoResponse] = Field(..., description="List of models")
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Voice Management ==============
|
||||||
|
|
||||||
|
class VoiceResponse(BaseModel):
|
||||||
|
"""Custom voice information."""
|
||||||
|
id: str = Field(..., description="Voice ID")
|
||||||
|
name: str = Field(..., description="Voice name")
|
||||||
|
language: str = Field(..., description="Voice language")
|
||||||
|
created_at: int = Field(..., description="Creation timestamp")
|
||||||
|
object: str = Field(default="voice", description="Object type")
|
||||||
|
|
||||||
|
|
||||||
|
class VoicesListResponse(BaseModel):
|
||||||
|
"""List of custom voices."""
|
||||||
|
object: str = Field(default="list", description="Object type")
|
||||||
|
data: List[VoiceResponse] = Field(..., description="List of voices")
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Text Validation ==============
|
||||||
|
|
||||||
|
class TextValidationRequest(BaseModel):
|
||||||
|
"""Request to validate text for TTS."""
|
||||||
|
text: str = Field(..., min_length=1, max_length=10000, description="Text to validate")
|
||||||
|
language: str = Field(default="English", description="Language for validation")
|
||||||
|
|
||||||
|
|
||||||
|
class TextValidationResponse(BaseModel):
|
||||||
|
"""Text validation response."""
|
||||||
|
valid: bool = Field(..., description="Whether text is valid for TTS")
|
||||||
|
language: str = Field(..., description="Detected language")
|
||||||
|
character_count: int = Field(..., description="Number of characters")
|
||||||
|
estimated_duration: float = Field(..., description="Estimated audio duration in seconds")
|
||||||
|
warnings: List[str] = Field(default_factory=list, description="Any warnings about the text")
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Batch Processing ==============
|
||||||
|
|
||||||
|
class BatchJobStatus(str, Enum):
|
||||||
|
"""Batch job status."""
|
||||||
|
PENDING = "pending"
|
||||||
|
PROCESSING = "processing"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
FAILED = "failed"
|
||||||
|
CANCELLED = "cancelled"
|
||||||
|
|
||||||
|
|
||||||
|
class BatchItem(BaseModel):
|
||||||
|
"""Single item in a batch request."""
|
||||||
|
text: str = Field(..., min_length=1, description="Text to synthesize")
|
||||||
|
language: str = Field(default="English", description="Language")
|
||||||
|
voice_clone_mode: VoiceCloneMode = Field(default=VoiceCloneMode.DISABLED, description="Voice cloning mode")
|
||||||
|
|
||||||
|
|
||||||
|
class CreateBatchRequest(BaseModel):
|
||||||
|
"""Request to create a batch job."""
|
||||||
|
items: List[BatchItem] = Field(..., min_items=1, max_items=100, description="Batch items")
|
||||||
|
model: TTSModel = Field(default=TTSModel.BASE, description="Model to use")
|
||||||
|
|
||||||
|
|
||||||
|
class BatchJobResponse(BaseModel):
|
||||||
|
"""Batch job information."""
|
||||||
|
id: str = Field(..., description="Job ID")
|
||||||
|
object: str = Field(default="batch", description="Object type")
|
||||||
|
status: BatchJobStatus = Field(..., description="Current status")
|
||||||
|
created_at: int = Field(..., description="Creation timestamp")
|
||||||
|
updated_at: int = Field(..., description="Last update timestamp")
|
||||||
|
request_counts: Dict[str, int] = Field(..., description="Counts: total, processing, completed, failed")
|
||||||
|
output_file_id: Optional[str] = Field(default=None, description="Output file ID when completed")
|
||||||
|
|
||||||
|
|
||||||
|
class BatchResultItem(BaseModel):
|
||||||
|
"""Single result item from batch job."""
|
||||||
|
index: int = Field(..., description="Item index")
|
||||||
|
status: str = Field(..., description="Item status: success or error")
|
||||||
|
error: Optional[str] = Field(default=None, description="Error message if failed")
|
||||||
|
audio_base64: Optional[str] = Field(default=None, description="Generated audio in base64")
|
||||||
|
duration: Optional[float] = Field(default=None, description="Audio duration")
|
||||||
|
|
||||||
|
|
||||||
|
class BatchResultsResponse(BaseModel):
|
||||||
|
"""Batch job results."""
|
||||||
|
job_id: str = Field(..., description="Batch job ID")
|
||||||
|
object: str = Field(default="batch.results", description="Object type")
|
||||||
|
status: BatchJobStatus = Field(..., description="Job status")
|
||||||
|
data: List[BatchResultItem] = Field(..., description="Result items")
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Usage & Quota ==============
|
||||||
|
|
||||||
|
class UsageResponse(BaseModel):
|
||||||
|
"""API usage statistics."""
|
||||||
|
object: str = Field(default="usage", description="Object type")
|
||||||
|
requests_made: int = Field(..., description="Total requests made")
|
||||||
|
audio_generated_seconds: float = Field(..., description="Total audio seconds generated")
|
||||||
|
audio_generated_minutes: float = Field(..., description="Total audio minutes generated")
|
||||||
|
requests_by_model: Dict[str, int] = Field(..., description="Requests per model")
|
||||||
|
requests_by_language: Dict[str, int] = Field(..., description="Requests per language")
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaResponse(BaseModel):
|
||||||
|
"""Rate limits and quotas."""
|
||||||
|
object: str = Field(default="quota", description="Object type")
|
||||||
|
requests_per_minute: int = Field(..., description="Rate limit (requests/minute)")
|
||||||
|
max_text_length: int = Field(..., description="Maximum text length")
|
||||||
|
max_batch_size: int = Field(..., description="Maximum batch items")
|
||||||
|
concurrent_requests: int = Field(..., description="Max concurrent requests")
|
||||||
|
remaining_requests: int = Field(..., description="Remaining requests in current window")
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Audio Conversion ==============
|
||||||
|
|
||||||
|
class AudioFormat(str, Enum):
|
||||||
|
"""Supported audio formats."""
|
||||||
|
WAV = "wav"
|
||||||
|
MP3 = "mp3"
|
||||||
|
OGG = "ogg"
|
||||||
|
FLAC = "flac"
|
||||||
|
|
||||||
|
|
||||||
|
class ConvertAudioRequest(BaseModel):
|
||||||
|
"""Request to convert audio format/sample rate."""
|
||||||
|
audio_base64: str = Field(..., description="Audio data in base64")
|
||||||
|
target_format: AudioFormat = Field(default=AudioFormat.WAV, description="Target audio format")
|
||||||
|
target_sample_rate: Optional[int] = Field(default=None, ge=8000, le=48000, description="Target sample rate (Hz)")
|
||||||
|
|
||||||
|
|
||||||
|
class ConvertAudioResponse(BaseModel):
|
||||||
|
"""Audio conversion response."""
|
||||||
|
id: str = Field(..., description="Request ID")
|
||||||
|
object: str = Field(default="audio", description="Object type")
|
||||||
|
created: int = Field(..., description="Timestamp")
|
||||||
|
format: str = Field(..., description="Output format")
|
||||||
|
sample_rate: int = Field(..., description="Output sample rate")
|
||||||
|
audio_base64: str = Field(..., description="Converted audio in base64")
|
||||||
|
duration: float = Field(..., description="Duration in seconds")
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Model Configuration ==============
|
||||||
|
|
||||||
|
class ModelConfigResponse(BaseModel):
|
||||||
|
"""Model configuration details."""
|
||||||
|
id: str = Field(..., description="Model ID")
|
||||||
|
object: str = Field(default="model.config", description="Object type")
|
||||||
|
size: str = Field(..., description="Model size (parameters)")
|
||||||
|
tokenizer_type: str = Field(..., description="Tokenizer type")
|
||||||
|
languages: List[str] = Field(..., description="Supported languages")
|
||||||
|
max_text_length: int = Field(..., description="Maximum input text length")
|
||||||
|
output_sample_rate: int = Field(..., description="Output sample rate (Hz)")
|
||||||
|
streaming_supported: bool = Field(default=True, description="Streaming support")
|
||||||
|
voice_cloning_supported: bool = Field(default=True, description="Voice cloning support")
|
||||||
|
recommended_parameters: Dict[str, Any] = Field(..., description="Recommended generation parameters")
|
||||||
|
|||||||
Reference in New Issue
Block a user