Calvin commited on
Commit
f1a1694
·
1 Parent(s): 5b37c3f

add space.yml

Browse files
Files changed (2) hide show
  1. app.py +53 -99
  2. space.yml +2 -0
app.py CHANGED
@@ -1,105 +1,59 @@
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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
 
 
2
  from gtts import gTTS
 
 
3
  import aiofiles
4
+ import uvicorn
5
+ import os
6
+ import time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
+ app = FastAPI()
9
 
10
+ def generate_timestamps(script: str, wpm: int = 150):
 
11
  """
12
+ Generate naive timestamps per sentence based on words per minute (WPM).
 
13
  """
14
+ sentences = [s.strip() for s in script.replace("\n", " ").split(".") if s.strip()]
15
+ timestamps = []
16
+ current_time = 0.0
17
+ seconds_per_word = 60.0 / wpm
18
+
19
+ for sentence in sentences:
20
+ word_count = len(sentence.split())
21
+ duration = round(word_count * seconds_per_word, 2)
22
+ start_time = round(current_time, 2)
23
+ end_time = round(current_time + duration, 2)
24
+ timestamps.append({
25
+ "sentence": sentence,
26
+ "start": start_time,
27
+ "end": end_time
28
+ })
29
+ current_time += duration
30
+
31
+ return timestamps
32
+
33
+ @app.get("/")
34
+ async def root():
35
+ return {"message": "API is running!"}
36
+
37
+ @app.post("/tts")
38
+ async def text_to_speech(text: str):
39
+ # Save TTS audio
40
+ tts = gTTS(text)
41
+ file_path = "output.mp3"
42
+ tts.save(file_path)
43
+
44
+ # Read audio file
45
+ async with aiofiles.open(file_path, mode="rb") as f:
46
+ audio_data = await f.read()
47
+
48
+ # Generate timestamps
49
+ timestamps = generate_timestamps(text)
50
+
51
+ return {
52
+ "script": text,
53
+ "timestamps": timestamps,
54
+ "file": file_path,
55
+ "size": len(audio_data)
56
+ }
57
+
58
+ if __name__ == "__main__":
59
+ uvicorn.run("app:app", host="0.0.0.0", port=7860)
space.yml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ app_file: app.py
2
+ port: 7860