Spaces:
Runtime error
Runtime error
| """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: | |
| 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) | |
| 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() | |