Spaces:
Running
Running
muralipala1504 commited on
Commit ·
b10a301
1
Parent(s): d61ee9c
feat: Piper Hindi TTS + backend translate/tts endpoints, drop direct LibreTranslate from frontend
Browse files- app.js +15 -13
- deepshell-backend/deepshell/__main__.py +82 -0
app.js
CHANGED
|
@@ -95,7 +95,7 @@ window.speakText = function(text) {
|
|
| 95 |
.replace(/🎯|💻|🔍|⚠️|🚀|💡/g, "")
|
| 96 |
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
| 97 |
.trim();
|
| 98 |
-
//
|
| 99 |
if (window.ttsLang !== 'en-US' && window.ttsLang !== 'en-GB') {
|
| 100 |
const langCode = window.ttsLang.split('-')[0];
|
| 101 |
const sentences = clean.match(/[^.!?]+[.!?]+/g) || [clean];
|
|
@@ -103,24 +103,26 @@ window.speakText = function(text) {
|
|
| 103 |
function translateAndSpeakNext() {
|
| 104 |
if (i >= sentences.length || !window.ttsEnabled) return;
|
| 105 |
const sentence = sentences[i].trim();
|
| 106 |
-
fetch('
|
| 107 |
method: 'POST',
|
| 108 |
headers: {'Content-Type': 'application/json'},
|
| 109 |
-
body: JSON.stringify({
|
| 110 |
})
|
| 111 |
.then(r => r.json())
|
| 112 |
.then(data => {
|
| 113 |
const translated = data.translatedText || sentence;
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
| 124 |
})
|
| 125 |
.catch(() => {
|
| 126 |
const utt = new SpeechSynthesisUtterance(sentence);
|
|
|
|
| 95 |
.replace(/🎯|💻|🔍|⚠️|🚀|💡/g, "")
|
| 96 |
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
| 97 |
.trim();
|
| 98 |
+
// Non-English: translate via backend then speak via Piper TTS
|
| 99 |
if (window.ttsLang !== 'en-US' && window.ttsLang !== 'en-GB') {
|
| 100 |
const langCode = window.ttsLang.split('-')[0];
|
| 101 |
const sentences = clean.match(/[^.!?]+[.!?]+/g) || [clean];
|
|
|
|
| 103 |
function translateAndSpeakNext() {
|
| 104 |
if (i >= sentences.length || !window.ttsEnabled) return;
|
| 105 |
const sentence = sentences[i].trim();
|
| 106 |
+
fetch('/translate', {
|
| 107 |
method: 'POST',
|
| 108 |
headers: {'Content-Type': 'application/json'},
|
| 109 |
+
body: JSON.stringify({text: sentence, target: langCode})
|
| 110 |
})
|
| 111 |
.then(r => r.json())
|
| 112 |
.then(data => {
|
| 113 |
const translated = data.translatedText || sentence;
|
| 114 |
+
return fetch('/tts', {
|
| 115 |
+
method: 'POST',
|
| 116 |
+
headers: {'Content-Type': 'application/json'},
|
| 117 |
+
body: JSON.stringify({text: translated, lang: langCode})
|
| 118 |
+
});
|
| 119 |
+
})
|
| 120 |
+
.then(r => r.blob())
|
| 121 |
+
.then(blob => {
|
| 122 |
+
const url = URL.createObjectURL(blob);
|
| 123 |
+
const audio = new Audio(url);
|
| 124 |
+
audio.onended = () => { URL.revokeObjectURL(url); i++; translateAndSpeakNext(); };
|
| 125 |
+
audio.play();
|
| 126 |
})
|
| 127 |
.catch(() => {
|
| 128 |
const utt = new SpeechSynthesisUtterance(sentence);
|
deepshell-backend/deepshell/__main__.py
CHANGED
|
@@ -371,6 +371,88 @@ async def run_agent_stream(req: ChatRequest, request: Request):
|
|
| 371 |
}
|
| 372 |
)
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
# ============================================
|
| 375 |
# COMMAND EXECUTION - DISABLED FOR FREE VERSION
|
| 376 |
# ============================================
|
|
|
|
| 371 |
}
|
| 372 |
)
|
| 373 |
|
| 374 |
+
# ============================================
|
| 375 |
+
# Translation + TTS Endpoints
|
| 376 |
+
# ============================================
|
| 377 |
+
|
| 378 |
+
import subprocess
|
| 379 |
+
import httpx
|
| 380 |
+
|
| 381 |
+
class TranslateRequest(BaseModel):
|
| 382 |
+
text: str
|
| 383 |
+
target: str = "hi"
|
| 384 |
+
|
| 385 |
+
class TTSRequest(BaseModel):
|
| 386 |
+
text: str
|
| 387 |
+
lang: str = "hi"
|
| 388 |
+
|
| 389 |
+
LIBRETRANSLATE_URL = os.getenv("LIBRETRANSLATE_URL", "http://localhost:5000/translate")
|
| 390 |
+
PIPER_BINARY = os.getenv("PIPER_BINARY", str(Path.home() / "piper/piper"))
|
| 391 |
+
PIPER_VOICE_DIR = os.getenv("PIPER_VOICE_DIR", str(Path.home() / "piper/voices"))
|
| 392 |
+
|
| 393 |
+
VOICE_MAP = {
|
| 394 |
+
"hi": "hi_IN-rohan-medium.onnx",
|
| 395 |
+
"ta": "ta_IN-cmu_indic-medium.onnx",
|
| 396 |
+
"te": "te_IN-cmu_indic-medium.onnx",
|
| 397 |
+
"ar": "ar_JO-kareem-medium.onnx",
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
@app.post("/translate")
|
| 401 |
+
async def translate_text(req: TranslateRequest):
|
| 402 |
+
"""Translate text via LibreTranslate."""
|
| 403 |
+
try:
|
| 404 |
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
| 405 |
+
resp = await client.post(
|
| 406 |
+
LIBRETRANSLATE_URL,
|
| 407 |
+
json={"q": req.text, "source": "en", "target": req.target}
|
| 408 |
+
)
|
| 409 |
+
data = resp.json()
|
| 410 |
+
return {"translatedText": data.get("translatedText", req.text)}
|
| 411 |
+
except Exception as e:
|
| 412 |
+
return {"translatedText": req.text, "error": str(e)}
|
| 413 |
+
|
| 414 |
+
@app.post("/tts")
|
| 415 |
+
async def text_to_speech(req: TTSRequest):
|
| 416 |
+
"""Convert text to speech via Piper, return WAV audio."""
|
| 417 |
+
voice_file = VOICE_MAP.get(req.lang)
|
| 418 |
+
if not voice_file:
|
| 419 |
+
return JSONResponse({"error": f"No voice for lang: {req.lang}"}, status_code=400)
|
| 420 |
+
|
| 421 |
+
model_path = os.path.join(PIPER_VOICE_DIR, voice_file)
|
| 422 |
+
if not os.path.exists(model_path):
|
| 423 |
+
return JSONResponse({"error": f"Voice model not found: {model_path}"}, status_code=404)
|
| 424 |
+
|
| 425 |
+
if not os.path.exists(PIPER_BINARY):
|
| 426 |
+
return JSONResponse({"error": f"Piper binary not found: {PIPER_BINARY}"}, status_code=404)
|
| 427 |
+
|
| 428 |
+
try:
|
| 429 |
+
import tempfile
|
| 430 |
+
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
|
| 431 |
+
tmp_path = tmp.name
|
| 432 |
+
|
| 433 |
+
proc = await asyncio.create_subprocess_exec(
|
| 434 |
+
PIPER_BINARY,
|
| 435 |
+
"--model", model_path,
|
| 436 |
+
"--output_file", tmp_path,
|
| 437 |
+
stdin=asyncio.subprocess.PIPE,
|
| 438 |
+
stdout=asyncio.subprocess.DEVNULL,
|
| 439 |
+
stderr=asyncio.subprocess.DEVNULL,
|
| 440 |
+
env={**os.environ, "LD_LIBRARY_PATH": str(Path(PIPER_BINARY).parent)}
|
| 441 |
+
)
|
| 442 |
+
await asyncio.wait_for(proc.communicate(input=req.text.encode()), timeout=30)
|
| 443 |
+
with open(tmp_path, "rb") as f:
|
| 444 |
+
wav_data = f.read()
|
| 445 |
+
os.unlink(tmp_path)
|
| 446 |
+
return StreamingResponse(
|
| 447 |
+
iter([wav_data]),
|
| 448 |
+
media_type="audio/wav",
|
| 449 |
+
headers={"Content-Disposition": "inline; filename=tts.wav"}
|
| 450 |
+
)
|
| 451 |
+
except asyncio.TimeoutError:
|
| 452 |
+
return JSONResponse({"error": "TTS generation timed out"}, status_code=504)
|
| 453 |
+
except Exception as e:
|
| 454 |
+
return JSONResponse({"error": str(e)}, status_code=500)
|
| 455 |
+
|
| 456 |
# ============================================
|
| 457 |
# COMMAND EXECUTION - DISABLED FOR FREE VERSION
|
| 458 |
# ============================================
|