github-actions[bot] commited on
Commit
8fc0cf8
·
1 Parent(s): 710ee6f

Deploy from Achraf-cyber/hackton-locallang@9a6624dfc5e7799c9920a85849625e3bcefd8a98

Browse files
app/deps.py CHANGED
@@ -9,9 +9,10 @@ class Settings(BaseSettings):
9
 
10
  ALLOWED_ORIGINS: list[str] = ["*"]
11
 
12
- # ASR temporaire via l'API d'inference Hugging Face pendant que
13
- # facebook/mms-1b-all finit de telecharger en local (voir asr.py).
14
- ASR_BACKEND: Literal["local", "hf_api"] = "local"
 
15
  HF_TOKEN: str | None = None
16
 
17
 
 
9
 
10
  ALLOWED_ORIGINS: list[str] = ["*"]
11
 
12
+ # Voir app/services/asr.py pour le detail des backends.
13
+ ASR_BACKEND: Literal["local", "hf_api", "omnilingual", "omnilingual_ctc"] = "local"
14
+ TRANSLATION_BACKEND: Literal["nllb", "afrimt5"] = "nllb"
15
+ TTS_BACKEND_DYU: Literal["mms", "omnivoice"] = "mms"
16
  HF_TOKEN: str | None = None
17
 
18
 
app/services/asr.py CHANGED
@@ -1,7 +1,7 @@
1
  """Reconnaissance vocale (speech-to-text) pour le Dioula, le Moore et le francais
2
  via facebook/mms-1b-all.
3
 
4
- Deux backends, choisis par Settings.ASR_BACKEND :
5
  - "local" (defaut) : Wav2Vec2ForCTC + AutoProcessor charges en local.
6
  - "hf_api" : pont temporaire vers l'API d'inference Hugging Face, utile tant
7
  que le modele local (~3.86 Go) n'est pas entierement telecharge.
@@ -10,7 +10,12 @@ Deux backends, choisis par Settings.ASR_BACKEND :
10
  utilise donc openai/whisper-large-v3 a la place, qui NE supporte PAS
11
  officiellement le Dioula ni le Moore (~99 langues entrainees, dyu/mos
12
  absentes) : fiable seulement pour lang="fra", best-effort pour dyu/mos.
13
- Le contrat de transcribe(audio_path, lang) est identique dans les deux cas.
 
 
 
 
 
14
  """
15
 
16
  import logging
@@ -34,6 +39,14 @@ MMS_LANG_CODES = {
34
  "fra": "fra",
35
  }
36
 
 
 
 
 
 
 
 
 
37
  TARGET_SAMPLE_RATE = 16_000
38
  WINDOW_SECONDS = 30
39
  OVERLAP_SECONDS = 2
@@ -50,6 +63,13 @@ class ASR:
50
  self._client = InferenceClient(model=HF_API_MODEL_NAME, token=settings.HF_TOKEN)
51
  return
52
 
 
 
 
 
 
 
 
53
  self.device = "cuda" if torch.cuda.is_available() else "cpu"
54
  self.processor = AutoProcessor.from_pretrained(MODEL_NAME)
55
  self.model = Wav2Vec2ForCTC.from_pretrained(MODEL_NAME).to(self.device)
@@ -101,10 +121,21 @@ class ASR:
101
  output = self._client.automatic_speech_recognition(audio_path)
102
  return output.text.strip()
103
 
 
 
 
 
 
 
 
 
104
  def transcribe(self, audio_path: str, lang: str) -> str:
105
  if self.backend == "hf_api":
106
  return self._transcribe_hf_api(audio_path, lang)
107
 
 
 
 
108
  self._set_lang(lang)
109
  samples = self._load_audio(audio_path)
110
 
 
1
  """Reconnaissance vocale (speech-to-text) pour le Dioula, le Moore et le francais
2
  via facebook/mms-1b-all.
3
 
4
+ Trois backends, choisis par Settings.ASR_BACKEND :
5
  - "local" (defaut) : Wav2Vec2ForCTC + AutoProcessor charges en local.
6
  - "hf_api" : pont temporaire vers l'API d'inference Hugging Face, utile tant
7
  que le modele local (~3.86 Go) n'est pas entierement telecharge.
 
10
  utilise donc openai/whisper-large-v3 a la place, qui NE supporte PAS
11
  officiellement le Dioula ni le Moore (~99 langues entrainees, dyu/mos
12
  absentes) : fiable seulement pour lang="fra", best-effort pour dyu/mos.
13
+ - "omnilingual" : Meta Omnilingual ASR (2025), couvre nativement dyu/mos
14
+ (verifie via lang_ids.py du modele). Necessite le paquet omnilingual-asr
15
+ (fairseq2 + fairseq2n), qui n'a AUCUN wheel Windows -- fonctionne
16
+ uniquement sous Linux/WSL. L'import est fait en lazy pour ne pas casser
17
+ les backends "local"/"hf_api" sur une machine Windows sans ce paquet.
18
+ Le contrat de transcribe(audio_path, lang) est identique dans les trois cas.
19
  """
20
 
21
  import logging
 
39
  "fra": "fra",
40
  }
41
 
42
+ OMNILINGUAL_MODEL_CARD = "omniASR_CTC_300M_v2"
43
+ OMNILINGUAL_CTC_MODEL_CARD = "omniASR_CTC_1B"
44
+ OMNILINGUAL_LANG_CODES = {
45
+ "dyu": "dyu_Latn",
46
+ "mos": "mos_Latn",
47
+ "fra": "fra_Latn",
48
+ }
49
+
50
  TARGET_SAMPLE_RATE = 16_000
51
  WINDOW_SECONDS = 30
52
  OVERLAP_SECONDS = 2
 
63
  self._client = InferenceClient(model=HF_API_MODEL_NAME, token=settings.HF_TOKEN)
64
  return
65
 
66
+ if self.backend in ("omnilingual", "omnilingual_ctc"):
67
+ from omnilingual_asr.models.inference.pipeline import ASRInferencePipeline
68
+
69
+ model_card = OMNILINGUAL_CTC_MODEL_CARD if self.backend == "omnilingual_ctc" else OMNILINGUAL_MODEL_CARD
70
+ self._omni_pipeline = ASRInferencePipeline(model_card=model_card)
71
+ return
72
+
73
  self.device = "cuda" if torch.cuda.is_available() else "cpu"
74
  self.processor = AutoProcessor.from_pretrained(MODEL_NAME)
75
  self.model = Wav2Vec2ForCTC.from_pretrained(MODEL_NAME).to(self.device)
 
121
  output = self._client.automatic_speech_recognition(audio_path)
122
  return output.text.strip()
123
 
124
+ def _transcribe_omnilingual(self, audio_path: str, lang: str) -> str:
125
+ if lang not in OMNILINGUAL_LANG_CODES:
126
+ raise ValueError(f"Langue non supportee: {lang}")
127
+ result = self._omni_pipeline.transcribe(
128
+ [audio_path], lang=[OMNILINGUAL_LANG_CODES[lang]], batch_size=1
129
+ )
130
+ return result[0].strip()
131
+
132
  def transcribe(self, audio_path: str, lang: str) -> str:
133
  if self.backend == "hf_api":
134
  return self._transcribe_hf_api(audio_path, lang)
135
 
136
+ if self.backend in ("omnilingual", "omnilingual_ctc"):
137
+ return self._transcribe_omnilingual(audio_path, lang)
138
+
139
  self._set_lang(lang)
140
  samples = self._load_audio(audio_path)
141
 
app/services/translator.py CHANGED
@@ -1,10 +1,13 @@
1
- """Traduction entre le francais et le Dioula / Moore via facebook/nllb-200-distilled-600M."""
2
-
3
  import re
4
 
5
  import torch
6
  from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
7
 
 
 
 
 
8
  MODEL_NAME = "facebook/nllb-200-distilled-600M"
9
 
10
  NLLB_LANG_CODES = {
@@ -21,9 +24,35 @@ class Translator:
21
 
22
  def __init__(self) -> None:
23
  self.device = "cuda" if torch.cuda.is_available() else "cpu"
24
- self.tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
25
- self.model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME).to(self.device)
26
- self.model.eval()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  @classmethod
29
  def get_instance(cls) -> "Translator":
@@ -36,25 +65,43 @@ class Translator:
36
  return sentences or [text.strip()]
37
 
38
  def _translate_batch(self, sentences: list[str], src: str, tgt: str) -> list[str]:
39
- self.tokenizer.src_lang = NLLB_LANG_CODES[src]
40
- inputs = self.tokenizer(sentences, return_tensors="pt", padding=True).to(self.device)
41
- forced_bos_token_id = self.tokenizer.convert_tokens_to_ids(NLLB_LANG_CODES[tgt])
 
42
  with torch.no_grad():
43
- generated = self.model.generate(
44
  **inputs,
45
  forced_bos_token_id=forced_bos_token_id,
46
  num_beams=4,
47
  max_length=256,
48
  )
49
- return self.tokenizer.batch_decode(generated, skip_special_tokens=True)
50
 
51
  def translate(self, text: str, src: str, tgt: str) -> str:
52
- if src not in NLLB_LANG_CODES or tgt not in NLLB_LANG_CODES:
53
  raise ValueError(f"Langue non supportee: src={src}, tgt={tgt}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  sentences = self._split_sentences(text)
55
- # Un seul appel batch (au lieu d'une boucle par phrase) : sur CPU, le
56
- # cout d'un forward/beam-search batche est tres inferieur a N appels
57
- # sequentiels (amorti sur tout le batch au lieu d'etre paye N fois).
58
  translated = self._translate_batch(sentences, src, tgt)
59
  return " ".join(translated)
60
 
 
1
+ import logging
 
2
  import re
3
 
4
  import torch
5
  from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
6
 
7
+ from app.deps import get_settings
8
+
9
+ logger = logging.getLogger("model-service.translator")
10
+
11
  MODEL_NAME = "facebook/nllb-200-distilled-600M"
12
 
13
  NLLB_LANG_CODES = {
 
24
 
25
  def __init__(self) -> None:
26
  self.device = "cuda" if torch.cuda.is_available() else "cpu"
27
+ settings = get_settings()
28
+ self.backend = settings.TRANSLATION_BACKEND
29
+
30
+ # Lazy init for NLLB
31
+ self.nllb_tokenizer = None
32
+ self.nllb_model = None
33
+
34
+ # Lazy init for AfriMT5
35
+ self.afrimt5_models = {}
36
+ self.afrimt5_tokenizers = {}
37
+
38
+ if self.backend == "nllb":
39
+ self._init_nllb()
40
+
41
+ def _init_nllb(self) -> None:
42
+ if self.nllb_model is None:
43
+ self.nllb_tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
44
+ self.nllb_model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME).to(self.device)
45
+ self.nllb_model.eval()
46
+
47
+ def _get_afrimt5_model(self, lang: str):
48
+ if lang not in self.afrimt5_models:
49
+ # masakhane/afrimt5_fr_bam_news pour dyu/bambara, masakhane/afrimt5_fr_mos_news pour mos
50
+ hf_repo = "masakhane/afrimt5_fr_bam_news" if lang == "dyu" else "masakhane/afrimt5_fr_mos_news"
51
+ self.afrimt5_tokenizers[lang] = AutoTokenizer.from_pretrained(hf_repo)
52
+ model = AutoModelForSeq2SeqLM.from_pretrained(hf_repo).to(self.device)
53
+ model.eval()
54
+ self.afrimt5_models[lang] = model
55
+ return self.afrimt5_models[lang], self.afrimt5_tokenizers[lang]
56
 
57
  @classmethod
58
  def get_instance(cls) -> "Translator":
 
65
  return sentences or [text.strip()]
66
 
67
  def _translate_batch(self, sentences: list[str], src: str, tgt: str) -> list[str]:
68
+ self._init_nllb()
69
+ self.nllb_tokenizer.src_lang = NLLB_LANG_CODES[src]
70
+ inputs = self.nllb_tokenizer(sentences, return_tensors="pt", padding=True).to(self.device)
71
+ forced_bos_token_id = self.nllb_tokenizer.convert_tokens_to_ids(NLLB_LANG_CODES[tgt])
72
  with torch.no_grad():
73
+ generated = self.nllb_model.generate(
74
  **inputs,
75
  forced_bos_token_id=forced_bos_token_id,
76
  num_beams=4,
77
  max_length=256,
78
  )
79
+ return self.nllb_tokenizer.batch_decode(generated, skip_special_tokens=True)
80
 
81
  def translate(self, text: str, src: str, tgt: str) -> str:
82
+ if src not in ["fr", "dyu", "mos"] or tgt not in ["fr", "dyu", "mos"]:
83
  raise ValueError(f"Langue non supportee: src={src}, tgt={tgt}")
84
+
85
+ # Traduction fr -> local avec AfriMT5 (si active et si le modele est dispo)
86
+ if self.backend == "afrimt5" and src == "fr":
87
+ lang = "dyu" if tgt == "dyu" else "mos"
88
+ try:
89
+ model, tokenizer = self._get_afrimt5_model(lang)
90
+ sentences = self._split_sentences(text)
91
+ translated = []
92
+ for sentence in sentences:
93
+ inputs = tokenizer(sentence, return_tensors="pt").to(self.device)
94
+ with torch.no_grad():
95
+ generated = model.generate(**inputs, max_length=256)
96
+ decoded = tokenizer.decode(generated[0], skip_special_tokens=True)
97
+ translated.append(decoded.strip())
98
+ return " ".join(translated)
99
+ except Exception as e:
100
+ logger.warning("AfriMT5 non disponible pour %s, fallback sur NLLB: %s", lang, e)
101
+ # Fallback sur NLLB
102
+
103
+ # Traduction local -> fr (ou si afrimt5 non dispo/erreur) : toujours NLLB
104
  sentences = self._split_sentences(text)
 
 
 
105
  translated = self._translate_batch(sentences, src, tgt)
106
  return " ".join(translated)
107
 
app/services/tts.py CHANGED
@@ -1,6 +1,7 @@
1
  """Synthese vocale (text-to-speech) pour le Dioula et le Moore via les modeles
2
  VITS facebook/mms-tts-dyu et facebook/mms-tts-mos."""
3
 
 
4
  import re
5
 
6
  import numpy as np
@@ -8,6 +9,10 @@ import soundfile as sf
8
  import torch
9
  from transformers import VitsModel, VitsTokenizer
10
 
 
 
 
 
11
  MMS_TTS_MODEL_NAMES = {
12
  "dyu": "facebook/mms-tts-dyu",
13
  "mos": "facebook/mms-tts-mos",
@@ -21,13 +26,6 @@ _SENTENCE_SPLIT_RE = re.compile(r"(?<=[.!?])\s+")
21
  _LETTERS_RE = re.compile(r"[^a-zA-ZÀ-ÖØ-öø-ÿ]")
22
  _SENTENCE_END_RE = re.compile(r"[.!?]+")
23
 
24
- # EXPERIMENTAL : NLLB laisse a raison les noms propres francais/anglais tels
25
- # quels (ex. "Jean Dupont", "Tetouan") -- mais le tokenizer VITS de
26
- # mms-tts-{dyu,mos} ne connait que l'alphabet phonetique de sa langue, et
27
- # SUPPRIME SILENCIEUSEMENT toute lettre absente de son vocabulaire (verifie
28
- # par inspection directe : "Achraf" -> "araf" en moore, "c" et "h" n'existant
29
- # pas dans le vocabulaire mos). On remplace donc chaque lettre absente par
30
- # l'approximation phonetique la plus proche plutot que de la perdre.
31
  _ACCENT_TRANSLATION = str.maketrans(
32
  {
33
  "é": "e", "è": "e", "ê": "e", "ë": "e",
@@ -51,6 +49,7 @@ class TTS:
51
  self._models: dict[str, VitsModel] = {}
52
  self._tokenizers: dict[str, VitsTokenizer] = {}
53
  self._allowed_chars: dict[str, set[str]] = {}
 
54
 
55
  @classmethod
56
  def get_instance(cls) -> "TTS":
@@ -174,7 +173,38 @@ class TTS:
174
  output = model(**inputs).waveform
175
  return output.squeeze().cpu().numpy()
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  def speak(self, text: str, lang: str, output_path: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  model, _ = self._get_model(lang)
179
  sample_rate = model.config.sampling_rate
180
 
 
1
  """Synthese vocale (text-to-speech) pour le Dioula et le Moore via les modeles
2
  VITS facebook/mms-tts-dyu et facebook/mms-tts-mos."""
3
 
4
+ import logging
5
  import re
6
 
7
  import numpy as np
 
9
  import torch
10
  from transformers import VitsModel, VitsTokenizer
11
 
12
+ from app.deps import get_settings
13
+
14
+ logger = logging.getLogger("model-service.tts")
15
+
16
  MMS_TTS_MODEL_NAMES = {
17
  "dyu": "facebook/mms-tts-dyu",
18
  "mos": "facebook/mms-tts-mos",
 
26
  _LETTERS_RE = re.compile(r"[^a-zA-ZÀ-ÖØ-öø-ÿ]")
27
  _SENTENCE_END_RE = re.compile(r"[.!?]+")
28
 
 
 
 
 
 
 
 
29
  _ACCENT_TRANSLATION = str.maketrans(
30
  {
31
  "é": "e", "è": "e", "ê": "e", "ë": "e",
 
49
  self._models: dict[str, VitsModel] = {}
50
  self._tokenizers: dict[str, VitsTokenizer] = {}
51
  self._allowed_chars: dict[str, set[str]] = {}
52
+ self._omnivoice_model = None
53
 
54
  @classmethod
55
  def get_instance(cls) -> "TTS":
 
173
  output = model(**inputs).waveform
174
  return output.squeeze().cpu().numpy()
175
 
176
+ def _get_omnivoice_model(self):
177
+ if self._omnivoice_model is None:
178
+ from omnivoice import OmniVoice
179
+ # on utilise cuda si disponible, sinon cpu
180
+ device = "cuda:0" if torch.cuda.is_available() else "cpu"
181
+ # CPU est plus stable avec float32 pour l'inference
182
+ dtype = torch.float32 if device == "cpu" else torch.float16
183
+ self._omnivoice_model = OmniVoice.from_pretrained(
184
+ "k2-fsa/OmniVoice",
185
+ device_map=device,
186
+ dtype=dtype
187
+ )
188
+ return self._omnivoice_model
189
+
190
  def speak(self, text: str, lang: str, output_path: str) -> str:
191
+ settings = get_settings()
192
+ if lang == "dyu" and settings.TTS_BACKEND_DYU == "omnivoice":
193
+ try:
194
+ model = self._get_omnivoice_model()
195
+ # Synthesiser l'audio avec Voice Design
196
+ audio = model.generate(
197
+ text=text,
198
+ instruct="female, young adult, clear speech, neutral accent"
199
+ )
200
+ # OmniVoice retourne du 24 kHz
201
+ sf.write(output_path, audio[0], 24000)
202
+ return output_path
203
+ except Exception as e:
204
+ logger.warning("OmniVoice non disponible pour dyu, fallback sur MMS-TTS: %s", e)
205
+ # Fallback sur MMS-TTS
206
+
207
+ # TTS MMS
208
  model, _ = self._get_model(lang)
209
  sample_rate = model.config.sampling_rate
210
 
download_models_wsl.sh ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Run from WSL (Ubuntu) to download NLLB + TTS-dyu + TTS-mos into the WSL
3
+ # environment's own HF cache (separate from Windows' cache). The Omnilingual
4
+ # ASR model is already cached from earlier setup; pre_download.py will skip
5
+ # it if already present via HF's own resume/cache-check logic.
6
+ #
7
+ # Usage (from Windows, via PowerShell or Git Bash):
8
+ # wsl -d Ubuntu -- bash /mnt/c/Users/User/coding/hackaton/locallang/model-service/download_models_wsl.sh
9
+ set -euo pipefail
10
+
11
+ cd /mnt/c/Users/User/coding/hackaton/locallang
12
+ /root/asr-bench/.venv/bin/python pre_download.py
requirements-omnilingual.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ASR_BACKEND=omnilingual : Meta Omnilingual ASR (2025), couvre nativement
2
+ # dyu_Latn/mos_Latn/fra_Latn -- LINUX UNIQUEMENT (fairseq2n n'a aucun wheel
3
+ # Windows, et exige une version de torch EXACTEMENT alignee avec un wheel
4
+ # fairseq2 prebuild : 2.9.1 au moment de la redaction, pas la derniere).
5
+ #
6
+ # Installation (venv Linux/WSL/Docker deja actif) :
7
+ #
8
+ # pip install torch==2.9.1 torchaudio==2.9.1 --index-url https://download.pytorch.org/whl/cpu
9
+ # pip install "fairseq2" --extra-index-url https://fair.pkg.atmeta.com/fairseq2/whl/pt2.9.1/cpu \
10
+ # --trusted-host fair.pkg.atmeta.com # certificat de ce domaine expire au moment de la redaction
11
+ # pip install omnilingual-asr --no-deps # --no-deps : evite de re-resoudre torch en variante CUDA
12
+ # pip install retrying xxhash # dependances transitives manquantes du package
13
+ #
14
+ # Si un torch plus recent que 2.9.1 est deja installe (ex. le venv Windows
15
+ # partage), le desinstaller/repointer d'abord : fairseq2 plante sinon
16
+ # (incompatibilite ABI C++, cf. avertissement officiel de fairseq2).
17
+ #
18
+ # Verifie le 2026-07-04 : fairseq2==0.8.1 / fairseq2n==0.8.1+cpu sont les
19
+ # versions resolues a cet index pour torch 2.9.1.
requirements.txt CHANGED
@@ -15,3 +15,7 @@ pillow
15
  requests
16
  pytest
17
  httpx
 
 
 
 
 
15
  requests
16
  pytest
17
  httpx
18
+
19
+ # ASR_BACKEND=omnilingual (voir app/services/asr.py) est OPTIONNEL et Linux
20
+ # uniquement (fairseq2n n'a aucun wheel Windows) : voir requirements-omnilingual.txt
21
+ # et pre_download.py pour l'installation (WSL/Docker/HF Spaces).
run_omnilingual_wsl.sh ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Runs model-service from WSL with ASR_BACKEND=omnilingual, on port 8000.
3
+ # WSL2 forwards localhost automatically, so the Next.js backend on Windows
4
+ # (MODEL_SERVICE_URL=http://localhost:8000) keeps working unchanged.
5
+ #
6
+ # IMPORTANT: stop any Windows-hosted uvicorn on :8000 first (only one process
7
+ # can bind that port). From PowerShell:
8
+ # Get-NetTCPConnection -LocalPort 8000 -State Listen | Select -Expand OwningProcess
9
+ # Stop-Process -Id <pid> -Force
10
+ #
11
+ # Usage (from Windows, via PowerShell or Git Bash):
12
+ # wsl -d Ubuntu -- bash /mnt/c/Users/User/coding/hackaton/locallang/model-service/run_omnilingual_wsl.sh
13
+ set -euo pipefail
14
+
15
+ cd /mnt/c/Users/User/coding/hackaton/locallang/model-service
16
+
17
+ # Override .env's ASR_BACKEND=local for this run only (env var takes
18
+ # priority over .env in pydantic-settings). Edit model-service/.env directly
19
+ # instead if you want this to stick permanently.
20
+ export ASR_BACKEND=omnilingual_ctc
21
+
22
+ /root/asr-bench/.venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
setup_wsl_env.sh ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Full from-scratch setup of the WSL environment for ASR_BACKEND=omnilingual.
3
+ # Only needed once (or again if the WSL distro / venv gets wiped) -- the
4
+ # environment already exists as of 2026-07-04, this documents how it was
5
+ # built so it's reproducible on another machine or after a reset.
6
+ #
7
+ # Usage (from Windows, after `wsl --install -d Ubuntu --no-launch`):
8
+ # wsl -d Ubuntu -- bash /mnt/c/Users/User/coding/hackaton/locallang/model-service/setup_wsl_env.sh
9
+ set -euo pipefail
10
+
11
+ apt-get update -qq
12
+ apt-get install -y -qq curl ca-certificates ffmpeg build-essential
13
+
14
+ curl -LsSf https://astral.sh/uv/install.sh | sh
15
+ UV=/root/.local/bin/uv
16
+
17
+ $UV python install 3.11
18
+
19
+ mkdir -p /root/asr-bench
20
+ cd /root/asr-bench
21
+ $UV venv --python 3.11 .venv
22
+ PY=/root/asr-bench/.venv/bin/python
23
+
24
+ # fairseq2 only has prebuilt wheels for specific torch versions (2.9.0/2.9.1
25
+ # at the time of writing) -- must match EXACTLY or fairseq2n segfaults.
26
+ $UV pip install --python "$PY" "torch==2.9.1" "torchaudio==2.9.1" \
27
+ --index-url https://download.pytorch.org/whl/cpu
28
+
29
+ # fair.pkg.atmeta.com's TLS cert was expired at the time of writing (Meta's
30
+ # own infra issue, not ours) -- --allow-insecure-host bypasses verification
31
+ # for this specific host only. Remove once Meta fixes their cert.
32
+ $UV pip install --python "$PY" "fairseq2" \
33
+ --extra-index-url https://fair.pkg.atmeta.com/fairseq2/whl/pt2.9.1/cpu \
34
+ --allow-insecure-host fair.pkg.atmeta.com \
35
+ --index-strategy unsafe-best-match
36
+
37
+ # --no-deps: omnilingual-asr's declared torch dependency is unpinned and
38
+ # would otherwise pull the CUDA build (~2GB) instead of reusing the CPU one
39
+ # just installed above.
40
+ $UV pip install --python "$PY" omnilingual-asr --no-deps
41
+
42
+ # Transitive deps missing from omnilingual-asr's/fairseq2's own metadata.
43
+ $UV pip install --python "$PY" retrying xxhash
44
+
45
+ # Rest of model-service's own requirements (see requirements.txt).
46
+ $UV pip install --python "$PY" \
47
+ fastapi "uvicorn[standard]" python-multipart accelerate sentencepiece \
48
+ pydantic-settings python-dotenv soundfile scipy pydub pillow requests \
49
+ huggingface_hub
50
+
51
+ echo "✅ WSL environment ready at /root/asr-bench/.venv"