xubayer's picture
Update app.py
602cc1c verified
Raw
History Blame Contribute Delete
6.65 kB
import os
import gradio as gr
import torch
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
BitsAndBytesConfig
)
from peft import PeftModel
from huggingface_hub import hf_hub_download
# --- 1. Configuration ---
# Ensure 'HF_TOKEN' is set in your Space's Settings > Secrets
HF_TOKEN = os.getenv("HF_TOKEN")
# Model IDs
LARGE_BASE = "meta-llama/Llama-3.1-8B"
LARGE_ADAPTER = "tuochao/Llama-3.1-8B-Proactive-Big-Peft"
SMALL_BASE = "meta-llama/Llama-3.2-1B"
# YOUR NEW REPO (Contains adapter + classifier_weights.pth)
SMALL_ADAPTER = "xubayer/Llama-3.2-1B-Proactive-Small-Complete"
# 4-bit Quantization Config (Crucial for fitting both models on T4 GPU)
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_quant_type="nf4",
)
# --- 2. Load Small Classifier (Custom Architecture) ---
print(f"Loading Classifier Base & Adapter: {SMALL_ADAPTER}...")
# A. Load Base Model (CausalLM) + LoRA Adapter
# We load as CausalLM because the PEFT adapter was trained on a CausalLM base.
cl_base = AutoModelForCausalLM.from_pretrained(
SMALL_BASE,
quantization_config=bnb_config,
device_map="auto",
token=HF_TOKEN
)
classifier_model = PeftModel.from_pretrained(cl_base, SMALL_ADAPTER, token=HF_TOKEN)
classifier_tokenizer = AutoTokenizer.from_pretrained(SMALL_BASE, token=HF_TOKEN)
# Fix for Llama 3 tokenizer
if classifier_tokenizer.pad_token is None:
classifier_tokenizer.pad_token = classifier_tokenizer.eos_token
classifier_model.config.pad_token_id = classifier_model.config.eos_token_id
# B. Reconstruct Custom Classifier Head
# This manually adds the Linear layer (hidden_size -> 2) that Unsloth used.
hidden_size = classifier_model.config.hidden_size
classifier_head = torch.nn.Linear(hidden_size, 2).to(classifier_model.device)
# C. Download and Load Custom Weights
try:
print("Downloading custom classifier head weights...")
weights_path = hf_hub_download(
repo_id=SMALL_ADAPTER,
filename="classifier_weights.pth",
token=HF_TOKEN
)
# Load weights
state_dict = torch.load(weights_path, map_location=classifier_model.device)
classifier_head.load_state_dict(state_dict)
print("βœ… Custom classifier weights loaded successfully.")
except Exception as e:
print(f"⚠️ CRITICAL ERROR: Could not load 'classifier_weights.pth'.\nDetails: {e}")
# We don't crash here, but inference will be random if this fails.
# --- 3. Load Large Generator ---
print(f"Loading Generator: {LARGE_ADAPTER}...")
gen_base = AutoModelForCausalLM.from_pretrained(
LARGE_BASE,
quantization_config=bnb_config,
device_map="auto",
token=HF_TOKEN
)
generator_model = PeftModel.from_pretrained(gen_base, LARGE_ADAPTER, token=HF_TOKEN)
generator_tokenizer = AutoTokenizer.from_pretrained(LARGE_BASE, token=HF_TOKEN)
if generator_tokenizer.pad_token is None:
generator_tokenizer.pad_token = generator_tokenizer.eos_token
# --- 4. Inference Logic ---
def predict(history_text):
"""
Pipeline:
1. Append |SILENCE > marker.
2. Extract hidden state from 1B model.
3. Pass through custom classifier head.
4. If 'Whisper' (Class 1) > Threshold, run 8B generator.
"""
# Input formatting
classifier_input = history_text.strip()
# Assuming your model was trained to see this marker at the end
if not classifier_input.endswith("|SILENCE >"):
classifier_input += " |SILENCE >"
# --- Stage 1: Classifier ---
inputs = classifier_tokenizer(
classifier_input,
return_tensors="pt",
truncation=True,
max_length=2048
).to(classifier_model.device)
with torch.no_grad():
# Run base model to get hidden states
outputs = classifier_model(
**inputs,
output_hidden_states=True
)
# Get the last hidden state of the last token (the '>' in |SILENCE >)
last_hidden_state = outputs.hidden_states[-1]
last_token_state = last_hidden_state[:, -1, :]
# Pass through our custom linear head
logits = classifier_head(last_token_state)
# Convert to probability
probs = torch.nn.functional.softmax(logits, dim=-1)
whisper_score = probs[0][1].item() # Probability of Class 1 (Whisper)
status_msg = f"Confidence: {whisper_score:.2%} (Threshold: 50%)"
# --- Stage 2: Generator ---
whisper_content = ""
threshold = 0.5
if whisper_score > threshold:
status_msg += " -> 🟒 TRIGGERED"
# Generator sees the same history
gen_inputs = generator_tokenizer(history_text, return_tensors="pt").to(generator_model.device)
with torch.no_grad():
gen_out = generator_model.generate(
**gen_inputs,
max_new_tokens=40, # Keep response short
do_sample=True, # Creative generation
temperature=0.6,
top_p=0.9,
pad_token_id=generator_tokenizer.pad_token_id
)
full_text = generator_tokenizer.decode(gen_out[0], skip_special_tokens=True)
# Clean up: Remove the original conversation history from the output
# (This logic assumes the model repeats the prompt, which CausalLMs do)
if full_text.startswith(history_text):
whisper_content = full_text[len(history_text):].strip()
else:
whisper_content = full_text # Fallback
else:
status_msg += " -> πŸ”΄ SILENT"
whisper_content = "(No whisper needed)"
return status_msg, whisper_content
# --- 5. UI Setup ---
with gr.Blocks(title="LlamaPIE Final Demo") as demo:
gr.Markdown("# πŸ₯§ LlamaPIE: Proactive In-Ear Assistant")
gr.Markdown("A dual-model pipeline: **1B Classifier** detects silence, **8B Generator** whispers suggestions.")
with gr.Row():
input_box = gr.Textbox(
label="Conversation Context",
lines=4,
placeholder="User: I'm bored.\nAI: Do you want to watch a movie? |SILENCE >",
value="User: I forgot to buy milk.\nAI: That's annoying.\nUser: Yeah, I guess I'll have black coffee. |SILENCE >"
)
btn = gr.Button("⚑ Run Inference Pipeline", variant="primary")
with gr.Row():
d_box = gr.Textbox(label="Stage 1: Decision (Confidence)")
w_box = gr.Textbox(label="Stage 2: Generated Whisper")
btn.click(fn=predict, inputs=input_box, outputs=[d_box, w_box])
if __name__ == "__main__":
demo.launch()