Spaces:
Running
Running
Upload 2 files
Browse files- app.py +61 -1
- index.html +163 -5
app.py
CHANGED
|
@@ -1057,7 +1057,67 @@ def api_gemini_voices():
|
|
| 1057 |
]
|
| 1058 |
return jsonify(ok=True, voices=voices)
|
| 1059 |
|
| 1060 |
-
# โโ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1061 |
@app.route('/api/preview_clip', methods=['POST'])
|
| 1062 |
def api_preview_clip():
|
| 1063 |
try:
|
|
|
|
| 1057 |
]
|
| 1058 |
return jsonify(ok=True, voices=voices)
|
| 1059 |
|
| 1060 |
+
# โโ TTS FREE โโ
|
| 1061 |
+
def _tts_day_key():
|
| 1062 |
+
from datetime import date
|
| 1063 |
+
return 'tts_day_' + date.today().isoformat() # e.g. tts_day_2025-06-15
|
| 1064 |
+
|
| 1065 |
+
@app.route('/api/tts_usage')
|
| 1066 |
+
def api_tts_usage():
|
| 1067 |
+
u = request.args.get('u', '').strip()
|
| 1068 |
+
db = load_db()
|
| 1069 |
+
if u not in db['users']:
|
| 1070 |
+
return jsonify(used=0)
|
| 1071 |
+
used = db['users'][u].get(_tts_day_key(), 0)
|
| 1072 |
+
return jsonify(used=used)
|
| 1073 |
+
|
| 1074 |
+
@app.route('/api/tts_free', methods=['POST'])
|
| 1075 |
+
def api_tts_free():
|
| 1076 |
+
try:
|
| 1077 |
+
u = (request.form.get('u') or '').strip()
|
| 1078 |
+
text = (request.form.get('text') or '').strip()
|
| 1079 |
+
voice = request.form.get('voice', 'my-MM-ThihaNeural')
|
| 1080 |
+
speed = int(request.form.get('speed', 30))
|
| 1081 |
+
|
| 1082 |
+
if not u:
|
| 1083 |
+
return jsonify(ok=False, msg='โ Not logged in')
|
| 1084 |
+
if not text:
|
| 1085 |
+
return jsonify(ok=False, msg='โ No text provided')
|
| 1086 |
+
|
| 1087 |
+
db = load_db()
|
| 1088 |
+
is_adm = (u == ADMIN_U)
|
| 1089 |
+
|
| 1090 |
+
if not is_adm:
|
| 1091 |
+
if u not in db['users']:
|
| 1092 |
+
return jsonify(ok=False, msg='โ User not found')
|
| 1093 |
+
if db['users'][u].get('banned'):
|
| 1094 |
+
return jsonify(ok=False, msg='โ Account banned')
|
| 1095 |
+
day_key = _tts_day_key()
|
| 1096 |
+
used = db['users'][u].get(day_key, 0)
|
| 1097 |
+
if used >= 10:
|
| 1098 |
+
return jsonify(ok=False, msg='โ แแ
แบแแแบ 10 แแผแญแแบ แแแทแบแแแบแแปแแบ แแผแแทแบแแฝแฌแธแแผแฎแ แแแแบแแผแแบ แแผแแบแแฌแแซ')
|
| 1099 |
+
db['users'][u][day_key] = used + 1
|
| 1100 |
+
save_db(db)
|
| 1101 |
+
new_used = used + 1
|
| 1102 |
+
else:
|
| 1103 |
+
new_used = 0
|
| 1104 |
+
|
| 1105 |
+
out = str(OUTPUT_DIR / f'tts_free_{uuid.uuid4().hex[:10]}.mp3')
|
| 1106 |
+
rate = f'+{speed}%' if speed >= 0 else f'{speed}%'
|
| 1107 |
+
|
| 1108 |
+
import asyncio
|
| 1109 |
+
|
| 1110 |
+
async def _do_tts():
|
| 1111 |
+
await edge_tts.Communicate(text, voice, rate=rate).save(out)
|
| 1112 |
+
|
| 1113 |
+
asyncio.run(_do_tts())
|
| 1114 |
+
|
| 1115 |
+
return jsonify(ok=True, url='/outputs/' + Path(out).name, used=new_used)
|
| 1116 |
+
except Exception as e:
|
| 1117 |
+
import traceback; traceback.print_exc()
|
| 1118 |
+
return jsonify(ok=False, msg=str(e))
|
| 1119 |
+
|
| 1120 |
+
|
| 1121 |
@app.route('/api/preview_clip', methods=['POST'])
|
| 1122 |
def api_preview_clip():
|
| 1123 |
try:
|
index.html
CHANGED
|
@@ -695,6 +695,9 @@ body{background:var(--bg);color:var(--text);font-family:var(--F);min-height:100v
|
|
| 695 |
<button class="mode-btn" id="mode-srt" onclick="switchMode('srt')">
|
| 696 |
<i class="fas fa-closed-captioning"></i> TRANSLATE SRT
|
| 697 |
</button>
|
|
|
|
|
|
|
|
|
|
| 698 |
</div>
|
| 699 |
|
| 700 |
<!-- โโ MOVIE RECAP MODE โโ -->
|
|
@@ -1402,6 +1405,82 @@ body{background:var(--bg);color:var(--text);font-family:var(--F);min-height:100v
|
|
| 1402 |
</div><!-- /ab -->
|
| 1403 |
</div><!-- /section-srt -->
|
| 1404 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1405 |
|
| 1406 |
</div><!-- /app-screen -->
|
| 1407 |
|
|
@@ -2432,11 +2511,11 @@ document.addEventListener('keydown',e=>{if(e.key==='Enter'&&document.getElementB
|
|
| 2432 |
|
| 2433 |
/* โโ MODE SWITCH โโ */
|
| 2434 |
function switchMode(m){
|
| 2435 |
-
|
| 2436 |
-
|
| 2437 |
-
|
| 2438 |
-
|
| 2439 |
-
|
| 2440 |
}
|
| 2441 |
|
| 2442 |
/* โโ TRANSLATE SRT โโ */
|
|
@@ -3161,6 +3240,85 @@ function srtReset(){
|
|
| 3161 |
srtResetBtn();
|
| 3162 |
}
|
| 3163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3164 |
|
| 3165 |
</script>
|
| 3166 |
|
|
|
|
| 695 |
<button class="mode-btn" id="mode-srt" onclick="switchMode('srt')">
|
| 696 |
<i class="fas fa-closed-captioning"></i> TRANSLATE SRT
|
| 697 |
</button>
|
| 698 |
+
<button class="mode-btn" id="mode-tts" onclick="switchMode('tts')">
|
| 699 |
+
<i class="fas fa-microphone-alt"></i> TEXT TO SPEECH
|
| 700 |
+
</button>
|
| 701 |
</div>
|
| 702 |
|
| 703 |
<!-- โโ MOVIE RECAP MODE โโ -->
|
|
|
|
| 1405 |
</div><!-- /ab -->
|
| 1406 |
</div><!-- /section-srt -->
|
| 1407 |
|
| 1408 |
+
<!-- โโ TEXT TO SPEECH MODE โโ -->
|
| 1409 |
+
<div id="section-tts" style="display:none">
|
| 1410 |
+
<div class="ab">
|
| 1411 |
+
|
| 1412 |
+
<div class="card">
|
| 1413 |
+
<div class="ch">
|
| 1414 |
+
<div class="ci ci-p"><i class="fas fa-microphone-alt"></i></div>
|
| 1415 |
+
<div class="ct">Text to Speech <span style="font-size:.72rem;color:#10b981;font-weight:800;background:#d1fae5;padding:2px 8px;border-radius:20px;margin-left:6px">FREE</span></div>
|
| 1416 |
+
<span id="tts-usage-badge" style="margin-left:auto;font-size:.75rem;font-weight:700;padding:3px 10px;border-radius:20px;background:#ede9fe;color:#7c3aed">0/10 แแผแญแแบ</span>
|
| 1417 |
+
</div>
|
| 1418 |
+
|
| 1419 |
+
<!-- Voice Select -->
|
| 1420 |
+
<div style="margin-bottom:12px">
|
| 1421 |
+
<label style="font-size:.8rem;font-weight:700;color:var(--text);display:block;margin-bottom:6px"><i class="fas fa-volume-up" style="color:var(--primary);margin-right:4px"></i>แกแแถแแฝแฑแธแแปแแบแแแบ</label>
|
| 1422 |
+
<select id="tts-voice" style="width:100%;padding:9px 12px;border:1.5px solid var(--border);border-radius:8px;font-family:var(--F);font-size:.83rem;background:#fff;color:var(--text)">
|
| 1423 |
+
<option value="my-MM-ThihaNeural">Thiha (Male) ๐ฒ๐ฒ</option>
|
| 1424 |
+
<option value="my-MM-NilarNeural">Nilar (Female) ๐ฒ๐ฒ</option>
|
| 1425 |
+
<option value="en-US-AndrewMultilingualNeural">Andrew Multilingual (Male)</option>
|
| 1426 |
+
<option value="en-US-AvaMultilingualNeural">Ava Multilingual (Female)</option>
|
| 1427 |
+
<option value="en-US-BrianMultilingualNeural">Brian Multilingual (Male)</option>
|
| 1428 |
+
<option value="en-US-EmmaMultilingualNeural">Emma Multilingual (Female)</option>
|
| 1429 |
+
<option value="en-US-JennyMultilingualNeural">Jenny Multilingual (Female)</option>
|
| 1430 |
+
<option value="en-US-RyanMultilingualNeural">Ryan Multilingual (Male)</option>
|
| 1431 |
+
<option value="th-TH-NiwatNeural">Niwat Thai (Male) ๐น๐ญ</option>
|
| 1432 |
+
<option value="th-TH-PremwadeeNeural">Premwadee Thai (Female) ๐น๐ญ</option>
|
| 1433 |
+
<option value="th-TH-AcharaNeural">Achara Thai (Female) ๐น๐ญ</option>
|
| 1434 |
+
</select>
|
| 1435 |
+
</div>
|
| 1436 |
+
|
| 1437 |
+
<!-- Speed -->
|
| 1438 |
+
<div style="margin-bottom:14px;display:flex;align-items:center;gap:10px">
|
| 1439 |
+
<label style="font-size:.8rem;font-weight:700;color:var(--text);white-space:nowrap"><i class="fas fa-tachometer-alt" style="color:var(--primary);margin-right:4px"></i>Speed</label>
|
| 1440 |
+
<input type="range" id="tts-speed" min="-50" max="100" value="30" style="flex:1">
|
| 1441 |
+
<span id="tts-speed-lbl" style="font-size:.8rem;font-weight:700;color:var(--primary);min-width:40px;text-align:right">+30%</span>
|
| 1442 |
+
</div>
|
| 1443 |
+
|
| 1444 |
+
<!-- Text Textarea -->
|
| 1445 |
+
<div style="margin-bottom:10px">
|
| 1446 |
+
<label style="font-size:.8rem;font-weight:700;color:var(--text);display:block;margin-bottom:6px"><i class="fas fa-align-left" style="color:var(--primary);margin-right:4px"></i>แ
แฌแแฌแธแแแทแบแแแบ <span style="color:var(--muted);font-weight:400">(unlimited)</span></label>
|
| 1447 |
+
<textarea id="tts-text" placeholder="แแฎแแฑแแฌแแพแฌ แ
แฌแแฌแธแแแทแบแแซโฆ" style="width:100%;min-height:200px;padding:10px 12px;border:1.5px solid var(--border);border-radius:8px;font-family:var(--F);font-size:.85rem;resize:vertical;box-sizing:border-box;line-height:1.7;color:var(--text);background:#fafafa"></textarea>
|
| 1448 |
+
<div style="font-size:.72rem;color:var(--muted);margin-top:4px;text-align:right"><span id="tts-char-count">0</span> characters</div>
|
| 1449 |
+
</div>
|
| 1450 |
+
|
| 1451 |
+
<!-- TXT File Upload -->
|
| 1452 |
+
<div style="margin-bottom:16px">
|
| 1453 |
+
<label style="font-size:.8rem;font-weight:700;color:var(--text);display:block;margin-bottom:6px"><i class="fas fa-file-alt" style="color:var(--primary);margin-right:4px"></i>.txt แแญแฏแแบแแแบแแแบ <span style="color:var(--muted);font-weight:400">(optional)</span></label>
|
| 1454 |
+
<div class="srt-drop" id="tts-drop-zone" style="padding:14px" onclick="document.getElementById('tts-file-inp').click()">
|
| 1455 |
+
<i class="fas fa-upload" style="font-size:1.2rem;color:var(--primary)"></i>
|
| 1456 |
+
<div class="srt-drop-t" id="tts-file-lbl">Click to upload .txt file</div>
|
| 1457 |
+
<input type="file" id="tts-file-inp" accept=".txt" style="display:none" onchange="ttsLoadFile(this)">
|
| 1458 |
+
</div>
|
| 1459 |
+
</div>
|
| 1460 |
+
|
| 1461 |
+
<!-- Generate Button -->
|
| 1462 |
+
<button id="tts-gen-btn" onclick="ttsGenerate()" style="width:100%;padding:13px;background:linear-gradient(135deg,var(--primary),#7c3aed);color:#fff;border:none;border-radius:10px;font-family:var(--F);font-size:.9rem;font-weight:800;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:.2s">
|
| 1463 |
+
<i class="fas fa-microphone-alt"></i> Generate Audio
|
| 1464 |
+
</button>
|
| 1465 |
+
|
| 1466 |
+
<!-- Progress -->
|
| 1467 |
+
<div id="tts-prog" style="display:none;margin-top:12px">
|
| 1468 |
+
<div class="ph"><span class="ptit">๐๏ธ TTS แแฏแแบแแฑแแแบโฆ</span><span class="ptmr" id="tts-ptmr">โฑ 0s</span></div>
|
| 1469 |
+
</div>
|
| 1470 |
+
|
| 1471 |
+
<!-- Result / Preview -->
|
| 1472 |
+
<div id="tts-result" style="display:none;margin-top:14px;padding:14px;background:#f5f3ff;border-radius:10px;border:1.5px solid #c4b5fd">
|
| 1473 |
+
<div style="font-size:.82rem;font-weight:700;color:#7c3aed;margin-bottom:10px"><i class="fas fa-check-circle" style="margin-right:4px"></i>TTS Preview</div>
|
| 1474 |
+
<audio id="tts-audio" controls style="width:100%;border-radius:8px"></audio>
|
| 1475 |
+
<div style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap">
|
| 1476 |
+
<a id="tts-dl" href="#" download="tts_output.mp3" style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;background:var(--primary);color:#fff;border-radius:8px;font-size:.8rem;font-weight:700;text-decoration:none"><i class="fas fa-download"></i> Download MP3</a>
|
| 1477 |
+
</div>
|
| 1478 |
+
</div>
|
| 1479 |
+
|
| 1480 |
+
</div>
|
| 1481 |
+
</div>
|
| 1482 |
+
</div><!-- /section-tts -->
|
| 1483 |
+
|
| 1484 |
|
| 1485 |
</div><!-- /app-screen -->
|
| 1486 |
|
|
|
|
| 2511 |
|
| 2512 |
/* โโ MODE SWITCH โโ */
|
| 2513 |
function switchMode(m){
|
| 2514 |
+
['recap','srt','tts'].forEach(x=>{
|
| 2515 |
+
document.getElementById('section-'+x).style.display=x===m?'':'none';
|
| 2516 |
+
document.getElementById('mode-'+x).classList.toggle('on',x===m);
|
| 2517 |
+
});
|
| 2518 |
+
if(m==='tts') ttsLoadUsage();
|
| 2519 |
}
|
| 2520 |
|
| 2521 |
/* โโ TRANSLATE SRT โโ */
|
|
|
|
| 3240 |
srtResetBtn();
|
| 3241 |
}
|
| 3242 |
|
| 3243 |
+
/* โโ TEXT TO SPEECH (FREE) โโ */
|
| 3244 |
+
let TTS_TICK=null, TTS_T0=0;
|
| 3245 |
+
|
| 3246 |
+
(function(){
|
| 3247 |
+
const sp=document.getElementById('tts-speed');
|
| 3248 |
+
const tx=document.getElementById('tts-text');
|
| 3249 |
+
if(sp) sp.addEventListener('input',function(){
|
| 3250 |
+
document.getElementById('tts-speed-lbl').textContent=(this.value>=0?'+':'')+this.value+'%';
|
| 3251 |
+
});
|
| 3252 |
+
if(tx) tx.addEventListener('input',function(){
|
| 3253 |
+
document.getElementById('tts-char-count').textContent=this.value.length.toLocaleString();
|
| 3254 |
+
});
|
| 3255 |
+
})();
|
| 3256 |
+
|
| 3257 |
+
function ttsLoadFile(inp){
|
| 3258 |
+
const f=inp.files[0]; if(!f) return;
|
| 3259 |
+
const r=new FileReader();
|
| 3260 |
+
r.onload=e=>{
|
| 3261 |
+
document.getElementById('tts-text').value=e.target.result;
|
| 3262 |
+
document.getElementById('tts-char-count').textContent=e.target.result.length.toLocaleString();
|
| 3263 |
+
document.getElementById('tts-file-lbl').textContent='โ
'+f.name;
|
| 3264 |
+
};
|
| 3265 |
+
r.onerror=()=>toast('โ แแญแฏแแบแแแบแแแแซ');
|
| 3266 |
+
r.readAsText(f,'utf-8');
|
| 3267 |
+
}
|
| 3268 |
+
|
| 3269 |
+
function ttsLoadUsage(){
|
| 3270 |
+
if(!U) return;
|
| 3271 |
+
fetch('/api/tts_usage?u='+encodeURIComponent(U))
|
| 3272 |
+
.then(r=>r.json())
|
| 3273 |
+
.then(d=>{ document.getElementById('tts-usage-badge').textContent=(d.used||0)+'/10 แแผแญแแบ'; })
|
| 3274 |
+
.catch(()=>{});
|
| 3275 |
+
}
|
| 3276 |
+
|
| 3277 |
+
async function ttsGenerate(){
|
| 3278 |
+
if(!U){ toast('โ แแปแฑแธแแฐแธแแผแฏแ แกแแฑแฌแแทแบแแแบแแซ'); return; }
|
| 3279 |
+
const txt=(document.getElementById('tts-text').value||'').trim();
|
| 3280 |
+
if(!txt){ toast('โ แ
แฌแแฌแธแแแทแบแแซ'); return; }
|
| 3281 |
+
const voice=document.getElementById('tts-voice').value;
|
| 3282 |
+
const speed=document.getElementById('tts-speed').value;
|
| 3283 |
+
const btn=document.getElementById('tts-gen-btn');
|
| 3284 |
+
|
| 3285 |
+
btn.disabled=true;
|
| 3286 |
+
btn.innerHTML='<i class="fas fa-spinner fa-spin"></i> Generatingโฆ';
|
| 3287 |
+
document.getElementById('tts-prog').style.display='';
|
| 3288 |
+
document.getElementById('tts-result').style.display='none';
|
| 3289 |
+
TTS_T0=Date.now();
|
| 3290 |
+
TTS_TICK=setInterval(()=>{
|
| 3291 |
+
document.getElementById('tts-ptmr').textContent='โฑ '+Math.floor((Date.now()-TTS_T0)/1000)+'s';
|
| 3292 |
+
},1000);
|
| 3293 |
+
|
| 3294 |
+
try{
|
| 3295 |
+
const fd=new FormData();
|
| 3296 |
+
fd.append('u',U);
|
| 3297 |
+
fd.append('text',txt);
|
| 3298 |
+
fd.append('voice',voice);
|
| 3299 |
+
fd.append('speed',speed);
|
| 3300 |
+
const res=await fetch('/api/tts_free',{method:'POST',body:fd});
|
| 3301 |
+
const d=await res.json();
|
| 3302 |
+
clearInterval(TTS_TICK);
|
| 3303 |
+
document.getElementById('tts-prog').style.display='none';
|
| 3304 |
+
btn.disabled=false;
|
| 3305 |
+
btn.innerHTML='<i class="fas fa-microphone-alt"></i> Generate Audio';
|
| 3306 |
+
if(!d.ok){ toast('โ '+d.msg); return; }
|
| 3307 |
+
const au=document.getElementById('tts-audio');
|
| 3308 |
+
au.src=d.url+'?t='+Date.now();
|
| 3309 |
+
document.getElementById('tts-dl').href=d.url;
|
| 3310 |
+
document.getElementById('tts-result').style.display='';
|
| 3311 |
+
document.getElementById('tts-usage-badge').textContent=d.used+'/10 แแผแญแแบ';
|
| 3312 |
+
toast('โ
TTS แแผแฎแธแแผแฎ!');
|
| 3313 |
+
} catch(e){
|
| 3314 |
+
clearInterval(TTS_TICK);
|
| 3315 |
+
document.getElementById('tts-prog').style.display='none';
|
| 3316 |
+
btn.disabled=false;
|
| 3317 |
+
btn.innerHTML='<i class="fas fa-microphone-alt"></i> Generate Audio';
|
| 3318 |
+
toast('โ Error: '+e.message);
|
| 3319 |
+
}
|
| 3320 |
+
}
|
| 3321 |
+
|
| 3322 |
|
| 3323 |
</script>
|
| 3324 |
|