Spaces:
Running
Running
| """Pass 1 β Extract ComedyMetadata from user input.""" | |
| import json | |
| import re | |
| from typing import Optional | |
| from loguru import logger | |
| from witgym.schemas import ComedyMetadata, fallback_metadata | |
| from witgym.model import generate_text, is_hf_transport_error | |
| from witgym.prompts import COACHING_EXTRACT_PROMPT | |
| EXTRACT_PROMPT = """\ | |
| Analyse the following conversational input and return a JSON object. | |
| INPUT: "{user_input}" | |
| Return ONLY a JSON object with these exact fields (no explanation, no markdown, no code block): | |
| {{ | |
| "surface": "what was literally said in one sentence", | |
| "subtext": "what the speaker actually means or feels", | |
| "archetype": one of ["status_assertion", "self_delusion", "power_inversion", "anxiety_escalation", "social_fail", "misplaced_conf"], | |
| "archetype_confidence": an integer from 1 to 10 (how confident you are in the archetype choice), | |
| "tension_type": one of ["social_embarrass", "existential", "status_threat", "identity_expose", "logic_collapse"], | |
| "power_dynamic": "who has power and who doesn't, one sentence", | |
| "speaker_strategy": "one short phrase describing how the speaker is trying to be perceived (e.g. competent, unbothered, in-control), or null if unclear", | |
| "obvious_response": "the most boring, expected response to this input", | |
| "violation_distance": one of ["mild", "moderate", "sharp"], | |
| "twist_potential": an integer from 1 to 10 rating how much hidden comedy tension is in this input (1=completely flat, 10=extremely rich setup for wit), | |
| "connector": "the specific word or phrase in the input that could mean two different things simultaneously, or null if no such word exists" | |
| }} | |
| Think carefully about the ARCHETYPE β pick the one that most accurately describes the comedy mechanism hiding in this input. | |
| Archetype selection guidance (avoid overusing social_fail β use it only for literal norm violations): | |
| - status_assertion: claiming authority/status/rightness as if saying it makes it true (e.g. "I just got promoted and have no idea what I'm doing" β claiming the title while admitting incompetence) | |
| - misplaced_conf: confident competence claim immediately unsupported by reality (e.g. "my boss called a quick sync that's been going for two hours" β the word "quick" is the mismatch) | |
| - anxiety_escalation: small trigger spun into catastrophe / inevitable doom logic (e.g. "my coworker keeps stealing my lunch" β escalating a minor loss into a power/control spiral) | |
| - social_fail: awkward performance, norm violation, cringe β ONLY when the person did/said something embarrassing in public (e.g. "I waved back at someone who wasn't waving at me" β literal public norm violation) | |
| - power_inversion: low-status person is the only honest/correct one, or the person with formal authority is visibly incompetent (e.g. "my therapist fell asleep during our session" β the helper needs help) | |
| - self_delusion: specifically a self-image story ("I'm fine / I'm great / I'm the best") contradicted by behavior/evidence (e.g. "I'm pretending to understand cryptocurrency at dinner parties" β actively constructing a false expert identity) | |
| CRITICAL: Do NOT use social_fail as a default. Most awkward situations are self_delusion, anxiety_escalation, power_inversion, or misplaced_conf. social_fail requires a public embarrassing action the person actually performed. | |
| If unsure between self_delusion vs social_fail, ask: did the person DO something publicly awkward (social_fail) or are they MAINTAINING a false self-image (self_delusion)? | |
| For twist_potential: score high if the input has self-delusion, status gap, or absurd logic. Score low if it is a neutral factual statement with no tension. | |
| If the input is ONLY a greeting, social opener, or typo-greeting with no described situation, set twist_potential to 1 and subtext to "greeting only β no situational content". Do not invent comedy tension. | |
| For connector: look for a single word or short phrase that carries an expected meaning in context AND a second meaning that reframes the situation. Most inputs will have null. Return null unless a genuine dual-reading exists (e.g. "manage" can mean control people or barely cope; "balance" can mean financial or emotional equilibrium). | |
| Return ONLY the JSON. Nothing else.""" | |
| def extract_comedy_metadata( | |
| user_input: str, | |
| model, | |
| tokenizer, | |
| *, | |
| coaching_context: Optional[tuple[str, str]] = None, | |
| ) -> ComedyMetadata: | |
| """Pass 1: extract comedy metadata. Retries once on parse failure.""" | |
| if coaching_context: | |
| original, follow_up = coaching_context | |
| prompt = COACHING_EXTRACT_PROMPT.format(original=original, follow_up=follow_up) | |
| fallback_key = follow_up | |
| else: | |
| prompt = EXTRACT_PROMPT.format(user_input=user_input) | |
| fallback_key = user_input | |
| for attempt in range(2): | |
| try: | |
| raw = generate_text(prompt, model, tokenizer, config_type="extract") | |
| except Exception as e: | |
| if is_hf_transport_error(e): | |
| logger.warning(f"HF API extract failed ({e}); using fallback_metadata") | |
| return fallback_metadata(fallback_key) | |
| raise | |
| logger.debug(f"Extractor raw output (attempt {attempt + 1}):\n{raw}") | |
| # Strip any accidental markdown fences | |
| cleaned = re.sub(r"```(?:json)?|```", "", raw).strip() | |
| # Find the JSON object | |
| match = re.search(r"\{.*\}", cleaned, re.DOTALL) | |
| if not match: | |
| logger.warning(f"No JSON found in extractor output (attempt {attempt + 1})") | |
| if attempt == 0: | |
| prompt += "\n\nYour previous response contained no valid JSON. Return ONLY the JSON object." | |
| continue | |
| try: | |
| data = json.loads(match.group()) | |
| metadata = ComedyMetadata.model_validate(data) | |
| logger.info(f"Extracted: archetype={metadata.archetype.value}, tension={metadata.tension_type.value}, twist_potential={metadata.twist_potential}") | |
| return metadata | |
| except Exception as e: | |
| logger.warning(f"Parse error (attempt {attempt + 1}): {e}") | |
| if attempt == 0: | |
| prompt += f"\n\nParse error: {e}. Fix and return ONLY valid JSON." | |
| logger.error("Extractor failed twice. Using fallback metadata.") | |
| return fallback_metadata(fallback_key) | |