Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| FastAPI Server untuk TTS Bahasa Indonesia | |
| Berdasarkan tts_standalone.py | |
| API ini menyediakan endpoint untuk menghasilkan audio dari teks Bahasa Indonesia | |
| dengan menggunakan model TTS yang sudah dilatih khusus untuk bahasa Indonesia. | |
| Fitur: | |
| - Limit maksimal 10 file di folder outputs | |
| - Auto cleanup file lama ketika melebihi limit | |
| - Kompatibel dengan HuggingFace Spaces | |
| Endpoints: | |
| - POST /api/tts: Generate audio dari teks | |
| - GET /api/health: Health check endpoint | |
| - GET /api/speakers: Daftar speaker tersedia | |
| - GET /api/download/{file_path}: Download file audio | |
| """ | |
| from fastapi import FastAPI, HTTPException, File, UploadFile | |
| from fastapi.responses import FileResponse, JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel, Field | |
| from typing import Optional | |
| import uuid | |
| from pathlib import Path | |
| import os | |
| from datetime import datetime | |
| from tts_standalone import generate_tts, params | |
| app = FastAPI( | |
| title="TTS Bahasa Indonesia API", | |
| description="API untuk Text-to-Speech Bahasa Indonesia", | |
| version="1.0.0" | |
| ) | |
| # Enable CORS | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Direktori untuk menyimpan file audio hasil TTS | |
| OUTPUT_DIR = Path("outputs") | |
| OUTPUT_DIR.mkdir(parents=True, exist_ok=True) | |
| # Limit maksimal file yang disimpan | |
| MAX_FILES = 10 | |
| # Pydantic models untuk request/response | |
| class TTSRequest(BaseModel): | |
| text: str = Field(..., description="Teks yang akan diubah menjadi suara") | |
| speaker: Optional[str] = Field(default=None, description="Nama speaker (default: dari config)") | |
| file_path: Optional[str] = Field(default=None, description="Custom file path (optional)") | |
| class TTSResponse(BaseModel): | |
| success: bool | |
| message: str | |
| file_path: Optional[str] = None | |
| download_url: Optional[str] = None | |
| text: Optional[str] = None | |
| speaker: Optional[str] = None | |
| class HealthResponse(BaseModel): | |
| status: str | |
| message: str | |
| def cleanup_old_files(max_files: int = MAX_FILES): | |
| """ | |
| Menghapus file lama jika jumlah file melebihi max_files. | |
| File yang paling lama akan dihapus terlebih dahulu. | |
| Args: | |
| max_files: Jumlah maksimal file yang diizinkan | |
| """ | |
| try: | |
| # Get semua file .wav di OUTPUT_DIR | |
| wav_files = list(OUTPUT_DIR.glob("*.wav")) | |
| if len(wav_files) <= max_files: | |
| return | |
| # Sort berdasarkan waktu modifikasi (file lama di depan) | |
| wav_files.sort(key=lambda x: x.stat().st_mtime) | |
| # Hapus file lama sampai jumlah file <= max_files | |
| files_to_delete = wav_files[:len(wav_files) - max_files + 1] | |
| for file_path in files_to_delete: | |
| try: | |
| file_path.unlink() | |
| print(f"Deleted old file: {file_path}") | |
| except Exception as e: | |
| print(f"Error deleting file {file_path}: {e}") | |
| except Exception as e: | |
| print(f"Error in cleanup_old_files: {e}") | |
| async def health_check(): | |
| """ | |
| Health check endpoint | |
| """ | |
| return HealthResponse( | |
| status="healthy", | |
| message="TTS API is running" | |
| ) | |
| async def tts_endpoint(request: TTSRequest): | |
| """ | |
| Endpoint untuk generate TTS audio dari teks | |
| Request Body (JSON): | |
| { | |
| "text": "Teks yang akan diubah menjadi suara", | |
| "speaker": "gadis", // Optional, default: dari config | |
| "file_path": "custom_filename.wav" // Optional | |
| } | |
| """ | |
| try: | |
| # Validasi text | |
| if not request.text or not request.text.strip(): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Parameter 'text' wajib diisi dan tidak boleh kosong" | |
| ) | |
| # Get speaker (gunakan default dari config jika tidak ada) | |
| speaker = request.speaker or params.get("speaker", "gadis") | |
| # Generate file path | |
| if request.file_path: | |
| # Jika user memberikan custom file path | |
| if not request.file_path.endswith(".wav"): | |
| request.file_path += ".wav" | |
| file_path = OUTPUT_DIR / request.file_path | |
| else: | |
| # Generate UUID untuk file name | |
| short_uuid = str(uuid.uuid4())[:8] | |
| file_path = OUTPUT_DIR / f"{speaker}-{short_uuid}.wav" | |
| # Cleanup file lama sebelum generate file baru | |
| cleanup_old_files(MAX_FILES) | |
| # Generate TTS | |
| success = generate_tts(request.text, str(file_path), speaker) | |
| if success: | |
| # Cleanup lagi setelah generate (untuk memastikan) | |
| cleanup_old_files(MAX_FILES) | |
| return TTSResponse( | |
| success=True, | |
| message="Audio berhasil dibuat", | |
| file_path=str(file_path), | |
| download_url=f"/api/download/{file_path}", | |
| text=request.text, | |
| speaker=speaker | |
| ) | |
| else: | |
| raise HTTPException( | |
| status_code=500, | |
| detail="Gagal membuat audio. Periksa log untuk detail." | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Internal server error: {str(e)}" | |
| ) | |
| async def download_file(file_path: str): | |
| """ | |
| Endpoint untuk download file audio | |
| Args: | |
| file_path: Path relatif ke file audio (contoh: outputs/gadis-abc12345.wav) | |
| """ | |
| try: | |
| file_path_obj = Path(file_path) | |
| # Security check: pastikan file ada di dalam OUTPUT_DIR | |
| try: | |
| file_path_obj.resolve().relative_to(OUTPUT_DIR.resolve()) | |
| except ValueError: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Invalid file path" | |
| ) | |
| if not file_path_obj.exists(): | |
| raise HTTPException( | |
| status_code=404, | |
| detail="File tidak ditemukan" | |
| ) | |
| return FileResponse( | |
| path=str(file_path_obj), | |
| filename=file_path_obj.name, | |
| media_type="audio/wav" | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Error: {str(e)}" | |
| ) | |
| async def get_speakers(): | |
| """ | |
| Endpoint untuk mendapatkan daftar speaker yang tersedia | |
| """ | |
| speakers = [ | |
| {"id": "wibowo", "name": "Wibowo - Suara jantan berwibawa"}, | |
| {"id": "ardi", "name": "Ardi - Suara lembut dan hangat"}, | |
| {"id": "gadis", "name": "Gadis - Suara perempuan yang merdu"}, | |
| {"id": "JV-00264", "name": "Juminten - Suara perempuan jawa (bahasa jawa)"}, | |
| {"id": "SU-00060", "name": "Asep - Suara lelaki sunda (bahasa sunda)"} | |
| ] | |
| return { | |
| "success": True, | |
| "speakers": speakers, | |
| "default": params.get("speaker", "gadis") | |
| } | |
| async def root(): | |
| """ | |
| Root endpoint dengan informasi API | |
| """ | |
| return { | |
| "message": "TTS Bahasa Indonesia API", | |
| "version": "1.0.0", | |
| "docs": "/docs", | |
| "endpoints": { | |
| "POST /api/tts": "Generate audio dari teks", | |
| "GET /api/health": "Health check", | |
| "GET /api/speakers": "Daftar speaker tersedia", | |
| "GET /api/download/{file_path}": "Download file audio" | |
| } | |
| } | |
| if __name__ == "__main__": | |
| import uvicorn | |
| # Get port from environment variable (untuk HuggingFace Spaces) | |
| port = int(os.environ.get("PORT", 7860)) | |
| print("=" * 60) | |
| print("TTS API Server - Bahasa Indonesia (FastAPI)") | |
| print("=" * 60) | |
| print("API Endpoints:") | |
| print(" POST /api/tts - Generate audio dari teks") | |
| print(" GET /api/health - Health check") | |
| print(" GET /api/speakers - Daftar speaker tersedia") | |
| print(" GET /api/download/{file_path} - Download file audio") | |
| print(" GET /docs - Swagger UI documentation") | |
| print("=" * 60) | |
| print(f"\nServer starting on http://0.0.0.0:{port}") | |
| print(f"Max files in outputs folder: {MAX_FILES}") | |
| print("Press CTRL+C to stop\n") | |
| uvicorn.run(app, host="0.0.0.0", port=port) | |