GTROX / services /coach.py
GT5557's picture
Upload 6 files
284c76e verified
Raw
History Blame Contribute Delete
4.45 kB
"""Qwen-powered speech coaching service."""
import modal
MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct"
CACHE_DIR = "/cache"
UNMEASURED_TERMS = (
"pause",
"breath",
"tone",
"emotion",
"pronunciation",
"emphasis",
"eye contact",
"volume",
)
app = modal.App("gtrox-coach")
model_cache = modal.Volume.from_name("gtrox-model-cache", create_if_missing=True)
image = (
modal.Image.debian_slim(python_version="3.12")
.env({"HF_HOME": CACHE_DIR, "HF_XET_HIGH_PERFORMANCE": "1"})
.uv_pip_install(
"torch==2.7.1",
"transformers==4.53.3",
"accelerate==1.8.1",
"huggingface_hub[hf-xet]==0.33.4",
"fastapi[standard]==0.115.4",
)
)
@app.cls(
gpu="T4",
image=image,
timeout=300,
max_containers=1,
scaledown_window=300,
volumes={CACHE_DIR: model_cache},
)
class SpeechCoach:
@modal.enter()
def setup(self):
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
self.tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
self.model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
torch_dtype=torch.float16,
device_map="auto",
)
@modal.fastapi_endpoint(method="POST")
def coach(self, payload: dict):
import torch
transcript = str(payload.get("transcript", ""))[:8000]
metrics = payload.get("metrics", {})
messages = [
{
"role": "system",
"content": (
"You are a supportive professional speech coach. Give concise, "
"specific advice grounded only in the provided transcript and "
"metrics. Do not invent pause, tone, emotion, or pronunciation "
"observations. Interpret speaking pace as: below 110 WPM is slow, "
"110-180 WPM is conversational, and above 180 WPM is fast. Treat "
"higher filler and repetition density as improvement areas. Never "
"contradict the supplied numbers. You may discuss only measured "
"pace, fillers, repetitions, and the transcript's wording or "
"organization. Never mention pauses, breathing, tone, emotion, "
"pronunciation, emphasis, eye contact, or volume, even as advice. "
"Return markdown with headings: Summary, Strengths, Improve Next, "
"and Practice Exercise. Give no more than three recommendations."
),
},
{
"role": "user",
"content": f"Metrics: {metrics}\n\nTranscript:\n{transcript}",
},
]
prompt = self.tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
inputs = self.tokenizer(prompt, return_tensors="pt").to("cuda")
with torch.inference_mode():
generated = self.model.generate(
**inputs,
max_new_tokens=240,
do_sample=False,
repetition_penalty=1.05,
)
new_tokens = generated[0][inputs["input_ids"].shape[1] :]
coaching = self.tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
coaching = "\n".join(
line
for line in coaching.splitlines()
if not any(term in line.lower() for term in UNMEASURED_TERMS)
).strip()
if len(coaching) < 120:
coaching = (
"## Summary\n"
f"Your measured speaking pace is {metrics.get('wpm', 0)} WPM with a "
f"fluency score of {metrics.get('fluency_score', 0)}/100.\n\n"
"## Strengths\n"
f"- Filler count: {metrics.get('filler_count', 0)}\n"
f"- Immediate repetitions: {metrics.get('repetitions', 0)}\n\n"
"## Improve Next\n"
"- Organize the message around one clear main idea and supporting points.\n\n"
"## Practice Exercise\n"
"- Deliver the same message with an opening, three points, and a conclusion."
)
return {"coaching": coaching, "model": MODEL_NAME}