Calvin commited on
Commit
c38128a
·
1 Parent(s): fd69c67
Files changed (2) hide show
  1. app.py +105 -0
  2. requirements.txt +5 -0
app.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, HTTPException
2
+ from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse
3
+ from pydantic import BaseModel
4
+ from gtts import gTTS
5
+ import tempfile
6
+ import os
7
+ import aiofiles
8
+ import asyncio
9
+
10
+ app = FastAPI(title="Simple Indonesian TTS Space (gTTS)")
11
+
12
+ # Pydantic model for request body
13
+ class TTSRequest(BaseModel):
14
+ text: str
15
+ lang: str = "id"
16
+ slow: bool = False
17
+
18
+ @app.get("/", response_class=HTMLResponse)
19
+ async def homepage():
20
+ html = """
21
+ <html>
22
+ <head><title>Simple TTS Space</title></head>
23
+ <body>
24
+ <h2>Simple Indonesian TTS (gTTS)</h2>
25
+ <p>POST JSON to <code>/api/tts</code> with <code>{ "text": "...", "lang":"id", "slow": false }</code></p>
26
+ <form id="form">
27
+ <textarea id="txt" rows="6" cols="80" placeholder="Masukkan teks bahasa Indonesia..."></textarea><br/>
28
+ <button type="button" onclick="doTTS()">Generate</button>
29
+ </form>
30
+ <audio id="player" controls></audio>
31
+ <script>
32
+ async function doTTS(){
33
+ const text = document.getElementById('txt').value;
34
+ if(!text) return alert('Teks kosong');
35
+ const res = await fetch('/api/tts', {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify({ text, lang: 'id', slow: false })
39
+ });
40
+ if(!res.ok){ alert('Error: ' + res.status); return; }
41
+ const blob = await res.blob();
42
+ const url = URL.createObjectURL(blob);
43
+ document.getElementById('player').src = url;
44
+ }
45
+ </script>
46
+ </body>
47
+ </html>
48
+ """
49
+ return HTMLResponse(content=html, status_code=200)
50
+
51
+ @app.post("/api/tts")
52
+ async def api_tts(req: TTSRequest):
53
+ text = req.text.strip()
54
+ if not text:
55
+ raise HTTPException(status_code=400, detail="text is required")
56
+
57
+ # Limit length to prevent abuse (change as needed)
58
+ if len(text) > 20000:
59
+ raise HTTPException(status_code=400, detail="text too long (max 20000 chars)")
60
+
61
+ # Use a temporary file to save mp3 and stream it back
62
+ try:
63
+ tmp = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
64
+ tmp_name = tmp.name
65
+ tmp.close()
66
+
67
+ # gTTS synthesize (synchronous) - small text ok
68
+ tts = gTTS(text=text, lang=req.lang, slow=req.slow)
69
+ tts.save(tmp_name)
70
+
71
+ # Stream file back as response
72
+ async def iterfile(path):
73
+ # read in chunks
74
+ async with aiofiles.open(path, 'rb') as f:
75
+ chunk = await f.read(8192)
76
+ while chunk:
77
+ yield chunk
78
+ chunk = await f.read(8192)
79
+ # cleanup file after streaming
80
+ try:
81
+ os.remove(path)
82
+ except Exception:
83
+ pass
84
+
85
+ return StreamingResponse(iterfile(tmp_name), media_type="audio/mpeg")
86
+ except Exception as e:
87
+ # ensure cleanup on error
88
+ try:
89
+ if 'tmp_name' in locals() and os.path.exists(tmp_name):
90
+ os.remove(tmp_name)
91
+ except Exception:
92
+ pass
93
+ raise HTTPException(status_code=500, detail=str(e))
94
+
95
+
96
+ @app.post("/api/tts/url")
97
+ async def api_tts_url(req: TTSRequest):
98
+ """
99
+ Alternate endpoint: returns a small JSON with a temporary url (if you prefer).
100
+ This implementation still returns inline audio bytes to keep it simple.
101
+ """
102
+ res = await api_tts(req)
103
+ # We can't easily return a hosted URL without external storage.
104
+ # So return error telling user to use /api/tts which streams audio.
105
+ return JSONResponse({"error": "This Space does not host persistent URLs. Use /api/tts to stream audio."}, status_code=400)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ gTTS
4
+ aiofiles
5
+ pydantic