Spaces:
Runtime error
Runtime error
File size: 5,473 Bytes
b6783b8 b425bec e18d5ed b6783b8 e18d5ed b6783b8 b425bec b6783b8 b425bec b6783b8 b425bec e18d5ed b6783b8 e18d5ed b6783b8 b425bec b6783b8 b425bec b6783b8 b425bec b6783b8 b425bec b6783b8 e18d5ed b6783b8 e18d5ed b425bec e18d5ed b425bec b6783b8 b425bec b6783b8 e18d5ed b6783b8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | """Umubyeyi fine-tuned generator inference API (Hugging Face Space, Gradio + ZeroGPU).
Loads the real BLOOMZ-560m + LoRA adapter fine-tuned on ESConv (the same
weights committed at models/umubyeyi-bloomz-lora/ in the main repo, merged
into a single checkpoint offline -- see hf_space/README.md for why) and
exposes it as a named Gradio API endpoint. The deployed Vercel app cannot
bundle PyTorch/Transformers within its 500MB serverless function limit, so
it calls this Space instead of falling straight back to retrieval -- this is
the only difference from local inference; the model and prompt format are
identical to src/finetuned_generator.py.
Called over HTTP as: POST {space_url}/call/generate {"data": [query, lang, history_json]}
then GET {space_url}/call/generate/{event_id} (Gradio's async SSE result contract).
"""
import json
import re
from pathlib import Path
import gradio as gr
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
try:
import spaces
except ImportError: # running outside a real ZeroGPU Space (local test)
class _NoOpSpaces:
@staticmethod
def GPU(fn):
return fn
spaces = _NoOpSpaces()
ROOT = Path(__file__).resolve().parent
MODEL_PATH = ROOT / "merged_model"
ACCEPTED_LANGUAGES = ["en"]
# Loaded (and moved to cuda) at module level, not inside the @spaces.GPU
# function: a CUDA emulation mode is active here even without a real GPU
# attached, and the ZeroGPU docs specifically recommend placement at
# startup over lazy/in-function loading. This is a plain merged checkpoint
# (LoRA folded in offline) rather than a PeftModel -- PEFT's own adapter
# loading path does not tolerate ZeroGPU's tensor-op patching well.
_tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
if _tokenizer.pad_token is None:
_tokenizer.pad_token = _tokenizer.eos_token
_model = AutoModelForCausalLM.from_pretrained(MODEL_PATH, dtype=torch.float16)
_model.to("cuda")
_model.eval()
def _normalise(text) -> str:
return " ".join(str(text or "").split()).strip()
def _format_generator_input(query: str, evidence: str, lang: str, history: str) -> str:
"""Mirrors src/generation_data.py's format_generator_input exactly --
the model was trained on this exact prompt shape."""
language = "Kinyarwanda" if lang == "rw" else "English"
evidence_text = _normalise(evidence) or "No external evidence supplied."
history_text = _normalise(history)
parts = [
"Write an empathetic emotional-support response.",
"Do not diagnose or invent medical facts.",
f"Language: {language}",
f"Evidence: {evidence_text}",
]
if history_text:
parts.append(f"Conversation: {history_text}")
parts.extend([f"User: {_normalise(query)}", "Response:"])
return "\n".join(parts)
def _history_text(history_items, max_turns: int = 6) -> str:
if not history_items:
return ""
lines = []
for item in history_items[-max_turns:]:
role = "Supporter" if item.get("role") == "bot" else "User"
content = (item.get("text") or "").strip()
if content:
lines.append(f"{role}: {content}")
return " ".join(lines)
@spaces.GPU
def generate(query: str, lang: str = "en", history_json: str = "[]") -> str:
if lang not in ACCEPTED_LANGUAGES:
return ""
try:
history_items = json.loads(history_json) if history_json else []
except (json.JSONDecodeError, TypeError):
history_items = []
prompt = _format_generator_input(query, "", lang, _history_text(history_items)) + "\n"
encoded = _tokenizer(prompt, return_tensors="pt", truncation=True, max_length=384).to("cuda")
try:
with torch.inference_mode():
output = _model.generate(
**encoded, max_new_tokens=120, do_sample=True, temperature=0.8, top_p=0.9,
no_repeat_ngram_size=3, pad_token_id=_tokenizer.pad_token_id,
)
new_tokens = output[0][encoded["input_ids"].shape[1]:]
answer = _tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
except Exception:
return ""
# The model was trained on single supporter turns; on rare degenerate
# continuations it hallucinates a further "User:"/"Supporter:" line.
answer = re.split(r"\n(?:User|Supporter):", answer)[0].strip()
if len(answer.split()) < 4:
return ""
return answer
with gr.Blocks(title="Umubyeyi -- fine-tuned generator") as demo:
gr.Markdown(
"# Umubyeyi -- fine-tuned generator inference API\n"
"Not a chat UI for end users. Real-time inference for Umubyeyi's own "
"fine-tuned BLOOMZ-560m generator (LoRA on real ESConv data, merged "
"offline) -- called by the deployed app's backend, since Vercel's "
"serverless functions cannot bundle PyTorch/Transformers within "
"their 500MB limit. Main project: https://github.com/IrutingaboRaissa/UMUBYEYI"
)
query_in = gr.Textbox(label="Message")
lang_in = gr.Textbox(label="Language (en only -- no Kinyarwanda training data yet)", value="en")
history_in = gr.Textbox(label="Conversation history (JSON list, optional)", value="[]")
answer_out = gr.Textbox(label="Answer")
submit_btn = gr.Button("Generate")
submit_btn.click(
fn=generate, inputs=[query_in, lang_in, history_in], outputs=answer_out, api_name="generate"
)
demo.queue()
if __name__ == "__main__":
demo.launch()
|