Spaces:
Running
on
Zero
Running
on
Zero
Commit
·
2babf34
1
Parent(s):
cb2f8ff
Fix: Disable OpenAPI docs and simplify endpoints to fix Content-Length error
Browse files- api/routes.py +30 -189
api/routes.py
CHANGED
|
@@ -52,41 +52,12 @@ class CustomJSONResponse(Response):
|
|
| 52 |
# =======================================
|
| 53 |
|
| 54 |
app = FastAPI(
|
| 55 |
-
title="Vocal Articulation
|
| 56 |
-
description=""
|
| 57 |
-
## API untuk Penilaian Artikulasi Vokal Indonesia
|
| 58 |
-
|
| 59 |
-
Sistem penilaian berbasis **Whisper ASR** dengan analisis audio komprehensif untuk 5 level artikulasi.
|
| 60 |
-
|
| 61 |
-
### Features
|
| 62 |
-
- **ASR-based Clarity Scoring** menggunakan Whisper model
|
| 63 |
-
- **6 Metrik Komprehensif**: Clarity, Energy, Speech Rate, Pitch Consistency, SNR, Articulation
|
| 64 |
-
- **Multi-level Support**: Level 1-5 (Vokal → Kalimat)
|
| 65 |
-
- **Grading System**: A-E berdasarkan overall score
|
| 66 |
-
|
| 67 |
-
### Documentation
|
| 68 |
-
- **Swagger UI**: `/docs` (interactive API testing)
|
| 69 |
-
- **ReDoc**: `/redoc` (alternative documentation)
|
| 70 |
-
- **OpenAPI JSON**: `/openapi.json`
|
| 71 |
-
|
| 72 |
-
### Endpoints
|
| 73 |
-
- `GET /` - API information
|
| 74 |
-
- `GET /health` - Health check & model status
|
| 75 |
-
- `GET /levels` - List all articulation levels
|
| 76 |
-
- `POST /score` - Score single audio file
|
| 77 |
-
- `POST /batch_score` - Score multiple audio files
|
| 78 |
-
""",
|
| 79 |
version="2.0.0",
|
| 80 |
-
docs_url=
|
| 81 |
-
redoc_url=
|
| 82 |
-
openapi_url=
|
| 83 |
-
contact={
|
| 84 |
-
"name": "Vocal Articulation Assessment Team",
|
| 85 |
-
"url": "https://huggingface.co/spaces/Cyberlace/latihan-artikulasi",
|
| 86 |
-
},
|
| 87 |
-
license_info={
|
| 88 |
-
"name": "MIT License",
|
| 89 |
-
}
|
| 90 |
)
|
| 91 |
|
| 92 |
# CORS middleware
|
|
@@ -99,48 +70,11 @@ app.add_middleware(
|
|
| 99 |
)
|
| 100 |
|
| 101 |
# =======================================
|
| 102 |
-
# PYDANTIC MODELS
|
| 103 |
# =======================================
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
success: bool
|
| 108 |
-
overall_score: float
|
| 109 |
-
grade: str
|
| 110 |
-
|
| 111 |
-
# Component scores
|
| 112 |
-
clarity_score: float
|
| 113 |
-
energy_score: float
|
| 114 |
-
speech_rate_score: float
|
| 115 |
-
pitch_consistency_score: float
|
| 116 |
-
snr_score: float
|
| 117 |
-
articulation_score: float
|
| 118 |
-
|
| 119 |
-
# ASR results
|
| 120 |
-
transcription: str
|
| 121 |
-
target: str
|
| 122 |
-
similarity: float
|
| 123 |
-
wer: float
|
| 124 |
-
|
| 125 |
-
# Feedback
|
| 126 |
-
feedback: str
|
| 127 |
-
suggestions: List[str]
|
| 128 |
-
|
| 129 |
-
# Audio features
|
| 130 |
-
audio_features: dict
|
| 131 |
-
level: int
|
| 132 |
-
|
| 133 |
-
class HealthResponse(BaseModel):
|
| 134 |
-
"""Response untuk health check"""
|
| 135 |
-
status: str
|
| 136 |
-
model_loaded: bool
|
| 137 |
-
device: str
|
| 138 |
-
whisper_model: str
|
| 139 |
-
|
| 140 |
-
class LevelsResponse(BaseModel):
|
| 141 |
-
"""Response untuk supported levels"""
|
| 142 |
-
levels: dict
|
| 143 |
-
total_levels: int
|
| 144 |
|
| 145 |
# =======================================
|
| 146 |
# GLOBAL VARIABLES
|
|
@@ -201,93 +135,31 @@ async def root():
|
|
| 201 |
}
|
| 202 |
)
|
| 203 |
|
| 204 |
-
@app.get("/health",
|
| 205 |
async def health_check():
|
| 206 |
-
"""
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
- `model_loaded`: Whether Whisper model is loaded
|
| 214 |
-
- `device`: CPU or CUDA
|
| 215 |
-
- `whisper_model`: Model name
|
| 216 |
-
"""
|
| 217 |
-
return HealthResponse(
|
| 218 |
-
status="healthy" if scorer is not None else "unhealthy",
|
| 219 |
-
model_loaded=scorer is not None,
|
| 220 |
-
device=scorer.device if scorer else "unknown",
|
| 221 |
-
whisper_model="openai/whisper-small" if scorer else "not loaded"
|
| 222 |
-
)
|
| 223 |
|
| 224 |
-
@app.get("/levels",
|
| 225 |
async def get_levels():
|
| 226 |
-
"""
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
**Levels:**
|
| 232 |
-
- **Level 1**: Vokal Tunggal (A, I, U, E, O)
|
| 233 |
-
- **Level 2**: Konsonan + Vokal (BA, DA, KA, etc.)
|
| 234 |
-
- **Level 3**: Suku Kata Kompleks (BRA, TRI, etc.)
|
| 235 |
-
- **Level 4**: Kata Penuh (RUMAH, STRATEGI, etc.)
|
| 236 |
-
- **Level 5**: Kalimat Lengkap
|
| 237 |
-
|
| 238 |
-
**Returns:**
|
| 239 |
-
- `levels`: Dictionary of all levels with targets
|
| 240 |
-
- `total_levels`: Total number of levels (5)
|
| 241 |
-
"""
|
| 242 |
-
return LevelsResponse(
|
| 243 |
-
levels=ARTICULATION_LEVELS,
|
| 244 |
-
total_levels=len(ARTICULATION_LEVELS)
|
| 245 |
-
)
|
| 246 |
|
| 247 |
@app.post("/score", response_class=CustomJSONResponse, tags=["Scoring"])
|
| 248 |
async def score_audio(
|
| 249 |
-
audio: UploadFile = File(
|
| 250 |
-
target_text: str = Form(
|
| 251 |
-
level: int = Form(1
|
| 252 |
):
|
| 253 |
-
"""
|
| 254 |
-
## Score Audio File
|
| 255 |
-
|
| 256 |
-
Upload audio dan dapatkan penilaian artikulasi vokal komprehensif.
|
| 257 |
-
|
| 258 |
-
**Request:**
|
| 259 |
-
- `audio`: Audio file (format: WAV, MP3, M4A, FLAC, OGG)
|
| 260 |
-
- `target_text`: Text yang seharusnya diucapkan (contoh: "A", "BA", "STRATEGI")
|
| 261 |
-
- `level`: Level artikulasi (1-5)
|
| 262 |
-
|
| 263 |
-
**Response:**
|
| 264 |
-
- `success`: Boolean status
|
| 265 |
-
- `overall_score`: Skor keseluruhan (0-100)
|
| 266 |
-
- `grade`: Grade (A-E)
|
| 267 |
-
- 6 component scores (clarity, energy, speech_rate, pitch_consistency, snr, articulation)
|
| 268 |
-
- `transcription`: Hasil ASR dari audio
|
| 269 |
-
- `target`: Target text (uppercase)
|
| 270 |
-
- `similarity`: Similarity score (0-1)
|
| 271 |
-
- `wer`: Word Error Rate (0-1)
|
| 272 |
-
- `feedback`: Feedback teks
|
| 273 |
-
- `suggestions`: List saran perbaikan
|
| 274 |
-
- `audio_features`: Dictionary fitur audio
|
| 275 |
-
- `level`: Level yang digunakan
|
| 276 |
-
|
| 277 |
-
**Example:**
|
| 278 |
-
```python
|
| 279 |
-
import requests
|
| 280 |
-
|
| 281 |
-
files = {'audio': open('recording.wav', 'rb')}
|
| 282 |
-
data = {'target_text': 'STRATEGI', 'level': 4}
|
| 283 |
-
response = requests.post('http://localhost:8000/score', files=files, data=data)
|
| 284 |
-
result = response.json()
|
| 285 |
-
print(f"Score: {result['overall_score']}, Grade: {result['grade']}")
|
| 286 |
-
```
|
| 287 |
-
|
| 288 |
-
Returns:
|
| 289 |
-
ScoreResponse dengan hasil penilaian lengkap
|
| 290 |
-
"""
|
| 291 |
if scorer is None:
|
| 292 |
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 293 |
|
|
@@ -340,42 +212,11 @@ async def score_audio(
|
|
| 340 |
|
| 341 |
@app.post("/batch_score", tags=["Scoring"])
|
| 342 |
async def batch_score_audio(
|
| 343 |
-
audios: List[UploadFile] = File(
|
| 344 |
-
target_texts: str = Form(
|
| 345 |
-
levels: str = Form("1"
|
| 346 |
):
|
| 347 |
-
"""
|
| 348 |
-
## Batch Score Multiple Audio Files
|
| 349 |
-
|
| 350 |
-
Upload beberapa audio files sekaligus dan dapatkan penilaian untuk masing-masing.
|
| 351 |
-
|
| 352 |
-
**Request:**
|
| 353 |
-
- `audios`: List of audio files
|
| 354 |
-
- `target_texts`: Comma-separated target texts (contoh: "A,I,U,E,O")
|
| 355 |
-
- `levels`: Comma-separated levels (contoh: "1,1,1,2,2") atau single value untuk semua
|
| 356 |
-
|
| 357 |
-
**Response:**
|
| 358 |
-
- `results`: Array of score results (sama seperti /score endpoint)
|
| 359 |
-
- `total`: Total number of processed files
|
| 360 |
-
|
| 361 |
-
**Example:**
|
| 362 |
-
```python
|
| 363 |
-
import requests
|
| 364 |
-
|
| 365 |
-
files = [
|
| 366 |
-
('audios', open('audio1.wav', 'rb')),
|
| 367 |
-
('audios', open('audio2.wav', 'rb')),
|
| 368 |
-
]
|
| 369 |
-
data = {
|
| 370 |
-
'target_texts': 'A,I',
|
| 371 |
-
'levels': '1,1'
|
| 372 |
-
}
|
| 373 |
-
response = requests.post('http://localhost:8000/batch_score', files=files, data=data)
|
| 374 |
-
results = response.json()['results']
|
| 375 |
-
for r in results:
|
| 376 |
-
print(f"{r['filename']}: {r['overall_score']}")
|
| 377 |
-
```
|
| 378 |
-
"""
|
| 379 |
if scorer is None:
|
| 380 |
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 381 |
|
|
|
|
| 52 |
# =======================================
|
| 53 |
|
| 54 |
app = FastAPI(
|
| 55 |
+
title="Vocal Articulation API",
|
| 56 |
+
description="API for Indonesian vocal articulation assessment",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
version="2.0.0",
|
| 58 |
+
docs_url=None, # Disable Swagger UI temporarily
|
| 59 |
+
redoc_url=None, # Disable ReDoc temporarily
|
| 60 |
+
openapi_url=None, # Disable OpenAPI JSON
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
)
|
| 62 |
|
| 63 |
# CORS middleware
|
|
|
|
| 70 |
)
|
| 71 |
|
| 72 |
# =======================================
|
| 73 |
+
# PYDANTIC MODELS (Minimal)
|
| 74 |
# =======================================
|
| 75 |
|
| 76 |
+
# Removed to reduce OpenAPI schema size
|
| 77 |
+
# Models are now returned as plain dicts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
# =======================================
|
| 80 |
# GLOBAL VARIABLES
|
|
|
|
| 135 |
}
|
| 136 |
)
|
| 137 |
|
| 138 |
+
@app.get("/health", tags=["System"])
|
| 139 |
async def health_check():
|
| 140 |
+
"""Health check endpoint"""
|
| 141 |
+
return {
|
| 142 |
+
"status": "healthy" if scorer is not None else "unhealthy",
|
| 143 |
+
"model_loaded": scorer is not None,
|
| 144 |
+
"device": scorer.device if scorer else "unknown",
|
| 145 |
+
"whisper_model": "openai/whisper-small" if scorer else "not loaded"
|
| 146 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
+
@app.get("/levels", tags=["Articulation"])
|
| 149 |
async def get_levels():
|
| 150 |
+
"""Get all articulation levels"""
|
| 151 |
+
return {
|
| 152 |
+
"levels": ARTICULATION_LEVELS,
|
| 153 |
+
"total_levels": len(ARTICULATION_LEVELS)
|
| 154 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
@app.post("/score", response_class=CustomJSONResponse, tags=["Scoring"])
|
| 157 |
async def score_audio(
|
| 158 |
+
audio: UploadFile = File(...),
|
| 159 |
+
target_text: str = Form(...),
|
| 160 |
+
level: int = Form(1)
|
| 161 |
):
|
| 162 |
+
"""Score audio file"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
if scorer is None:
|
| 164 |
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 165 |
|
|
|
|
| 212 |
|
| 213 |
@app.post("/batch_score", tags=["Scoring"])
|
| 214 |
async def batch_score_audio(
|
| 215 |
+
audios: List[UploadFile] = File(...),
|
| 216 |
+
target_texts: str = Form(...),
|
| 217 |
+
levels: str = Form("1")
|
| 218 |
):
|
| 219 |
+
"""Batch score multiple audio files"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
if scorer is None:
|
| 221 |
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 222 |
|