import time
import base64
import io
import re
from PIL import Image
import torch
from .model_loader import model_loader
from .persona_manager import persona_manager
from .config import MAX_NEW_TOKENS, TEMPERATURE, TOP_K, TOP_P
def predict(
pictogram_sequence: list[str],
persona_id: str,
context: str = "daily",
audio_transcript: str = None,
image_base64: str = None,
n_alternatives: int = 3
) -> dict:
start_time = time.time()
# 1. Get persona
persona = persona_manager.get_persona(persona_id)
if not persona:
return {"alternatives": ["Persona not found"], "primary": "Persona not found", "inference_time_ms": 0, "persona_id": persona_id}
system_prompt = persona.to_system_prompt()
# 2. Build prompt
pictogram_str = ", ".join(pictogram_sequence)
audio_note = f"\nChild vocalization heard: '{audio_transcript}' — use this to refine the prediction" if audio_transcript else ""
image_note = "\nImage provided for visual grounding." if image_base64 else ""
prompt = f"""
{system_prompt}
Understood. Ready.
Pictogram sequence: [{pictogram_str}]
Context: {context}{audio_note}{image_note}
Predict {n_alternatives} most likely sentences. Use child's language mix. Short sentences. Numbered list.
"""
# 3. Handle image
image = None
if image_base64:
try:
image_data = base64.b64decode(image_base64)
image = Image.open(io.BytesIO(image_data))
except Exception as e:
print(f"Error decoding image: {e}")
# 4. Inference
if not model_loader.is_ready():
return {"alternatives": ["Model not loaded"], "primary": "Model not loaded", "inference_time_ms": 0, "persona_id": persona_id}
# Ensure correct adapter is loaded
try:
model_loader.load_persona_adapter(persona_id)
except Exception as e:
return {"alternatives": [f"Error loading adapter: {str(e)}"], "primary": "Error loading adapter", "inference_time_ms": 0, "persona_id": persona_id}
try:
# Preparation
if image:
inputs = model_loader.processor(text=prompt, images=image, return_tensors="pt").to(model_loader.model.device)
else:
inputs = model_loader.processor(text=prompt, return_tensors="pt").to(model_loader.model.device)
# Generation
with torch.no_grad():
output_ids = model_loader.model.generate(
**inputs,
max_new_tokens=MAX_NEW_TOKENS,
temperature=TEMPERATURE,
top_k=TOP_K,
top_p=TOP_P,
do_sample=True,
pad_token_id=model_loader.processor.tokenizer.pad_token_id or model_loader.processor.tokenizer.eos_token_id
)
# Decode only the new tokens
input_len = inputs.input_ids.shape[1]
response = model_loader.processor.decode(output_ids[0][input_len:], skip_special_tokens=True)
# 5. Parse results
alternatives = []
# Look for numbered list: 1. Sentence 2. Sentence 3. Sentence
matches = re.findall(r'\d+\.\s*(.+)', response)
if matches:
alternatives = [m.strip() for m in matches[:n_alternatives]]
else:
# Fallback if no numbered list found
alternatives = [line.strip() for line in response.split('\n') if line.strip()][:n_alternatives]
# If still empty, use the whole response as primary
if not alternatives:
alternatives = [response.strip()]
inference_time_ms = (time.time() - start_time) * 1000
return {
"alternatives": alternatives,
"primary": alternatives[0] if alternatives else "",
"inference_time_ms": inference_time_ms,
"persona_id": persona_id
}
except Exception as e:
print(f"Prediction error: {e}")
return {
"alternatives": ["Prediction failed — please try again"],
"primary": "Prediction failed",
"inference_time_ms": (time.time() - start_time) * 1000,
"persona_id": persona_id
}