stt and ttt together commit
Browse files- Dockerfile +41 -1
- app/main.py +62 -40
- 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 ["
|
|
|
|
| 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 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
def encode_header(value: str) -> str:
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 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-
|
| 63 |
-
"X-
|
| 64 |
-
"X-
|
| 65 |
-
"
|
| 66 |
-
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
try:
|
| 89 |
user_message = prepare_user_message(body.message)
|
| 90 |
except ValueError as e:
|
| 91 |
-
raise HTTPException(
|
| 92 |
-
|
| 93 |
-
|
| 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 |
-
|
| 104 |
-
|
| 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
|
| 7 |
-
AutoTokenizer,
|
| 8 |
-
AutoModelForCausalLM,
|
| 9 |
-
TextIteratorStreamer,
|
| 10 |
-
)
|
| 11 |
|
| 12 |
from app.config import config
|
| 13 |
|
| 14 |
-
|
| 15 |
SYSTEM_PROMPT = (
|
| 16 |
-
"You are
|
| 17 |
-
"
|
| 18 |
-
"
|
| 19 |
-
"
|
| 20 |
-
"
|
| 21 |
-
"
|
| 22 |
-
"
|
| 23 |
-
"
|
| 24 |
-
"
|
| 25 |
-
"
|
| 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 |
-
|
| 53 |
-
|
| 54 |
-
|
| 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 |
-
|
| 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 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 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 |
|