PlotweaverModel commited on
Commit
46d077f
·
verified ·
1 Parent(s): 75a410e

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +2 -0
  2. app.py +49 -9
  3. 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(audio: UploadFile = File(...), language: str = Form(default="auto")):
 
 
 
 
232
  """
233
- Transcribe audio. Two modes:
234
- - mode=api → POST to OpenAI-compatible /audio/transcriptions
235
- - mode=local use local Whisper model (requires faster-whisper or openai-whisper)
 
236
  """
237
- asr_cfg = CONFIG.get("asr", {})
238
- mode = asr_cfg.get("mode", "api")
 
 
 
 
239
 
 
240
  audio_bytes = await audio.read()
241
 
242
  if mode == "api":
243
- return await _asr_via_api(audio_bytes, audio.filename, language, asr_cfg)
244
  else:
245
- return await _asr_via_local(audio_bytes, audio.filename, language, asr_cfg)
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
  ],