Upload 3 files
Browse files- README.md +2 -0
- app.py +49 -9
- config.json +4 -0
README.md
CHANGED
|
@@ -125,6 +125,8 @@ The app auto-detects these DashScope endpoints and uses the native multimodal fo
|
|
| 125 |
|
| 126 |
**Per-service override (only if services differ):** set any of `ASR_BASE_URL` / `LLM_BASE_URL` / `TTS_BASE_URL` and `ASR_API_KEY` / `LLM_API_KEY` / `TTS_API_KEY` to point a single service somewhere else. A per-service value wins over the shared `DASHSCOPE_*` value. Optional model/voice overrides: `ASR_MODEL`, `LLM_MODEL`, `TTS_MODEL`, `TTS_VOICE`.
|
| 127 |
|
|
|
|
|
|
|
| 128 |
**Per-language TTS backend:** each language can use its own TTS engine, so e.g. English speaks via Qwen while Yoruba speaks via a self-hosted model. A language uses its own endpoint when it sets `tts_format` / `tts_base_url` / `tts_api_key` (in `config.json` or via env). Supported `tts_format` values: `dashscope`, `openai`, or `custom`. The `custom` format POSTs `{text, speed}` to the URL **verbatim** (so an API Gateway invoke URL like `.../prod/tts` is used exactly as given), sends both `Authorization: Bearer` and `x-api-key` when a key is set, and accepts either raw audio (`audio/*`) or JSON carrying base64 audio (key `audio` / `audio_base64` / `data` / `wav` / `audio_content`). Per-language env vars follow the pattern `TTS_<LANGID>_BASE_URL`, `TTS_<LANGID>_API_KEY`, `TTS_<LANGID>_FORMAT`, `TTS_<LANGID>_MODEL`, `TTS_<LANGID>_VOICE` — e.g. `TTS_YORUBA_BASE_URL`. A `custom`-format language never inherits the global Qwen URL/key, so a missing value fails safely instead of sending text to the wrong engine.
|
| 129 |
|
| 130 |
Leave the placeholder values in `config.json` as they are — they're scrubbed automatically at load, and the Secrets fill in the real values.
|
|
|
|
| 125 |
|
| 126 |
**Per-service override (only if services differ):** set any of `ASR_BASE_URL` / `LLM_BASE_URL` / `TTS_BASE_URL` and `ASR_API_KEY` / `LLM_API_KEY` / `TTS_API_KEY` to point a single service somewhere else. A per-service value wins over the shared `DASHSCOPE_*` value. Optional model/voice overrides: `ASR_MODEL`, `LLM_MODEL`, `TTS_MODEL`, `TTS_VOICE`.
|
| 127 |
|
| 128 |
+
**Per-language ASR (speech-to-text):** a language can route its voice input to a different ASR engine — useful because Qwen's `qwen3-asr-flash` doesn't cover Yoruba, while Whisper does. Set `asr_base_url` / `asr_api_key` / `asr_model` on the language (or via env `ASR_<LANGID>_BASE_URL` etc.), and it uses that OpenAI-compatible `/audio/transcriptions` endpoint with the language's `asr_lang` hint. Yoruba ships pointed at a Whisper model (`whisper-large-v3`, hint `yo`); set `ASR_YORUBA_BASE_URL` to any OpenAI-compatible Whisper endpoint (e.g. `https://api.groq.com/openai/v1` or `https://api.openai.com/v1`) plus `ASR_YORUBA_API_KEY`. A language with its own `asr_base_url` never inherits the global Qwen ASR endpoint.
|
| 129 |
+
|
| 130 |
**Per-language TTS backend:** each language can use its own TTS engine, so e.g. English speaks via Qwen while Yoruba speaks via a self-hosted model. A language uses its own endpoint when it sets `tts_format` / `tts_base_url` / `tts_api_key` (in `config.json` or via env). Supported `tts_format` values: `dashscope`, `openai`, or `custom`. The `custom` format POSTs `{text, speed}` to the URL **verbatim** (so an API Gateway invoke URL like `.../prod/tts` is used exactly as given), sends both `Authorization: Bearer` and `x-api-key` when a key is set, and accepts either raw audio (`audio/*`) or JSON carrying base64 audio (key `audio` / `audio_base64` / `data` / `wav` / `audio_content`). Per-language env vars follow the pattern `TTS_<LANGID>_BASE_URL`, `TTS_<LANGID>_API_KEY`, `TTS_<LANGID>_FORMAT`, `TTS_<LANGID>_MODEL`, `TTS_<LANGID>_VOICE` — e.g. `TTS_YORUBA_BASE_URL`. A `custom`-format language never inherits the global Qwen URL/key, so a missing value fails safely instead of sending text to the wrong engine.
|
| 131 |
|
| 132 |
Leave the placeholder values in `config.json` as they are — they're scrubbed automatically at load, and the Secrets fill in the real values.
|
app.py
CHANGED
|
@@ -105,6 +105,12 @@ def apply_env_overrides(cfg):
|
|
| 105 |
lang["tts_model"] = _env(f"TTS_{lid}_MODEL")
|
| 106 |
if _env(f"TTS_{lid}_VOICE"):
|
| 107 |
lang["tts_voice"] = _env(f"TTS_{lid}_VOICE")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
return cfg
|
| 110 |
|
|
@@ -193,6 +199,30 @@ def get_lang_config(lang_id: str) -> dict:
|
|
| 193 |
return langs[0] if langs else {}
|
| 194 |
|
| 195 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
def get_tts_config(lang_id: str) -> dict:
|
| 197 |
"""Resolve the TTS endpoint for a language. A language may carry its own
|
| 198 |
tts_base_url / tts_api_key / tts_format / tts_model / tts_voice; anything not
|
|
@@ -228,21 +258,31 @@ _local_whisper = None
|
|
| 228 |
|
| 229 |
|
| 230 |
@app.post("/api/asr")
|
| 231 |
-
async def transcribe_audio(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
"""
|
| 233 |
-
Transcribe audio.
|
| 234 |
-
|
| 235 |
-
- mode=
|
|
|
|
| 236 |
"""
|
| 237 |
-
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
|
|
|
|
| 240 |
audio_bytes = await audio.read()
|
| 241 |
|
| 242 |
if mode == "api":
|
| 243 |
-
return await _asr_via_api(audio_bytes, audio.filename,
|
| 244 |
else:
|
| 245 |
-
return await _asr_via_local(audio_bytes, audio.filename,
|
| 246 |
|
| 247 |
|
| 248 |
async def _asr_via_api(audio_bytes: bytes, filename: str, language: str, asr_cfg: dict):
|
|
@@ -1010,7 +1050,7 @@ async function processAudio(blob){
|
|
| 1010 |
// Get asr_lang hint from current language config
|
| 1011 |
const langCfg=languages.find(l=>l.id===currentLang)||{};
|
| 1012 |
const asrLang=langCfg.asr_lang||'auto';
|
| 1013 |
-
const fd=new FormData();fd.append('audio',blob,'rec.webm');fd.append('language',asrLang||'auto');
|
| 1014 |
const r=await fetch('/api/asr',{method:'POST',body:fd});
|
| 1015 |
if(!r.ok){const e=await r.json();throw new Error(e.detail||'ASR error')}
|
| 1016 |
const d=await r.json();
|
|
|
|
| 105 |
lang["tts_model"] = _env(f"TTS_{lid}_MODEL")
|
| 106 |
if _env(f"TTS_{lid}_VOICE"):
|
| 107 |
lang["tts_voice"] = _env(f"TTS_{lid}_VOICE")
|
| 108 |
+
if _env(f"ASR_{lid}_BASE_URL"):
|
| 109 |
+
lang["asr_base_url"] = _env(f"ASR_{lid}_BASE_URL")
|
| 110 |
+
if _env(f"ASR_{lid}_API_KEY"):
|
| 111 |
+
lang["asr_api_key"] = _env(f"ASR_{lid}_API_KEY")
|
| 112 |
+
if _env(f"ASR_{lid}_MODEL"):
|
| 113 |
+
lang["asr_model"] = _env(f"ASR_{lid}_MODEL")
|
| 114 |
|
| 115 |
return cfg
|
| 116 |
|
|
|
|
| 199 |
return langs[0] if langs else {}
|
| 200 |
|
| 201 |
|
| 202 |
+
def get_asr_config(lang_id: str) -> dict:
|
| 203 |
+
"""Resolve the ASR endpoint for a language. A language may carry its own
|
| 204 |
+
asr_base_url / asr_api_key / asr_model (e.g. Yoruba -> a Whisper endpoint).
|
| 205 |
+
If a language sets its own asr_base_url it fully overrides the global block,
|
| 206 |
+
so it never inherits the Qwen URL/key for a different engine."""
|
| 207 |
+
lang = get_lang_config(lang_id)
|
| 208 |
+
g = CONFIG.get("asr", {})
|
| 209 |
+
if lang.get("asr_base_url"):
|
| 210 |
+
base_url = lang.get("asr_base_url")
|
| 211 |
+
api_key = lang.get("asr_api_key") or ""
|
| 212 |
+
model = lang.get("asr_model") or "whisper-large-v3"
|
| 213 |
+
else:
|
| 214 |
+
base_url = g.get("base_url", "")
|
| 215 |
+
api_key = g.get("api_key", "")
|
| 216 |
+
model = g.get("model", "")
|
| 217 |
+
return {
|
| 218 |
+
"mode": lang.get("asr_mode") or g.get("mode", "api"),
|
| 219 |
+
"base_url": base_url or "",
|
| 220 |
+
"api_key": api_key or "",
|
| 221 |
+
"model": model or "",
|
| 222 |
+
"lang_hint": lang.get("asr_lang") or "auto",
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
|
| 226 |
def get_tts_config(lang_id: str) -> dict:
|
| 227 |
"""Resolve the TTS endpoint for a language. A language may carry its own
|
| 228 |
tts_base_url / tts_api_key / tts_format / tts_model / tts_voice; anything not
|
|
|
|
| 258 |
|
| 259 |
|
| 260 |
@app.post("/api/asr")
|
| 261 |
+
async def transcribe_audio(
|
| 262 |
+
audio: UploadFile = File(...),
|
| 263 |
+
language: str = Form(default="auto"),
|
| 264 |
+
lang_id: str = Form(default=""),
|
| 265 |
+
):
|
| 266 |
"""
|
| 267 |
+
Transcribe audio. Resolves a per-language ASR endpoint when lang_id is given
|
| 268 |
+
(e.g. Yoruda -> Whisper), otherwise uses the global ASR config.
|
| 269 |
+
- mode=api → POST to OpenAI-compatible /audio/transcriptions (or DashScope)
|
| 270 |
+
- mode=local → use local Whisper model
|
| 271 |
"""
|
| 272 |
+
if lang_id:
|
| 273 |
+
acfg = get_asr_config(lang_id)
|
| 274 |
+
lang_hint = acfg["lang_hint"]
|
| 275 |
+
else:
|
| 276 |
+
acfg = CONFIG.get("asr", {})
|
| 277 |
+
lang_hint = language
|
| 278 |
|
| 279 |
+
mode = acfg.get("mode", "api")
|
| 280 |
audio_bytes = await audio.read()
|
| 281 |
|
| 282 |
if mode == "api":
|
| 283 |
+
return await _asr_via_api(audio_bytes, audio.filename, lang_hint, acfg)
|
| 284 |
else:
|
| 285 |
+
return await _asr_via_local(audio_bytes, audio.filename, lang_hint, acfg)
|
| 286 |
|
| 287 |
|
| 288 |
async def _asr_via_api(audio_bytes: bytes, filename: str, language: str, asr_cfg: dict):
|
|
|
|
| 1050 |
// Get asr_lang hint from current language config
|
| 1051 |
const langCfg=languages.find(l=>l.id===currentLang)||{};
|
| 1052 |
const asrLang=langCfg.asr_lang||'auto';
|
| 1053 |
+
const fd=new FormData();fd.append('audio',blob,'rec.webm');fd.append('language',asrLang||'auto');fd.append('lang_id',currentLang);
|
| 1054 |
const r=await fetch('/api/asr',{method:'POST',body:fd});
|
| 1055 |
if(!r.ok){const e=await r.json();throw new Error(e.detail||'ASR error')}
|
| 1056 |
const d=await r.json();
|
config.json
CHANGED
|
@@ -11,7 +11,11 @@
|
|
| 11 |
"id": "yoruba",
|
| 12 |
"label": "Yoruba",
|
| 13 |
"asr_lang": "yo",
|
|
|
|
|
|
|
| 14 |
"system_prompt": "You are a helpful AI assistant that converses in Yoruba. Understand the user's Yoruba input, respond in Yoruba naturally. Keep responses concise (2-4 sentences max) since they will be spoken aloud. Be warm and culturally appropriate.",
|
|
|
|
|
|
|
| 15 |
"tts_model": ""
|
| 16 |
}
|
| 17 |
],
|
|
|
|
| 11 |
"id": "yoruba",
|
| 12 |
"label": "Yoruba",
|
| 13 |
"asr_lang": "yo",
|
| 14 |
+
"asr_base_url": "",
|
| 15 |
+
"asr_model": "whisper-large-v3",
|
| 16 |
"system_prompt": "You are a helpful AI assistant that converses in Yoruba. Understand the user's Yoruba input, respond in Yoruba naturally. Keep responses concise (2-4 sentences max) since they will be spoken aloud. Be warm and culturally appropriate.",
|
| 17 |
+
"tts_format": "custom",
|
| 18 |
+
"tts_base_url": "",
|
| 19 |
"tts_model": ""
|
| 20 |
}
|
| 21 |
],
|