drrobot9 commited on
Commit
3c95cb7
·
verified ·
1 Parent(s): be72610

stt and ttt together commit

Browse files
Files changed (3) hide show
  1. Dockerfile +41 -1
  2. app/main.py +62 -40
  3. llm/engine.py +33 -57
Dockerfile CHANGED
@@ -1,5 +1,6 @@
1
  FROM python:3.11-slim
2
 
 
3
  RUN apt-get update && apt-get install -y --no-install-recommends \
4
  ffmpeg \
5
  git \
@@ -7,11 +8,50 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
7
 
8
  WORKDIR /app
9
 
 
10
  COPY requirements.txt .
11
  RUN pip install --no-cache-dir -r requirements.txt
12
 
 
13
  COPY . .
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  EXPOSE 7860
16
 
17
- CMD ["python", "start.py"]
 
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 \
 
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/main.py CHANGED
@@ -1,15 +1,18 @@
1
  import os
2
- import base64
3
-
4
  from transformers import safetensors_conversion
5
  safetensors_conversion.auto_conversion = lambda *args, **kwargs: None
 
6
  os.environ["TRANSFORMERS_NO_ADVISORY_WARNINGS"] = "1"
7
 
 
 
8
  from huggingface_hub import login, get_token
9
- from fastapi import FastAPI, UploadFile, File, HTTPException
10
  from fastapi.responses import StreamingResponse
11
  from pydantic import BaseModel
 
12
 
 
13
  hf_token = os.environ.get("HF_TOKEN") or get_token()
14
  if hf_token:
15
  login(token=hf_token)
@@ -19,64 +22,74 @@ else:
19
  "Set HF_TOKEN environment variable or run `huggingface-cli login`."
20
  )
21
 
 
22
  from stt_module.pipeline import stt_pipeline
23
  from text_module.router import prepare_user_message
24
  from llm.engine import llm_engine
25
 
26
- app = FastAPI(
27
- title="FarmLingua AI",
28
- version="1.0.0"
29
- )
 
 
 
30
 
31
 
32
  def encode_header(value: str) -> str:
33
- return base64.b64encode(
34
- value.encode("utf-8")
35
- ).decode("ascii")
36
 
 
 
 
37
 
38
- def stream_llm(user_message: str):
39
- streamer = llm_engine.stream(user_message)
40
 
41
  for token in streamer:
 
42
  yield token
43
 
 
 
 
 
 
 
44
 
45
  @app.post("/stt/transcribe-and-chat")
46
  async def stt_transcribe_and_chat(
47
- audio: UploadFile = File(...)
 
48
  ):
 
49
  audio_bytes = await audio.read()
50
 
51
  try:
52
  stt_result = stt_pipeline.transcribe(audio_bytes)
53
  except ValueError as e:
54
- raise HTTPException(
55
- status_code=422,
56
- detail=str(e),
57
- )
58
 
59
  transcription = stt_result["transcription"]
60
 
61
  headers = {
62
- "X-Transcription": encode_header(transcription),
63
- "X-Language": stt_result["language"],
64
- "X-Confidence": str(stt_result["confidence"]),
65
- "Access-Control-Expose-Headers":
66
- "X-Transcription, X-Language, X-Confidence",
67
  }
68
 
69
  try:
70
  return StreamingResponse(
71
- stream_llm(transcription),
72
  media_type="text/plain",
73
  headers=headers,
74
  )
75
  except RuntimeError as e:
76
- raise HTTPException(
77
- status_code=503,
78
- detail=str(e),
79
- )
80
 
81
 
82
  class TextRequest(BaseModel):
@@ -84,29 +97,38 @@ class TextRequest(BaseModel):
84
 
85
 
86
  @app.post("/text/chat")
87
- async def text_chat(body: TextRequest):
 
 
 
 
 
88
  try:
89
  user_message = prepare_user_message(body.message)
90
  except ValueError as e:
91
- raise HTTPException(
92
- status_code=400,
93
- detail=str(e),
94
- )
 
 
95
 
96
  try:
97
  return StreamingResponse(
98
- stream_llm(user_message),
99
  media_type="text/plain",
 
100
  )
101
  except RuntimeError as e:
102
- raise HTTPException(
103
- status_code=503,
104
- detail=str(e),
105
- )
 
 
 
106
 
107
 
108
  @app.get("/health")
109
  async def health():
110
- return {
111
- "status": "ok"
112
- }
 
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)
 
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
+
60
+ # Routes
61
 
62
  @app.post("/stt/transcribe-and-chat")
63
  async def stt_transcribe_and_chat(
64
+ audio: UploadFile = File(...),
65
+ x_uid: Optional[str] = Header(default=None),
66
  ):
67
+ uid = resolve_uid(x_uid)
68
  audio_bytes = await audio.read()
69
 
70
  try:
71
  stt_result = stt_pipeline.transcribe(audio_bytes)
72
  except ValueError as e:
73
+ raise HTTPException(status_code=422, detail=str(e))
 
 
 
74
 
75
  transcription = stt_result["transcription"]
76
 
77
  headers = {
78
+ "X-UID": uid,
79
+ "X-Transcription": encode_header(transcription), # base64 — safe for latin-1
80
+ "X-Language": stt_result["language"],
81
+ "X-Confidence": str(stt_result["confidence"]),
82
+ "Access-Control-Expose-Headers": "X-UID, X-Transcription, X-Language, X-Confidence",
83
  }
84
 
85
  try:
86
  return StreamingResponse(
87
+ stream_llm(uid, "stt", transcription),
88
  media_type="text/plain",
89
  headers=headers,
90
  )
91
  except RuntimeError as e:
92
+ raise HTTPException(status_code=503, detail=str(e))
 
 
 
93
 
94
 
95
  class TextRequest(BaseModel):
 
97
 
98
 
99
  @app.post("/text/chat")
100
+ async def text_chat(
101
+ body: TextRequest,
102
+ x_uid: Optional[str] = Header(default=None),
103
+ ):
104
+ uid = resolve_uid(x_uid)
105
+
106
  try:
107
  user_message = prepare_user_message(body.message)
108
  except ValueError as e:
109
+ raise HTTPException(status_code=400, detail=str(e))
110
+
111
+ headers = {
112
+ "X-UID": uid,
113
+ "Access-Control-Expose-Headers": "X-UID",
114
+ }
115
 
116
  try:
117
  return StreamingResponse(
118
+ stream_llm(uid, "text", user_message),
119
  media_type="text/plain",
120
+ headers=headers,
121
  )
122
  except RuntimeError as e:
123
+ raise HTTPException(status_code=503, detail=str(e))
124
+
125
+
126
+ @app.delete("/session")
127
+ async def clear_user_session(x_uid: str = Header(...)):
128
+ clear_session(x_uid)
129
+ return {"status": "cleared", "uid": x_uid}
130
 
131
 
132
  @app.get("/health")
133
  async def health():
134
+ return {"status": "ok"}
 
 
llm/engine.py CHANGED
@@ -1,79 +1,59 @@
1
  import torch
2
-
3
  from datetime import datetime
4
  from threading import Thread
5
-
6
- from transformers import (
7
- AutoTokenizer,
8
- AutoModelForCausalLM,
9
- TextIteratorStreamer,
10
- )
11
 
12
  from app.config import config
13
 
14
-
15
  SYSTEM_PROMPT = (
16
- "You are FarmLingua AI, an agricultural expert developed by Kawafarm LTD for Nigerian "
17
- "explain broadly in users language of question so they can understand"
18
- "farmers. Answer only agriculture-related questions, including crops, livestock, "
19
- "poultry, fisheries, soil, irrigation, fertilizers, pests, diseases, farm machinery, "
20
- "and agribusiness. If a question is unrelated to agriculture, politely explain that "
21
- "you specialize in farming and ask the user to ask an agriculture-related question. "
22
- "Understand spelling mistakes and informal language automatically. Give practical, "
23
- "answer igbo question when asked with igbo , yoruba when asked with yoruba,"
24
- "hausa when asked with hausa and english when asked with english"
25
- "accurate, and well-structured advice. Never invent facts. "
26
- "after every answer you give tell the users FarmLingua AI was developed by Kawafarm LTD to support farmers."
27
  )
28
 
29
 
30
  class LLMEngine:
31
-
32
  def __init__(self):
33
-
34
  token = config.HF_TOKEN or None
35
 
36
  self.tokenizer = AutoTokenizer.from_pretrained(
37
  config.LLM_MODEL,
38
  token=token,
39
  )
40
-
41
  self.model = AutoModelForCausalLM.from_pretrained(
42
  config.LLM_MODEL,
43
  dtype=torch.float16,
44
  device_map="auto",
45
  token=token,
 
46
  )
47
-
48
  self.model.eval()
49
 
50
- def _build_messages(self, user_message: str):
51
-
52
- return [
53
- {
54
- "role": "system",
55
- "content": SYSTEM_PROMPT,
56
- },
57
- {
58
- "role": "user",
59
- "content": user_message,
60
- },
61
- ]
62
-
63
- def _format_prompt(self, messages):
64
 
 
65
  return self.tokenizer.apply_chat_template(
66
  messages,
67
- tokenize=False,
68
  add_generation_prompt=True,
 
69
  date_string=datetime.now().strftime("%d %b %Y"),
70
  )
71
 
72
- def stream(self, user_message: str):
73
-
74
- messages = self._build_messages(user_message)
75
-
76
- prompt = self._format_prompt(messages)
77
 
78
  inputs = self.tokenizer(
79
  prompt,
@@ -87,22 +67,18 @@ class LLMEngine:
87
  skip_special_tokens=True,
88
  )
89
 
90
- generation_kwargs = {
91
  **inputs,
92
- "streamer": streamer,
93
- "max_new_tokens": config.LLM_MAX_NEW_TOKENS,
94
- "temperature": config.LLM_TEMPERATURE,
95
- "repetition_penalty": config.LLM_REPETITION_PENALTY,
96
- "do_sample": True,
97
- "use_cache": True,
98
- }
99
-
100
- Thread(
101
- target=self.model.generate,
102
- kwargs=generation_kwargs,
103
- daemon=True,
104
- ).start()
105
 
 
 
106
  return streamer
107
 
108
 
 
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
+ "do not answer question on any other field if it not about farming , farm business."
17
+ "users might not complete a question always understand smartly the intent of the question and answer."
18
+ "so always tell the users after answering their questions that kawafarm LTD built you for farmers."
19
+ "make sure you answer the users questions very well ."
 
20
  )
21
 
22
 
23
  class LLMEngine:
 
24
  def __init__(self):
 
25
  token = config.HF_TOKEN or None
26
 
27
  self.tokenizer = AutoTokenizer.from_pretrained(
28
  config.LLM_MODEL,
29
  token=token,
30
  )
 
31
  self.model = AutoModelForCausalLM.from_pretrained(
32
  config.LLM_MODEL,
33
  dtype=torch.float16,
34
  device_map="auto",
35
  token=token,
36
+ # N-ATLaS ships sharded safetensors — do not set use_safetensors=False
37
  )
 
38
  self.model.eval()
39
 
40
+ def _build_messages(self, history: List[dict], user_message: str) -> List[dict]:
41
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
42
+ messages.extend(history)
43
+ messages.append({"role": "user", "content": user_message})
44
+ return messages
 
 
 
 
 
 
 
 
 
45
 
46
+ def _format_prompt(self, messages: List[dict]) -> str:
47
  return self.tokenizer.apply_chat_template(
48
  messages,
 
49
  add_generation_prompt=True,
50
+ tokenize=False,
51
  date_string=datetime.now().strftime("%d %b %Y"),
52
  )
53
 
54
+ def stream(self, history: List[dict], user_message: str):
55
+ messages = self._build_messages(history, user_message)
56
+ prompt = self._format_prompt(messages)
 
 
57
 
58
  inputs = self.tokenizer(
59
  prompt,
 
67
  skip_special_tokens=True,
68
  )
69
 
70
+ generation_kwargs = dict(
71
  **inputs,
72
+ streamer=streamer,
73
+ max_new_tokens=config.LLM_MAX_NEW_TOKENS,
74
+ temperature=config.LLM_TEMPERATURE,
75
+ repetition_penalty=config.LLM_REPETITION_PENALTY,
76
+ use_cache=True,
77
+ do_sample=True,
78
+ )
 
 
 
 
 
 
79
 
80
+ thread = Thread(target=self.model.generate, kwargs=generation_kwargs)
81
+ thread.start()
82
  return streamer
83
 
84