drrobot9 commited on
Commit
371d12f
·
verified ·
1 Parent(s): 267ef3a

Upload folder using huggingface_hub

Browse files
Dockerfile ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # System dependencies
4
+ RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ ffmpeg \
6
+ git \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ WORKDIR /app
10
+
11
+ # Install dependencies
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy application code
16
+ COPY . .
17
+
18
+ # Pre-download all models at build time using snapshot_download
19
+ # HF_TOKEN must be set as a build secret or Space secret
20
+ ARG HF_TOKEN
21
+ ENV HF_TOKEN=${HF_TOKEN}
22
+
23
+ RUN python - <<'EOF'
24
+ import os
25
+ from huggingface_hub import snapshot_download, login
26
+
27
+ token = os.environ.get("HF_TOKEN", "")
28
+ if token:
29
+ login(token=token)
30
+
31
+ models = [
32
+ "drrobot9/wav2vec2-nigerian-language-identifier-v8",
33
+ "NCAIR1/Yoruba-ASR",
34
+ "NCAIR1/Igbo-ASR",
35
+ "NCAIR1/Hausa-ASR",
36
+ "NCAIR1/NigerianAccentedEnglish",
37
+ "NCAIR1/N-ATLaS",
38
+ ]
39
+
40
+ for model_id in models:
41
+ print(f"Downloading {model_id}...")
42
+ snapshot_download(
43
+ repo_id=model_id,
44
+ token=token,
45
+ ignore_patterns=["*.msgpack", "*.h5", "flax_model*", "tf_model*",
46
+ "rust_model*", "optimizer.pt", "rng_state*",
47
+ "training_args.bin", "trainer_state.json",
48
+ "scheduler.pt"],
49
+ )
50
+ print(f" Done: {model_id}")
51
+
52
+ print("All models downloaded.")
53
+ EOF
54
+
55
+ EXPOSE 7860
56
+
57
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
app/config.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dataclasses import dataclass, field
3
+ from typing import Dict
4
+
5
+
6
+ @dataclass
7
+ class ModelConfig:
8
+
9
+ HF_TOKEN: str = field(default_factory=lambda: os.environ.get("HF_TOKEN", "HF_TOKEN"))
10
+
11
+
12
+ LID_MODEL: str = "drrobot9/wav2vec2-nigerian-language-identifier-v8"
13
+
14
+
15
+ ASR_MODELS: Dict[str, str] = field(default_factory=lambda: {
16
+ "yoruba": "NCAIR1/Yoruba-ASR",
17
+ "igbo": "NCAIR1/Igbo-ASR",
18
+ "hausa": "NCAIR1/Hausa-ASR",
19
+ "english": "NCAIR1/NigerianAccentedEnglish",
20
+ })
21
+
22
+ # LLM
23
+ LLM_MODEL: str = "NCAIR1/N-ATLaS"
24
+
25
+ # Audio
26
+ SAMPLING_RATE: int = 16_000
27
+
28
+ # LLM generation
29
+ LLM_MAX_NEW_TOKENS: int = 1000
30
+ LLM_TEMPERATURE: float = 0.1
31
+ LLM_REPETITION_PENALTY: float = 1.12
32
+
33
+ # Memory
34
+ MAX_HISTORY_TURNS: int = 10
35
+
36
+
37
+ config = ModelConfig()
app/main.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from transformers import safetensors_conversion
3
+ safetensors_conversion.auto_conversion = lambda *args, **kwargs: None
4
+
5
+ os.environ["TRANSFORMERS_NO_ADVISORY_WARNINGS"] = "1"
6
+
7
+ import uuid
8
+ import base64
9
+ from huggingface_hub import login, get_token
10
+ from fastapi import FastAPI, UploadFile, File, Header, HTTPException
11
+ from fastapi.responses import StreamingResponse
12
+ from pydantic import BaseModel
13
+ from typing import Optional
14
+
15
+ # Authenticate
16
+ hf_token = os.environ.get("HF_TOKEN") or get_token()
17
+ if hf_token:
18
+ login(token=hf_token)
19
+ else:
20
+ raise RuntimeError(
21
+ "No HuggingFace token found. "
22
+ "Set HF_TOKEN environment variable or run `huggingface-cli login`."
23
+ )
24
+
25
+ from memory import get_history, append_turn, clear_session
26
+ from stt_module.pipeline import stt_pipeline
27
+ from text_module.router import prepare_user_message
28
+ from llm.engine import llm_engine
29
+
30
+ app = FastAPI(title="FarmLingua AI", version="1.0.0")
31
+
32
+
33
+ # Helpers
34
+
35
+ def resolve_uid(x_uid: Optional[str]) -> str:
36
+ return x_uid if x_uid else str(uuid.uuid4())
37
+
38
+
39
+ def encode_header(value: str) -> str:
40
+ """Base64-encode header values that may contain non-latin-1 characters."""
41
+ return base64.b64encode(value.encode("utf-8")).decode("ascii")
42
+
43
+
44
+ def stream_llm(uid: str, channel: str, user_message: str):
45
+ history = get_history(uid, channel)
46
+ append_turn(uid, channel, "user", user_message)
47
+
48
+ streamer = llm_engine.stream(history, user_message)
49
+ full_response = []
50
+
51
+ for token in streamer:
52
+ full_response.append(token)
53
+ yield token
54
+
55
+ assistant_reply = "".join(full_response).strip()
56
+ append_turn(uid, channel, "assistant", assistant_reply)
57
+
58
+
59
+ # Routes
60
+
61
+ @app.post("/stt/transcribe-and-chat")
62
+ async def stt_transcribe_and_chat(
63
+ audio: UploadFile = File(...),
64
+ x_uid: Optional[str] = Header(default=None),
65
+ ):
66
+ uid = resolve_uid(x_uid)
67
+ audio_bytes = await audio.read()
68
+
69
+ try:
70
+ stt_result = stt_pipeline.transcribe(audio_bytes)
71
+ except ValueError as e:
72
+ raise HTTPException(status_code=422, detail=str(e))
73
+
74
+ transcription = stt_result["transcription"]
75
+
76
+ headers = {
77
+ "X-UID": uid,
78
+ "X-Transcription": encode_header(transcription), # base64 — safe for latin-1
79
+ "X-Language": stt_result["language"],
80
+ "X-Confidence": str(stt_result["confidence"]),
81
+ "Access-Control-Expose-Headers": "X-UID, X-Transcription, X-Language, X-Confidence",
82
+ }
83
+
84
+ try:
85
+ return StreamingResponse(
86
+ stream_llm(uid, "stt", transcription),
87
+ media_type="text/plain",
88
+ headers=headers,
89
+ )
90
+ except RuntimeError as e:
91
+ raise HTTPException(status_code=503, detail=str(e))
92
+
93
+
94
+ class TextRequest(BaseModel):
95
+ message: str
96
+
97
+
98
+ @app.post("/text/chat")
99
+ async def text_chat(
100
+ body: TextRequest,
101
+ x_uid: Optional[str] = Header(default=None),
102
+ ):
103
+ uid = resolve_uid(x_uid)
104
+
105
+ try:
106
+ user_message = prepare_user_message(body.message)
107
+ except ValueError as e:
108
+ raise HTTPException(status_code=400, detail=str(e))
109
+
110
+ headers = {
111
+ "X-UID": uid,
112
+ "Access-Control-Expose-Headers": "X-UID",
113
+ }
114
+
115
+ try:
116
+ return StreamingResponse(
117
+ stream_llm(uid, "text", user_message),
118
+ media_type="text/plain",
119
+ headers=headers,
120
+ )
121
+ except RuntimeError as e:
122
+ raise HTTPException(status_code=503, detail=str(e))
123
+
124
+
125
+ @app.delete("/session")
126
+ async def clear_user_session(x_uid: str = Header(...)):
127
+ clear_session(x_uid)
128
+ return {"status": "cleared", "uid": x_uid}
129
+
130
+
131
+ @app.get("/health")
132
+ async def health():
133
+ return {"status": "ok"}
llm/__init__.py ADDED
File without changes
llm/engine.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from datetime import datetime
3
+ from threading import Thread
4
+ from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
5
+ from typing import List
6
+
7
+ from app.config import config
8
+
9
+ SYSTEM_PROMPT = (
10
+ "You are a helpful multilingual assistant supporting Yoruba, Igbo, Hausa, and English. "
11
+ "Respond clearly and directly. Do not repeat the user's question. "
12
+ "Do not include any prompt, preamble, or meta-commentary in your response. "
13
+ "You are called FarmLingua AI that is you name. "
14
+ "You are an agricultural expert on all kinds of farming. "
15
+ "You are here to help Nigerian farmers."
16
+ "so always tell the users after answering their questions that kawafarm LTD built you for farmers."
17
+ "make sure you answer the users questions very well ."
18
+ )
19
+
20
+
21
+ class LLMEngine:
22
+ def __init__(self):
23
+ token = config.HF_TOKEN or None
24
+
25
+ self.tokenizer = AutoTokenizer.from_pretrained(
26
+ config.LLM_MODEL,
27
+ token=token,
28
+ )
29
+ self.model = AutoModelForCausalLM.from_pretrained(
30
+ config.LLM_MODEL,
31
+ dtype=torch.float16,
32
+ device_map="auto",
33
+ token=token,
34
+ # N-ATLaS ships sharded safetensors — do not set use_safetensors=False
35
+ )
36
+ self.model.eval()
37
+
38
+ def _build_messages(self, history: List[dict], user_message: str) -> List[dict]:
39
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
40
+ messages.extend(history)
41
+ messages.append({"role": "user", "content": user_message})
42
+ return messages
43
+
44
+ def _format_prompt(self, messages: List[dict]) -> str:
45
+ return self.tokenizer.apply_chat_template(
46
+ messages,
47
+ add_generation_prompt=True,
48
+ tokenize=False,
49
+ date_string=datetime.now().strftime("%d %b %Y"),
50
+ )
51
+
52
+ def stream(self, history: List[dict], user_message: str):
53
+ messages = self._build_messages(history, user_message)
54
+ prompt = self._format_prompt(messages)
55
+
56
+ inputs = self.tokenizer(
57
+ prompt,
58
+ return_tensors="pt",
59
+ add_special_tokens=False,
60
+ ).to(self.model.device)
61
+
62
+ streamer = TextIteratorStreamer(
63
+ self.tokenizer,
64
+ skip_prompt=True,
65
+ skip_special_tokens=True,
66
+ )
67
+
68
+ generation_kwargs = dict(
69
+ **inputs,
70
+ streamer=streamer,
71
+ max_new_tokens=config.LLM_MAX_NEW_TOKENS,
72
+ temperature=config.LLM_TEMPERATURE,
73
+ repetition_penalty=config.LLM_REPETITION_PENALTY,
74
+ use_cache=True,
75
+ do_sample=True,
76
+ )
77
+
78
+ thread = Thread(target=self.model.generate, kwargs=generation_kwargs)
79
+ thread.start()
80
+ return streamer
81
+
82
+
83
+ llm_engine = LLMEngine()
memory.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import defaultdict
2
+ from typing import Dict, List
3
+ from app.config import config
4
+
5
+
6
+ # In-memory store: { uid: { "stt": [...], "text": [...] } }
7
+ _store: Dict[str, Dict[str, List[dict]]] = defaultdict(
8
+ lambda: {"stt": [], "text": []}
9
+ )
10
+
11
+
12
+ def get_history(uid: str, channel: str) -> List[dict]:
13
+ """Return conversation history for a user on a given channel (stt | text)."""
14
+ return _store[uid][channel]
15
+
16
+
17
+ def append_turn(uid: str, channel: str, role: str, content: str) -> None:
18
+ """Append a single turn and enforce the history cap."""
19
+ history = _store[uid][channel]
20
+ history.append({"role": role, "content": content})
21
+
22
+ # Keep only the last N turns (each turn = 1 message, cap covers both roles)
23
+ max_messages = config.MAX_HISTORY_TURNS * 2
24
+ if len(history) > max_messages:
25
+ _store[uid][channel] = history[-max_messages:]
26
+
27
+
28
+ def clear_session(uid: str) -> None:
29
+ """Clear all history for a user across both channels."""
30
+ _store.pop(uid, None)
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ transformers
4
+ torch
5
+ torchaudio
6
+ pydub
7
+ accelerate
8
+ soundfile
9
+ python-multipart
10
+ huggingface-hub
11
+ datasets
12
+ numpy
stt_module/__init__.py ADDED
File without changes
stt_module/pipeline.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import numpy as np
3
+ import torch
4
+ from pydub import AudioSegment
5
+ from transformers import (
6
+ pipeline,
7
+ WhisperProcessor,
8
+ WhisperForConditionalGeneration,
9
+ )
10
+ from typing import Tuple
11
+
12
+ from app.config import config
13
+
14
+
15
+ class WhisperASR:
16
+ """Explicit Whisper loader — avoids pipeline preprocessor num_frames bug."""
17
+
18
+ def __init__(self, model_id: str, token: str, device: torch.device):
19
+ self.processor = WhisperProcessor.from_pretrained(
20
+ model_id,
21
+ token=token,
22
+ )
23
+ self.model = WhisperForConditionalGeneration.from_pretrained(
24
+ model_id,
25
+ token=token,
26
+ use_safetensors=False,
27
+ ).to(device)
28
+ self.model.eval()
29
+ self.device = device
30
+
31
+ def transcribe(self, samples: np.ndarray, sampling_rate: int) -> str:
32
+ inputs = self.processor(
33
+ samples,
34
+ sampling_rate=sampling_rate,
35
+ return_tensors="pt",
36
+ ).to(self.device)
37
+
38
+ with torch.no_grad():
39
+ predicted_ids = self.model.generate(**inputs)
40
+
41
+ transcription = self.processor.batch_decode(
42
+ predicted_ids,
43
+ skip_special_tokens=True,
44
+ )
45
+ return transcription[0].strip()
46
+
47
+
48
+ class STTPipeline:
49
+ def __init__(self):
50
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
51
+ token = config.HF_TOKEN or None
52
+
53
+ # Language identifier — wav2vec2, pipeline is fine here
54
+ self.lid = pipeline(
55
+ "audio-classification",
56
+ model=config.LID_MODEL,
57
+ device=0 if self.device.type == "cuda" else -1,
58
+ token=token,
59
+ )
60
+
61
+ # ASR models — explicit Whisper loader per language
62
+ self.asr: dict = {
63
+ lang: WhisperASR(model_id, token, self.device)
64
+ for lang, model_id in config.ASR_MODELS.items()
65
+ }
66
+
67
+ def decode_audio(self, audio_bytes: bytes) -> Tuple[np.ndarray, float]:
68
+ seg = AudioSegment.from_file(io.BytesIO(audio_bytes))
69
+ seg = seg.set_channels(1).set_frame_rate(config.SAMPLING_RATE)
70
+ samples = np.array(seg.get_array_of_samples()).astype(np.float32)
71
+ samples /= np.iinfo(seg.array_type).max
72
+ duration = len(seg) / 1000.0
73
+ return samples, duration
74
+
75
+ def classify_language(self, samples: np.ndarray) -> Tuple[str, float]:
76
+ result = self.lid(
77
+ {"array": samples, "sampling_rate": config.SAMPLING_RATE},
78
+ top_k=1,
79
+ )
80
+ label = result[0]["label"].lower()
81
+ confidence = result[0]["score"]
82
+ return label, confidence
83
+
84
+ def transcribe(self, audio_bytes: bytes) -> dict:
85
+ samples, duration = self.decode_audio(audio_bytes)
86
+ language, confidence = self.classify_language(samples)
87
+
88
+ asr_model = self.asr.get(language)
89
+ if asr_model is None:
90
+ raise ValueError(
91
+ f"No ASR model available for detected language: '{language}'. "
92
+ f"Supported languages: {list(self.asr.keys())}"
93
+ )
94
+
95
+ transcription = asr_model.transcribe(samples, config.SAMPLING_RATE)
96
+ return {
97
+ "transcription": transcription,
98
+ "language": language,
99
+ "confidence": round(confidence, 4),
100
+ "duration_sec": round(duration, 2),
101
+ }
102
+
103
+
104
+ stt_pipeline = STTPipeline()
text_module/__init__.py ADDED
File without changes
text_module/router.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+
2
+ def prepare_user_message(text: str) -> str:
3
+ text = text.strip()
4
+ if not text:
5
+ raise ValueError("User message cannot be empty.")
6
+ return text
wav2vec2-nigerian-lid-v2/config.json ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "activation_dropout": 0.0,
3
+ "adapter_attn_dim": null,
4
+ "adapter_kernel_size": 3,
5
+ "adapter_stride": 2,
6
+ "add_adapter": false,
7
+ "apply_spec_augment": true,
8
+ "architectures": [
9
+ "Wav2Vec2ForSequenceClassification"
10
+ ],
11
+ "attention_dropout": 0.1,
12
+ "bos_token_id": 1,
13
+ "classifier_proj_size": 256,
14
+ "codevector_dim": 768,
15
+ "contrastive_logits_temperature": 0.1,
16
+ "conv_bias": true,
17
+ "conv_dim": [
18
+ 512,
19
+ 512,
20
+ 512,
21
+ 512,
22
+ 512,
23
+ 512,
24
+ 512
25
+ ],
26
+ "conv_kernel": [
27
+ 10,
28
+ 3,
29
+ 3,
30
+ 3,
31
+ 3,
32
+ 2,
33
+ 2
34
+ ],
35
+ "conv_stride": [
36
+ 5,
37
+ 2,
38
+ 2,
39
+ 2,
40
+ 2,
41
+ 2,
42
+ 2
43
+ ],
44
+ "ctc_loss_reduction": "sum",
45
+ "ctc_zero_infinity": false,
46
+ "diversity_loss_weight": 0.1,
47
+ "do_stable_layer_norm": true,
48
+ "dtype": "float32",
49
+ "eos_token_id": 2,
50
+ "feat_extract_activation": "gelu",
51
+ "feat_extract_dropout": 0.0,
52
+ "feat_extract_norm": "layer",
53
+ "feat_proj_dropout": 0.1,
54
+ "feat_quantizer_dropout": 0.0,
55
+ "final_dropout": 0.0,
56
+ "gradient_checkpointing": false,
57
+ "hidden_act": "gelu",
58
+ "hidden_dropout": 0.1,
59
+ "hidden_size": 1024,
60
+ "id2label": {
61
+ "0": "hausa",
62
+ "1": "igbo",
63
+ "2": "yoruba",
64
+ "3": "english"
65
+ },
66
+ "initializer_range": 0.02,
67
+ "intermediate_size": 4096,
68
+ "label2id": {
69
+ "english": 3,
70
+ "hausa": 0,
71
+ "igbo": 1,
72
+ "yoruba": 2
73
+ },
74
+ "layer_norm_eps": 1e-05,
75
+ "layerdrop": 0.1,
76
+ "mask_channel_length": 10,
77
+ "mask_channel_min_space": 1,
78
+ "mask_channel_other": 0.0,
79
+ "mask_channel_prob": 0.0,
80
+ "mask_channel_selection": "static",
81
+ "mask_feature_length": 10,
82
+ "mask_feature_min_masks": 0,
83
+ "mask_feature_prob": 0.0,
84
+ "mask_time_length": 10,
85
+ "mask_time_min_masks": 2,
86
+ "mask_time_min_space": 1,
87
+ "mask_time_other": 0.0,
88
+ "mask_time_prob": 0.075,
89
+ "mask_time_selection": "static",
90
+ "model_type": "wav2vec2",
91
+ "num_adapter_layers": 3,
92
+ "num_attention_heads": 16,
93
+ "num_codevector_groups": 2,
94
+ "num_codevectors_per_group": 320,
95
+ "num_conv_pos_embedding_groups": 16,
96
+ "num_conv_pos_embeddings": 128,
97
+ "num_feat_extract_layers": 7,
98
+ "num_hidden_layers": 24,
99
+ "num_negatives": 100,
100
+ "output_hidden_size": 1024,
101
+ "pad_token_id": 0,
102
+ "proj_codevector_dim": 768,
103
+ "tdnn_dilation": [
104
+ 1,
105
+ 2,
106
+ 3,
107
+ 1,
108
+ 1
109
+ ],
110
+ "tdnn_dim": [
111
+ 512,
112
+ 512,
113
+ 512,
114
+ 512,
115
+ 1500
116
+ ],
117
+ "tdnn_kernel": [
118
+ 5,
119
+ 3,
120
+ 3,
121
+ 1,
122
+ 1
123
+ ],
124
+ "transformers_version": "5.0.0",
125
+ "use_cache": false,
126
+ "use_weighted_layer_sum": false,
127
+ "vocab_size": 32,
128
+ "xvector_output_dim": 512
129
+ }