""" app.py — FutureSelves for Build Small (Gradio Space). Two models, one Space: - MiniCPM 2.5B (~2.5B) — primary LLM for transmission generation - Nemotron-Parse (<1B) — structured note extraction (NVIDIA prize) TTS via Kokoro (82M) — fully local. Targeted prizes (8): Backyard AI, OpenBMB, NVIDIA Nemotron, Tiny Titan, Best Agent, Off Brand, Best Demo, Bonus Quest Champion. """ from __future__ import annotations import json import logging import os import shutil import tempfile import threading import uuid from dataclasses import dataclass, field, asdict from datetime import date from html import escape as html_escape from pathlib import Path from typing import Any, Optional import gradio as gr from transmission import ( CastMember, GenerationContext, GeneratedTransmission, PersonaContext, RecentChoice, RecentResponse, RecentTransmission, build_prompt, fallback_transmission, get_system_prompt, parse_transmission, ) from parse_notes import extract_note_insights, fast_insights from tts import generate_speech, get_voice_for_cast_member from demo.maya import build_maya_demo, MayaDemoBundle from modal_eval import log_agent_trace, summarize_persona_modal, write_modal_app, write_demo_trace from form_helpers import ( PLACEHOLDERS, EXAMPLES, PRIMER_HTML, BUTTON_TEXT, ARC_OPTIONS, ARC_EMOJI, VOICE_EMOJI, chip_html, _memory_cards_html, _word_chips_html, _arc_cards_html, MEMORY_TO_CHOICE_REACTION, ) logger = logging.getLogger(__name__) MODEL_NAME = os.environ.get("LLM_MODEL", "openbmb/MiniCPM-2.5-sft-bf16") CAST_MEMBER_NAMES = { "future_self": ("Your Future Self", "Always transmitting"), "future_partner": ("Future Partner", "Love arc required"), "future_mentor": ("Future Mentor", "7-day streak + toward choices"), "future_best_friend": ("Future Best Friend", "3-day streak + repair"), "shadow": ("The Shadow", "High divergence"), "alternate_self": ("Alternate Self", "14-day streak + drift"), } # ─── Model ─────────────────────────────────────────────────────────────────── _LLM = None _LLM_LOCK = threading.Lock() def _load_llm(): global _LLM if _LLM is not None: return _LLM with _LLM_LOCK: if _LLM is not None: return _LLM import torch from transformers import AutoModelForCausalLM, AutoTokenizer logger.info("Loading MiniCPM: %s", MODEL_NAME) model = AutoModelForCausalLM.from_pretrained( MODEL_NAME, trust_remote_code=True, torch_dtype=torch.float16, device_map="auto", attn_implementation="sdpa", ) model.eval() tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True) _LLM = (model, tokenizer) logger.info("MiniCPM loaded") return _LLM def _generate_with_llm(context: GenerationContext, cast_member: CastMember, local_now: str) -> tuple[GeneratedTransmission, str]: """Generate a transmission and report which path produced it. Returns (transmission, source) where source is one of: - "live" : MiniCPM produced valid JSON we could parse - "parse-fail": MiniCPM ran but its output couldn't be parsed - "llm-error": LLM call raised (OOM, network, missing model) The agent trace records this so judges can audit live vs. fallback. """ try: model, tokenizer = _load_llm() prompt = build_prompt(context, cast_member) system_prompt = get_system_prompt(context.persona.timeline_divergence_score) full = f"{system_prompt}\n\n{prompt}\n\nLocal open time: {local_now}" import torch messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": full}, ] input_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) inputs = tokenizer(input_text, return_tensors="pt").to(model.device) with torch.no_grad(): outputs = model.generate( **inputs, max_new_tokens=700, temperature=0.8, top_p=0.9, do_sample=True, pad_token_id=tokenizer.pad_token_id or tokenizer.eos_token_id, ) decoded = tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True).strip() parsed = parse_transmission(decoded) if parsed: return parsed, "live" return fallback_transmission(context, cast_member), "parse-fail" except Exception as exc: logger.warning("LLM failed: %s", exc) return fallback_transmission(context, cast_member), "llm-error" # ─── State ─────────────────────────────────────────────────────────────────── @dataclass class AppState: persona: Optional[PersonaContext] = None onboarded: bool = False onboard_step: int = 0 checked_in: bool = False check_in_word: str = "" check_in_note: str = "" today_cast: Optional[CastMember] = None today_transmission: Optional[GeneratedTransmission] = None today_audio: str = "" today_choice: str = "" generating: bool = False generation_done: bool = False choice_made: bool = False recent_transmissions: list[RecentTransmission] = field(default_factory=list) recent_choices: list[RecentChoice] = field(default_factory=list) recent_responses: list[RecentResponse] = field(default_factory=list) open_threads: list = field(default_factory=list) def apply_checkin(self, word: str, note: str): if not self.persona: self.persona = _default_persona() self.check_in_word = word.strip()[:40] self.check_in_note = note.strip() if note.strip() else "" self.checked_in = True def start_generation(self): self.generating = True self.generation_done = False self.today_audio = "" self.today_transmission = None self.today_cast = _choose_cast(self) def record_memory(self, memory_value: str, note: str): choice_val, reaction_val = MEMORY_TO_CHOICE_REACTION.get(memory_value, ("toward", "did_it")) if self.today_transmission: self.recent_transmissions.append(RecentTransmission( date_key=date.today().isoformat(), title=self.today_transmission.title or "", cliffhanger=self.today_transmission.cliffhanger or "", cast_member=self.today_cast or "future_self", )) self.recent_choices.append(RecentChoice( date_key=date.today().isoformat(), choice=choice_val, prompt=self.today_transmission.action_prompt if self.today_transmission else "", )) if self.persona: self.persona.streak += 1 setattr(self.persona, f'{choice_val}_count', getattr(self.persona, f'{choice_val}_count', 0) + 1) self.recent_responses.append(RecentResponse( reaction=reaction_val, reply_note=note.strip() if note.strip() else None, )) self.checked_in = False self.generation_done = False self.choice_made = False self.today_transmission = None self.today_audio = "" self.today_choice = "" self.check_in_word = "" self.check_in_note = "" def to_context(self) -> GenerationContext: assert self.persona ci = type("C", (), {"word": self.check_in_word, "note": self.check_in_note or None})() if self.checked_in else None return GenerationContext( persona=self.persona, check_in=ci, recent_transmissions=self.recent_transmissions, recent_choices=self.recent_choices, recent_responses=self.recent_responses, open_threads=self.open_threads, ) def streak(self) -> int: return self.persona.streak if self.persona else 0 def divergence(self) -> int: return self.persona.timeline_divergence_score if self.persona else 0 def to_dict(self) -> dict: d = asdict(self) d["persona"] = asdict(self.persona) if self.persona else None return d @staticmethod def from_dict(d: dict | None) -> AppState: if not d: return AppState() d = {k: v for k, v in d.items() if k in AppState.__dataclass_fields__} if d.get("persona"): d["persona"] = PersonaContext(**{ k: v for k, v in d["persona"].items() if k in PersonaContext.__dataclass_fields__ }) if d.get("recent_transmissions"): d["recent_transmissions"] = [ RecentTransmission(**t) for t in d["recent_transmissions"] ] if d.get("recent_choices"): d["recent_choices"] = [ RecentChoice(**c) for c in d["recent_choices"] ] if d.get("recent_responses"): d["recent_responses"] = [ RecentResponse(**r) for r in d["recent_responses"] ] return AppState(**d) def _default_persona(name: str = "you", city: str = "") -> PersonaContext: return PersonaContext( name=name.strip() or "you", city=city.strip(), current_chapter="this part of your life", primary_arc="purpose", miraculous_year="a year that feels more honest and alive", avoiding="something you keep circling", afraid_wont_happen="the future you still want", draining="carrying too much alone", selected_voice_name="Ember", selected_voice_description="warm, intimate, certain", ) # ─── Choose cast member ────────────────────────────────────────────────────── def _choose_cast(state: AppState) -> CastMember: import random if not state.recent_transmissions: return "future_self" recent = {t.cast_member for t in state.recent_transmissions[-3:]} available = [c for c in CAST_MEMBER_NAMES if c not in recent] if not available: return random.choice(["future_self", "future_partner", "future_mentor"]) return random.choices(available, weights=[3 if c == "future_self" else 2 for c in available], k=1)[0] def _constellation(state: AppState) -> list[tuple[str, str, str, str]]: """Return list of (cast_member, label, state, hint) for grid display.""" s = state.streak() d = state.divergence() p = state.persona results = [] for cm, (label, hint) in CAST_MEMBER_NAMES.items(): if cm == "future_self": results.append((cm, label, "lit", hint)) elif cm == "future_partner" and p and p.primary_arc == "love": results.append((cm, label, "lit" if d < 4 else "dim", hint)) elif cm == "future_mentor" and s >= 7: results.append((cm, label, "lit", hint)) elif cm == "future_best_friend" and s >= 3: results.append((cm, label, "lit", hint)) elif cm == "shadow" and d >= 4: results.append((cm, label, "dim", hint)) elif cm == "alternate_self" and s >= 14: results.append((cm, label, "dim", hint)) else: results.append((cm, label, "locked", hint)) return results # ─── CSS — transmission console ───────────────────────────────────────────── # Design intent: a private future-radio, not a styled Gradio dashboard. # - The container is the signal chamber; everything else orbits it. # - Typography: Fraunces (literary serif) for the chamber voice, IBM Plex Mono # for instrument labels and signal-path readouts. No Inter. # - Surfaces are atmospheric, not bordered cards: layered gradients, faint # scanlines, glow rings, no generic rounded panels. # - Constellation is a horizontal rail of voice orbs, not a card grid. # - Progress is a horizontal signal path with an active pulse, not step pills. CSS = """ /* Fonts are loaded via in create_app() head — more reliable than CSS @import in an HF Space iframe, and async/non-blocking. */ :root{ --ink:#0a0c1a; --ink-2:#0e1124; --ink-3:#141a30; --paper:#f6efdc; --paper-dim:#d8d0bd; --paper-mute:#9c9485; --amber:#e9a847; --amber-soft:#c4842d; --amber-glow:rgba(233,168,71,.45); --signal:rgba(233,168,71,.18); --violet:#7a6cc7; --violet-soft:rgba(122,108,199,.22); --live:#7adf9b; --locked:rgba(246,239,220,.16); --line:rgba(246,239,220,.10); --line-2:rgba(246,239,220,.05); } *{box-sizing:border-box} html,body{margin:0;padding:0;background:var(--ink);color:var(--paper);} body{ font-family:'IBM Plex Mono','SF Mono',ui-monospace,monospace; font-size:14px; line-height:1.55; background: radial-gradient(ellipse 80% 50% at 50% -10%,rgba(233,168,71,.08),transparent 60%), radial-gradient(ellipse 60% 50% at 50% 110%,rgba(122,108,199,.06),transparent 60%), linear-gradient(180deg,var(--ink) 0%,var(--ink-2) 60%,var(--ink) 100%); background-attachment:fixed; min-height:100vh; -webkit-font-smoothing:antialiased; letter-spacing:.01em; } body::before{ content:''; position:fixed;inset:0; background-image:repeating-linear-gradient(0deg,transparent 0,transparent 2px,rgba(246,239,220,.012) 2px,rgba(246,239,220,.012) 3px); pointer-events:none; z-index:1; } body::after{ content:''; position:fixed;inset:0; background-image:radial-gradient(circle at 20% 30%,rgba(246,239,220,.015) 0,transparent 50%), radial-gradient(circle at 80% 70%,rgba(233,168,71,.025) 0,transparent 50%); pointer-events:none; z-index:1; } ::selection{background:var(--amber-glow);color:var(--ink);} /* ─── Gradio chrome — quiet it down ─── */ .gradio-container{ max-width:780px !important; margin:0 auto; padding:0 !important; position:relative; z-index:2; font-family:'IBM Plex Mono',monospace !important; background:transparent !important; } footer, .gradio-container > .footer, .gradio-container > div > .footer {display:none !important} .app, .wrap, .panel, .container, .gap, .form, .panel-body {background:transparent !important;border:none !important;box-shadow:none !important;} /* ─── Identity strip ─── */ .identity-strip{ display:flex;align-items:center;justify-content:space-between;gap:16px; padding:18px 20px 14px; border-bottom:1px solid var(--line-2); position:relative; } .identity-strip::after{ content:'';position:absolute;left:20px;right:20px;bottom:-1px;height:1px; background:linear-gradient(90deg,transparent,var(--amber) 50%,transparent); opacity:.35; } .brand-mark{ display:flex;align-items:center;gap:10px; font-family:'IBM Plex Mono',monospace; font-size:11px;letter-spacing:.18em;text-transform:uppercase; color:var(--paper); } .brand-mark .glyph{ display:inline-block;width:14px;height:14px; border:1px solid var(--amber);border-radius:50%; position:relative; box-shadow:0 0 14px var(--amber-glow); } .brand-mark .glyph::after{ content:'';position:absolute;inset:3px;border-radius:50%; background:var(--amber); box-shadow:0 0 6px var(--amber); animation:glow 2.4s ease-in-out infinite; } .brand-mark .name{color:var(--paper);font-weight:500} .brand-mark .sub{color:var(--paper-mute);font-weight:300;margin-left:6px;letter-spacing:.22em} .instrument-cluster{display:flex;gap:14px;align-items:center} .instrument{ display:flex;flex-direction:column;align-items:flex-end;gap:1px; font-family:'IBM Plex Mono',monospace; border-left:1px solid var(--line-2); padding-left:12px; } .instrument:first-child{border-left:none;padding-left:0} .instrument .label{font-size:9px;letter-spacing:.18em;text-transform:uppercase;color:var(--paper-mute)} .instrument .value{font-size:13px;color:var(--amber);font-variant-numeric:tabular-nums;letter-spacing:.04em} .instrument .value .unit{color:var(--paper-mute);font-size:10px;margin-left:2px} .privacy-pulse{ display:inline-flex;align-items:center;gap:6px; font-size:9px;letter-spacing:.2em;text-transform:uppercase; color:var(--live); } .privacy-pulse .dot{width:6px;height:6px;border-radius:50%;background:var(--live);box-shadow:0 0 8px var(--live);animation:glow 2s ease-in-out infinite} /* ─── Signal chamber — the centerpiece ─── */ .signal-chamber{ position:relative; margin:24px 20px 0; padding:38px 28px 30px; border-radius:4px; background: radial-gradient(ellipse 100% 70% at 50% 0%,rgba(233,168,71,.06),transparent 60%), linear-gradient(180deg,rgba(20,26,48,.65) 0%,rgba(14,17,36,.7) 100%); border:1px solid rgba(233,168,71,.18); overflow:hidden; isolation:isolate; } .signal-chamber::before{ content:'';position:absolute;inset:-1px;border-radius:4px; background:linear-gradient(180deg,rgba(233,168,71,.4),transparent 30%,transparent 70%,rgba(122,108,199,.25)); -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0); -webkit-mask-composite:xor;mask-composite:exclude; padding:1px;pointer-events:none;z-index:0; } .signal-chamber::after{ content:'';position:absolute;inset:0; background-image:repeating-linear-gradient(0deg,transparent 0,transparent 3px,rgba(246,239,220,.014) 3px,rgba(246,239,220,.014) 4px); pointer-events:none;z-index:0; } .chamber-content{position:relative;z-index:1} .chamber-eyebrow{ display:flex;align-items:center;gap:10px; font-size:10px;letter-spacing:.28em;text-transform:uppercase; color:var(--amber); margin-bottom:16px; } .chamber-eyebrow .pulse{ width:6px;height:6px;border-radius:50%;background:var(--amber); box-shadow:0 0 10px var(--amber);animation:glow 1.6s ease-in-out infinite; } .chamber-eyebrow .sep{color:var(--paper-mute);opacity:.5} .chamber-title{ font-family:'Fraunces','Times New Roman',serif; font-weight:500; font-size:34px;line-height:1.1;letter-spacing:-.015em; color:var(--paper); margin:0 0 8px; } .chamber-title em{font-style:italic;color:var(--amber);font-weight:400} .chamber-body{ font-family:'Fraunces',serif; font-size:17px;line-height:1.7; font-weight:300; color:var(--paper-dim); } .chamber-body em{color:var(--paper);font-style:italic} .chamber-body strong{color:var(--amber);font-weight:500;font-family:'IBM Plex Mono',monospace;font-size:14px;letter-spacing:.04em} .chamber-divider{ height:1px;margin:24px 0 18px; background:linear-gradient(90deg,transparent,var(--line) 20%,var(--line) 80%,transparent); } .chamber-meta-row{ display:flex;flex-wrap:wrap;gap:18px; font-size:10px;letter-spacing:.18em;text-transform:uppercase; color:var(--paper-mute); } .chamber-meta-row .meta-key{color:var(--paper-mute)} .chamber-meta-row .meta-val{color:var(--paper);font-weight:500;margin-left:6px} .chamber-callout{ margin-top:20px;padding:14px 16px; border-left:2px solid var(--amber); background:linear-gradient(90deg,rgba(233,168,71,.06),transparent); font-family:'Fraunces',serif;font-style:italic;font-size:15px;line-height:1.6; color:var(--paper); } .chamber-callout .tag{ display:block; font-family:'IBM Plex Mono',monospace;font-style:normal;font-size:9px;letter-spacing:.22em; text-transform:uppercase;color:var(--amber); margin-bottom:6px; } .chamber-actions{ display:flex;flex-direction:column;gap:14px; margin-top:24px; } .chamber-section-label{ font-size:9px;letter-spacing:.28em;text-transform:uppercase; color:var(--paper-mute); margin-bottom:8px; } /* Tuning sweep state */ .tuning-display{ display:flex;flex-direction:column;align-items:center;gap:18px; padding:14px 0 6px; } .tuning-svg{width:280px;height:90px;display:block} .tuning-svg .wave{fill:none;stroke:var(--amber);stroke-width:1.5;stroke-linecap:round;filter:drop-shadow(0 0 4px var(--amber))} .tuning-svg .wave-back{stroke:rgba(233,168,71,.18);animation:drift 3s ease-in-out infinite} .tuning-svg .wave-front{animation:drift 2.2s ease-in-out infinite reverse} .tuning-readout{ font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:.18em; text-transform:uppercase;color:var(--paper-mute); display:flex;gap:18px;flex-wrap:wrap;justify-content:center; } .tuning-readout .v{color:var(--amber);font-weight:500} .tuning-readout .v.live::before{content:'●';margin-right:6px;color:var(--amber);animation:blink 1.2s ease-in-out infinite} /* ─── Constellation rail (voice orbs) ─── */ .constellation-rail{ margin:24px 20px 0; padding:14px 0 4px; border-top:1px solid var(--line-2); } .rail-head{ display:flex;align-items:baseline;justify-content:space-between; padding:0 4px 10px; } .rail-head .rail-title{ font-size:10px;letter-spacing:.28em;text-transform:uppercase; color:var(--paper); } .rail-head .rail-title em{color:var(--amber);font-style:normal;font-weight:500} .rail-head .rail-status{font-size:10px;letter-spacing:.18em;color:var(--paper-mute);text-transform:uppercase} .voice-rail{ display:flex;gap:8px;overflow-x:auto; scrollbar-width:thin;scrollbar-color:var(--line) transparent; padding:6px 4px 14px; } .voice-rail::-webkit-scrollbar{height:4px} .voice-rail::-webkit-scrollbar-thumb{background:var(--line);border-radius:2px} .voice-orb{ flex:0 0 auto; display:flex;flex-direction:column;align-items:center;gap:7px; min-width:78px;padding:10px 6px; border-radius:4px; background:rgba(20,26,48,.4); border:1px solid var(--line); transition:all .35s cubic-bezier(.4,0,.2,1); position:relative; } .voice-orb.lit{background:linear-gradient(180deg,rgba(233,168,71,.10),rgba(20,26,48,.55));border-color:rgba(233,168,71,.45)} .voice-orb.dim{background:linear-gradient(180deg,rgba(122,108,199,.08),rgba(20,26,48,.55));border-color:rgba(122,108,199,.35)} .voice-orb.locked{opacity:.42;filter:saturate(.4)} .voice-orb .orb{ width:38px;height:38px;border-radius:50%; display:flex;align-items:center;justify-content:center; background:rgba(20,26,48,.7); border:1px solid var(--line); position:relative; font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--paper-mute); letter-spacing:.04em; } .voice-orb.lit .orb{ background:radial-gradient(circle at 30% 30%,rgba(233,168,71,.4),rgba(233,168,71,.05)); border-color:rgba(233,168,71,.6); color:var(--amber); box-shadow:0 0 18px rgba(233,168,71,.35),inset 0 0 12px rgba(233,168,71,.2); } .voice-orb.lit .orb::after{ content:'';position:absolute;inset:-4px;border-radius:50%; border:1px solid rgba(233,168,71,.3); animation:ripple 2.2s ease-out infinite; } .voice-orb.dim .orb{ background:radial-gradient(circle at 30% 30%,rgba(122,108,199,.4),rgba(122,108,199,.05)); border-color:rgba(122,108,199,.55); color:var(--violet); } .voice-orb .orb-label{font-size:10px;letter-spacing:.04em;color:var(--paper-dim);text-align:center;line-height:1.2;max-width:78px} .voice-orb .orb-state{ position:absolute;top:6px;right:6px; font-size:8px;letter-spacing:.18em;text-transform:uppercase;color:var(--paper-mute); } .voice-orb.lit .orb-state{color:var(--amber)} .voice-orb.dim .orb-state{color:var(--violet)} .voice-orb.locked{cursor:help} /* ─── Signal path (timeline progress) ─── */ .signal-path{ margin:28px 20px 0; padding-top:18px; border-top:1px solid var(--line-2); } .path-head{ display:flex;align-items:baseline;justify-content:space-between; font-size:10px;letter-spacing:.22em;text-transform:uppercase; color:var(--paper-mute); margin-bottom:14px; } .path-head .now{color:var(--amber)} .path-track{ position:relative;height:34px;display:flex;align-items:center; } .path-line{ position:absolute;left:0;right:0;top:50%;transform:translateY(-50%); height:1px;background:var(--line); } .path-line.lit{ background:linear-gradient(90deg,var(--amber),var(--violet)); box-shadow:0 0 8px rgba(233,168,71,.5); } .path-nodes{display:flex;justify-content:space-between;width:100%;position:relative} .path-node{ display:flex;flex-direction:column;align-items:center;gap:6px; font-size:9px;letter-spacing:.18em;text-transform:uppercase; color:var(--paper-mute); } .path-node .dot{ width:9px;height:9px;border-radius:50%; background:var(--ink);border:1px solid var(--line); transition:all .3s ease; } .path-node.done .dot{background:var(--amber);border-color:var(--amber);box-shadow:0 0 8px var(--amber-glow)} .path-node.now .dot{ background:var(--paper);border-color:var(--amber); box-shadow:0 0 0 3px rgba(233,168,71,.2),0 0 14px var(--amber); animation:pulse 1.6s ease-in-out infinite; } .path-node.now{color:var(--amber);font-weight:500} .path-node.done{color:var(--paper-dim)} /* ─── Memory log (history) ─── */ .memory-log{margin:24px 20px 0;padding-top:18px;border-top:1px solid var(--line-2)} .memory-log .log-head{font-size:10px;letter-spacing:.28em;text-transform:uppercase;color:var(--paper);margin-bottom:14px} .memory-log .log-head em{color:var(--amber);font-style:normal;font-weight:500} .memory-row{ display:grid;grid-template-columns:90px 1fr auto;gap:14px;align-items:baseline; padding:12px 0; border-bottom:1px dashed var(--line-2); } .memory-row:last-child{border-bottom:none} .memory-row .ts{font-size:10px;letter-spacing:.12em;color:var(--paper-mute);font-variant-numeric:tabular-nums} .memory-row .body{font-family:'Fraunces',serif;font-size:14px;line-height:1.5;color:var(--paper-dim)} .memory-row .body em{color:var(--paper);font-style:italic} .memory-row .tag{ font-size:9px;letter-spacing:.2em;text-transform:uppercase; color:var(--amber);padding:3px 8px;border:1px solid var(--line);border-radius:2px; white-space:nowrap; } .memory-row .tag.choice-toward{color:var(--live);border-color:rgba(122,223,155,.4)} .memory-row .tag.choice-steady{color:var(--amber);border-color:rgba(233,168,71,.4)} .memory-row .tag.choice-release{color:var(--violet);border-color:rgba(122,108,199,.4)} .memory-row .tag.choice-repair{color:#c98ad1;border-color:rgba(201,138,209,.4)} /* Audio play chip beside memory-log tags — only renders when the transmission has a pre-rendered voice sample. Hover for play hint. */ .audio-chip{ display:inline-flex;align-items:center;justify-content:center; width:18px;height:18px;margin-left:8px; border-radius:50%; background:rgba(233,168,71,.12); color:var(--amber); font-size:9px; text-decoration:none; border:1px solid rgba(233,168,71,.4); transition:all .2s ease; vertical-align:middle; cursor:pointer; } .audio-chip:hover{ background:rgba(233,168,71,.25); color:var(--paper); border-color:var(--amber); transform:scale(1.1); text-decoration:none; } /* Demo button — softer than the primary, lives below the onboarding step 1 inputs. The amber dot in the label is the only visual signal that this is a curated showcase, not a regular action. */ button.demo-btn, button.demo-btn.lg, .gr-button.demo-btn{ background:transparent !important; border:1px solid var(--line) !important; color:var(--paper-dim) !important; font-size:10px !important; letter-spacing:.18em !important; padding:8px 14px !important; margin-top:6px !important; } button.demo-btn:hover, .gr-button.demo-btn:hover{ border-color:var(--amber) !important; color:var(--amber) !important; background:rgba(233,168,71,.06) !important; } /* Share button — copy-to-clipboard for the transmission. Lives beneath the audio module. Smaller, quieter than the chamber action buttons, but visibly interactive. */ .share-btn{ background:transparent; border:1px solid var(--line); color:var(--amber); font-family:'IBM Plex Mono',monospace; font-size:10px; letter-spacing:.18em; text-transform:uppercase; padding:8px 14px; border-radius:2px; cursor:pointer; transition:all .2s ease; } .share-btn:hover{ border-color:var(--amber); background:rgba(233,168,71,.08); transform:translateY(-1px); } .share-btn:active{ transform:translateY(0); background:rgba(233,168,71,.15); } /* ─── Footer status line ─── */ .signal-footer{ margin-top:32px;padding:18px 20px 28px; border-top:1px solid var(--line-2); display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap; font-size:9px;letter-spacing:.22em;text-transform:uppercase;color:var(--paper-mute); } .signal-footer .freq{color:var(--amber);font-variant-numeric:tabular-nums} .signal-footer .live-line{display:flex;gap:14px;align-items:center} .signal-footer .live-line .dot{width:6px;height:6px;border-radius:50%;background:var(--live);box-shadow:0 0 8px var(--live);animation:glow 1.6s ease-in-out infinite} /* ─── Animations ─── */ @keyframes glow{0%,100%{opacity:.55;transform:scale(1)}50%{opacity:1;transform:scale(1.25)}} @keyframes pulse{0%,100%{opacity:.7}50%{opacity:1}} @keyframes ripple{0%{transform:scale(1);opacity:.55}100%{transform:scale(1.6);opacity:0}} @keyframes drift{0%,100%{transform:translateX(0)}50%{transform:translateX(6px)}} @keyframes blink{0%,49%{opacity:1}50%,100%{opacity:.25}} @keyframes fadeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}} .fade-in{animation:fadeIn .55s ease both} /* ─── Gradio form controls — instrument them, don't card them ─── */ .gr-button{ border-radius:2px !important; font-family:'IBM Plex Mono',monospace !important; font-weight:500 !important;letter-spacing:.14em !important; text-transform:uppercase !important; font-size:11px !important; padding:10px 18px !important; transition:all .25s cubic-bezier(.4,0,.2,1) !important; border:1px solid var(--line) !important; background:rgba(20,26,48,.5) !important; color:var(--paper) !important; } .gr-button:hover{ border-color:var(--amber) !important; background:rgba(233,168,71,.08) !important; color:var(--amber) !important; transform:translateY(-1px); } .gr-button-primary{ background:linear-gradient(180deg,var(--amber),var(--amber-soft)) !important; border:1px solid var(--amber) !important; color:var(--ink) !important; box-shadow:0 0 0 1px rgba(233,168,71,.25),0 6px 24px -8px var(--amber-glow) !important; position:relative;overflow:hidden; } .gr-button-primary:hover{ background:linear-gradient(180deg,#f3b357,var(--amber)) !important; color:var(--ink) !important; border-color:var(--amber) !important; box-shadow:0 0 0 1px rgba(233,168,71,.4),0 8px 30px -6px var(--amber-glow) !important; transform:translateY(-1px); } .gr-button-primary::after{ content:'';position:absolute;inset:0; background:linear-gradient(90deg,transparent,rgba(255,255,255,.25),transparent); transform:translateX(-100%);transition:transform .8s; } .gr-button-primary:hover::after{transform:translateX(100%)} .gr-input, .gr-textarea, .gr-box, .gr-form, .gr-input-wrap, .gr-textarea-wrap, .gr-panel, .gr-block, .gr-component{ background:rgba(20,26,48,.55) !important; border:1px solid var(--line) !important; border-radius:2px !important; color:var(--paper) !important; font-family:'IBM Plex Mono',monospace !important; font-size:13px !important; box-shadow:none !important; } .gr-input:focus, .gr-textarea:focus{ border-color:var(--amber) !important; box-shadow:0 0 0 1px var(--amber-glow) !important; outline:none !important; } .gr-input::placeholder, .gr-textarea::placeholder{color:var(--paper-mute) !important;font-style:italic} label, .gr-label, .gr-form-label{ font-family:'IBM Plex Mono',monospace !important; font-size:9px !important;letter-spacing:.22em !important;text-transform:uppercase !important; color:var(--paper-mute) !important; margin-bottom:4px !important; } .radio-group, .gr-radio, .gr-radio-wrap{ background:transparent !important;border:none !important;padding:0 !important; } .gr-radio-label, .gr-radio .label{ font-family:'IBM Plex Mono',monospace !important;font-size:12px !important; color:var(--paper-dim) !important; } /* Tabs are nearly invisible — this is one signal chamber, not a dashboard */ .tab-nav, .tabitem, .tabs, .tab-wrapper{ background:transparent !important; border:none !important; border-bottom:1px solid var(--line-2) !important; border-radius:0 !important; margin:0 20px !important; padding:0 !important; } .tab-nav button, .tabitem button{ background:transparent !important;border:none !important; font-family:'IBM Plex Mono',monospace !important; font-size:10px !important;letter-spacing:.22em !important;text-transform:uppercase !important; color:var(--paper-mute) !important; padding:14px 4px !important;margin-right:24px !important; border-bottom:1px solid transparent !important; transition:color .3s,border-color .3s !important; } .tab-nav button:hover, .tabitem button:hover{color:var(--paper) !important} .tab-nav button.selected, .tabitem button.selected{ color:var(--amber) !important; border-bottom-color:var(--amber) !important; font-weight:500 !important; } h1,h2,h3,h4{ font-family:'Fraunces',serif !important; font-weight:500 !important;letter-spacing:-.01em !important; color:var(--paper) !important; } h3{font-size:18px !important;margin:0 0 6px !important;} audio{width:100%;margin:6px 0;border-radius:2px;filter:invert(.92) hue-rotate(180deg) saturate(.6)} audio::-webkit-media-controls-panel{background:var(--ink-3);} .column, .row, .group, .form{background:transparent !important;border:none !important;box-shadow:none !important;gap:10px !important;} /* Status text on the awaiting/choice-result screens */ .choice-outcome{ margin-top:18px;padding:14px 18px; border-left:2px solid var(--live); background:linear-gradient(90deg,rgba(122,223,155,.08),transparent); font-family:'Fraunces',serif;font-style:italic;font-size:16px;line-height:1.6; color:var(--paper); } .choice-outcome .tag{ display:block;font-family:'IBM Plex Mono',monospace;font-style:normal; font-size:9px;letter-spacing:.22em;text-transform:uppercase;color:var(--live); margin-bottom:6px; } /* Unlock toast — appears after voice unlock */ .unlock-toast{ display:block; margin-top:10px; padding:8px 10px; background:linear-gradient(90deg,rgba(233,168,71,.12),rgba(122,108,199,.08)); border:1px solid var(--amber); border-radius:2px; font-size:10px; letter-spacing:.12em; text-transform:uppercase; color:var(--paper); font-family:'IBM Plex Mono',monospace; animation:fadeIn .5s ease both; } .unlock-toast .tag{ display:inline; color:var(--amber); font-size:9px; letter-spacing:.18em; text-transform:uppercase; margin-right:6px; } .timeline-shift{ display:flex;gap:10px;align-items:stretch;margin-top:18px; } .timeline-shift .stem{ flex:1;display:flex;flex-direction:column;align-items:center;gap:6px; padding:14px 8px; border:1px dashed var(--line);border-radius:2px; font-size:9px;letter-spacing:.18em;text-transform:uppercase;color:var(--paper-mute); } .timeline-shift .stem .v{font-size:18px;color:var(--paper);font-family:'Fraunces',serif;font-weight:500} .timeline-shift .stem .delta{color:var(--amber)} /* Architecture display */ .arch-pane{ margin:18px 20px 0;padding:20px; background:rgba(14,17,36,.55);border:1px solid var(--line-2);border-radius:2px; font-family:'IBM Plex Mono',monospace;font-size:12px;line-height:1.7;color:var(--paper-dim); white-space:pre-wrap;overflow-x:auto; } .arch-pane .arch-h{ color:var(--amber);font-size:9px;letter-spacing:.24em;text-transform:uppercase; border-bottom:1px solid var(--line-2);padding-bottom:8px;margin-bottom:12px; } .arch-pane .k{color:var(--paper-mute)} .arch-pane .v{color:var(--amber)} .arch-pane a{color:var(--amber);text-decoration:none;border-bottom:1px dotted rgba(233,168,71,.4)} .arch-pane a:hover{border-bottom-style:solid} /* ─── Mobile responsive ─── */ @media (max-width: 720px){ .identity-strip{flex-direction:column;align-items:flex-start;gap:12px;padding:14px 16px 12px} .instrument-cluster{flex-wrap:wrap;gap:10px 14px;width:100%;justify-content:space-between} .instrument{padding-left:0;border-left:none} .instrument:not(:last-child){padding-right:14px;border-right:1px solid var(--line-2)} .signal-chamber{margin:18px 14px 0;padding:28px 18px 22px} .chamber-title{font-size:26px;line-height:1.12} .chamber-body{font-size:16px;line-height:1.65} .constellation-rail{margin:18px 14px 0} .signal-path{margin:22px 14px 0} .memory-log{margin:18px 14px 0} .signal-footer{padding:14px 16px 22px;flex-direction:column;align-items:flex-start;gap:6px} .gradio-container{max-width:none !important} .tab-nav, .tabitem, .tabs, .tab-wrapper{margin:0 14px !important} .tab-nav button, .tabitem button{margin-right:14px !important;padding:12px 2px !important} .path-node{font-size:8px;letter-spacing:.12em} .path-node .dot{width:8px;height:8px} .voice-orb{min-width:70px;padding:8px 4px} .voice-orb .orb{width:34px;height:34px;font-size:10px} .tuning-svg{width:100%;max-width:280px;height:70px} .chamber-callout{font-size:14px} .chamber-eyebrow{font-size:9px;letter-spacing:.22em} } @media (max-width: 480px){ .chamber-title{font-size:22px} .chamber-body{font-size:15px} .identity-strip{padding:12px 14px 10px} .brand-mark{font-size:10px} .instrument .value{font-size:12px} .instrument .label{font-size:8px} .privacy-pulse{font-size:8px} } @media (prefers-reduced-motion: reduce){ *,*::before,*::after{animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important} } /* ─── Example chips — inline phrase buttons ─── */ .ex-chip-row{ display:flex;flex-wrap:wrap;gap:5px; margin:3px 0 8px; } .ex-chip{ background:transparent; border:1px solid var(--line); border-radius:10px; color:var(--paper-mute); font-family:'IBM Plex Mono',monospace; font-size:10px; font-style:italic; padding:3px 10px; cursor:pointer; transition:all .2s ease; white-space:nowrap; } .ex-chip:hover{ border-color:var(--amber); color:var(--amber); background:rgba(233,168,71,.08); } .ex-chip.used{ color:var(--live); border-color:rgba(122,223,155,.5); font-style:normal; } /* ─── Choice/reaction cards ─── */ .card-grid{ display:grid; gap:8px; margin:6px 0; } .choice-grid{grid-template-columns:repeat(2,1fr)} .reaction-grid{grid-template-columns:repeat(2,1fr)} .arc-grid{grid-template-columns:repeat(2,1fr)} .memory-grid{grid-template-columns:repeat(2,1fr)} .choice-card, .reaction-card, .arc-card, .memory-card{ display:flex;flex-direction:column;align-items:flex-start;gap:3px; padding:12px 14px; border:1px solid var(--line); border-radius:3px; background:rgba(20,26,48,.4); cursor:pointer; transition:all .25s cubic-bezier(.4,0,.2,1); text-align:left; font-family:'IBM Plex Mono',monospace; animation:fadeSlideIn .35s ease-out backwards; } .arc-card:nth-child(1){animation-delay:.05s} .arc-card:nth-child(2){animation-delay:.1s} .arc-card:nth-child(3){animation-delay:.15s} .arc-card:nth-child(4){animation-delay:.2s} @keyframes fadeSlideIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}} /* Transmission entrance animation */ .transmission-card{animation:fadeSlideIn .5s ease-out backwards} /* Audio player glow when playing */ audio.playing{animation:pulseGlow 1.5s ease-in-out infinite} @keyframes pulseGlow{0%,100%{box-shadow:0 0 0 0 rgba(233,168,71,0)}50%{box-shadow:0 0 12px 2px rgba(233,168,71,.4)}} /* Memory log staggered entrance */ .memory-entry{animation:fadeSlideIn .35s ease-out backwards} .memory-entry:nth-child(1){animation-delay:.05s} .memory-entry:nth-child(2){animation-delay:.1s} .memory-entry:nth-child(3){animation-delay:.15s} .memory-entry:nth-child(4){animation-delay:.2s} /* Tonight's move callout glow */ .tonight-card{transition:all .3s ease} .tonight-card:hover{box-shadow:0 0 0 1px var(--amber),0 0 16px -4px var(--amber-glow)} /* Constellation rail hover sparkle */ .voice-orb{transition:all .25s ease} .voice-orb:hover{transform:scale(1.15);filter:brightness(1.3)} .voice-orb:hover::after{content:'';position:absolute;width:100%;height:100%;top:0;left:0;background:radial-gradient(circle,rgba(255,255,255,.4) 0%,transparent 70%);animation:sparkle .6s ease-out} /* Loading skeleton shimmer */ .skeleton{background:linear-gradient(90deg,rgba(30,40,60,.3) 25%,rgba(50,65,85,.5) 50%,rgba(30,40,60,.3) 75%);background-size:200% 100%;animation:shimmer 1.5s infinite} @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}} /* Share toast */ .toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:var(--amber);color:#000;padding:8px 16px;border-radius:4px;font-size:12px;opacity:0;transition:opacity .3s;z-index:9999} .toast.show{opacity:1} .choice-card:hover, .reaction-card:hover, .arc-card:hover, .memory-card:hover{ border-color:var(--amber); background:linear-gradient(135deg,rgba(233,168,71,.08),rgba(20,26,48,.55)); } .choice-card.active, .reaction-card.active, .arc-card.active, .memory-card.active{ border-color:var(--amber); background:linear-gradient(135deg,rgba(233,168,71,.12),rgba(20,26,48,.55)); box-shadow:0 0 0 1px rgba(233,168,71,.3),0 0 14px -4px var(--amber-glow); } .card-emoji{ font-size:16px; line-height:1; margin-bottom:2px; } .card-label{ font-size:12px; font-weight:500; letter-spacing:.06em; color:var(--paper); } .card-sub{ font-size:9px; color:var(--paper-mute); letter-spacing:.04em; font-style:italic; } /* ─── Hidden radio — visually hidden but DOM-present for JS interactions ─── */ .hidden-radio{ position:absolute !important; width:1px !important; height:1px !important; padding:0 !important; margin:-1px !important; overflow:hidden !important; clip:rect(0,0,0,0) !important; white-space:nowrap !important; border:0 !important; opacity:0 !important; pointer-events:none !important; } .hidden-radio .gr-form-label, .hidden-radio label{display:none !important} /* ─── Section form — seamlessly floats with the chamber ─── */ .section-form{ margin:8px 20px 0; padding:2px 0; border:none; background:transparent; } .section-form .gr-box, .section-form .gr-form, .section-form .gr-panel{ background:transparent !important; border:none !important; padding:0 !important; margin:0 !important; } /* ─── Accordion primer — permission/guidance text ─── */ .acc-primer{ font-size:10px; line-height:1.5; color:var(--paper-mute); font-style:italic; margin-bottom:8px; } /* ─── Onboarding primer — warmup before the form ─── */ .onboard-primer{ margin-bottom:18px; } .primer-eyebrow{ font-size:9px; letter-spacing:.28em; text-transform:uppercase; color:var(--paper-mute); margin-bottom:8px; } .primer-line{ font-family:'Fraunces',serif; font-size:15px; line-height:1.7; color:var(--paper-dim); font-style:italic; border-left:2px solid var(--line); padding-left:14px; } @media (max-width: 720px){ .choice-grid, .reaction-grid, .arc-grid{grid-template-columns:1fr} }""" # ─── Render helpers — transmission console primitives ─────────────────────── # Every renderer composes from these. No generic bordered card helper. def _identity_strip(state: AppState) -> str: """Compact top bar — brand + privacy + instrument readouts (streak, divergence, choices).""" return f"""