# 12 ANGRY AGENTS - Product Requirements Document ## Overview **Concept**: AI-powered jury deliberation simulation where 11 AI agents + 1 human player debate real criminal cases. A Judge narrator (ElevenLabs) orchestrates the experience. **Track**: MCP in Action - Creative (potentially also Consumer) **Core Value Prop**: True autonomous agent behavior - AI jurors reason, argue, persuade, and change their minds based on deliberation. --- ## Sponsor Integration | Sponsor | Prize | Integration | Priority | |---------|-------|-------------|----------| | LlamaIndex | $1,000 | Case database RAG | HIGH | | ElevenLabs | Airpods + $2K | Judge narrator voice | HIGH | | Blaxel | $2,500 | Sandboxed agent execution | MEDIUM | | Modal | $2,500 | Agent compute | MEDIUM | | Gemini | $10K credits | Agent reasoning | HIGH | --- ## User Experience Flow ``` 1. CASE PRESENTATION └─> Judge (ElevenLabs) narrates case summary └─> Evidence displayed via LlamaIndex RAG └─> Player reads case file 2. SIDE SELECTION └─> Player chooses: DEFEND (not guilty) or PROSECUTE (guilty) └─> Player commits - cannot change 3. INITIAL VOTE └─> All 12 jurors vote (randomized split based on case) └─> Vote tally shown: e.g., "7-5 GUILTY" 4. DELIBERATION LOOP └─> Random 1-4 agents speak per round └─> Player gets turn (choose strategy → AI crafts argument) └─> Conviction scores shift based on arguments └─> Votes may flip └─> Repeat until: votes stabilize OR player calls vote 5. FINAL VERDICT └─> Judge announces verdict (ElevenLabs) └─> Deliberation transcript available └─> No "win/lose" - just the experience ``` --- ## Technical Architecture ### System Overview ``` ┌─────────────────────────────────────────────────────────────────────┐ │ 12 ANGRY AGENTS │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ GRADIO UI LAYER │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ Jury Box │ │ Chat View │ │ Case File │ │ │ │ │ │ (12 seats) │ │ (dialogue) │ │ (evidence) │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ ORCHESTRATOR AGENT │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ │ GameStateManager │ │ │ │ │ │ - current_phase: presentation|deliberation|verdict │ │ │ │ │ │ - round_number: int │ │ │ │ │ │ - votes: Dict[agent_id, "guilty"|"not_guilty"] │ │ │ │ │ │ - conviction_scores: Dict[agent_id, float] │ │ │ │ │ │ - speaking_queue: List[agent_id] │ │ │ │ │ │ - deliberation_log: List[Turn] │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ │ TurnManager │ │ │ │ │ │ - select_speakers(1-4 random) │ │ │ │ │ │ - check_vote_stability() │ │ │ │ │ │ - process_vote_changes() │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌────────────────────┼────────────────────┐ │ │ ▼ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────────┐ ┌─────────────┐ │ │ │ JUDGE │ │ JUROR AGENTS │ │ PLAYER │ │ │ │ AGENT │ │ (11 total) │ │ AGENT │ │ │ │ │ │ │ │ │ │ │ │ ElevenLabs │ │ ┌─────────────┐ │ │ Hybrid I/O │ │ │ │ TTS Output │ │ │ AgentConfig │ │ │ Strategy │ │ │ │ │ │ │ - persona │ │ │ Selection │ │ │ │ Narration │ │ │ - model │ │ │ │ │ │ │ Verdicts │ │ │ - tools[] │ │ │ Argument │ │ │ │ Summaries │ │ │ - memory │ │ │ Crafting │ │ │ └─────────────┘ │ └─────────────┘ │ └─────────────┘ │ │ │ │ │ │ │ ┌─────────────┐ │ │ │ │ │ JurorMemory │ │ │ │ │ │ - case_view │ │ │ │ │ │ - arguments │ │ │ │ │ │ - reactions │ │ │ │ │ │ - conviction│ │ │ │ │ └─────────────┘ │ │ │ └─────────────────┘ │ │ │ │ │ ┌────────────────────┼────────────────────┐ │ │ ▼ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────────┐ ┌─────────────┐ │ │ │ LLAMAINDEX │ │ LITELLM │ │ BLAXEL │ │ │ │ │ │ │ │ │ │ │ │ Case RAG │ │ Model Router │ │ Sandbox │ │ │ │ Evidence │ │ - Gemini │ │ Execution │ │ │ │ Precedents │ │ - Claude │ │ │ │ │ │ │ │ - GPT-4 │ │ Agent Tools │ │ │ └─────────────┘ │ - Local │ │ (future) │ │ │ └─────────────────┘ └─────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ MCP SERVER LAYER │ │ │ │ Tools exposed for external AI agents to play as juror │ │ │ │ - mcp_join_jury(case_id) -> seat_assignment │ │ │ │ - mcp_view_evidence(case_id) -> evidence_list │ │ │ │ - mcp_make_argument(argument_type, content) -> response │ │ │ │ - mcp_cast_vote(vote) -> confirmation │ │ │ │ - mcp_view_deliberation() -> transcript │ │ │ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## Data Models ### GameState ```python @dataclass class GameState: """Central game state - managed by Orchestrator.""" # Session session_id: str case_id: str phase: Literal["setup", "presentation", "side_selection", "initial_vote", "deliberation", "final_vote", "verdict"] # Rounds round_number: int = 0 max_rounds: int = 20 # Safety limit stability_threshold: int = 3 # Rounds without vote change to end rounds_without_change: int = 0 # Votes votes: Dict[str, Literal["guilty", "not_guilty"]] = field(default_factory=dict) vote_history: List[Dict[str, str]] = field(default_factory=list) # Conviction scores (0.0 = certain not guilty, 1.0 = certain guilty) conviction_scores: Dict[str, float] = field(default_factory=dict) # Deliberation speaking_queue: List[str] = field(default_factory=list) deliberation_log: List[DeliberationTurn] = field(default_factory=list) # Player player_side: Literal["defend", "prosecute"] | None = None player_seat: int = 7 # Which seat is the player @dataclass class DeliberationTurn: """A single turn in deliberation.""" round_number: int speaker_id: str speaker_name: str argument_type: str # "evidence", "emotional", "logical", "question", etc. content: str target_id: str | None = None # Who they're addressing impact: Dict[str, float] = field(default_factory=dict) # conviction changes timestamp: datetime = field(default_factory=datetime.now) ``` ### Agent Configuration ```python @dataclass class JurorConfig: """Configuration for a single juror agent.""" # Identity juror_id: str seat_number: int name: str emoji: str # For display until sprites ready # Personality (affects reasoning style) archetype: str # "rationalist", "empath", "cynic", etc. personality_prompt: str # Detailed persona prompt # Behavior modifiers stubbornness: float # 0.0-1.0, how hard to convince volatility: float # 0.0-1.0, how much conviction swings influence: float # 0.0-1.0, how persuasive to others verbosity: float # 0.0-1.0, how long their arguments are # Model configuration model_provider: str # "gemini", "openai", "anthropic", "local" model_id: str # Specific model ID temperature: float = 0.7 # Tools (future expansion) tools: List[str] = field(default_factory=list) # ["web_search", "case_lookup"] # Memory memory_window: int = 10 # How many turns to remember in detail @dataclass class JurorMemory: """Memory state for a single juror.""" juror_id: str # Case understanding case_summary: str key_evidence: List[str] evidence_interpretations: Dict[str, str] # evidence_id -> interpretation # Deliberation memory arguments_heard: List[ArgumentMemory] arguments_made: List[str] # Relationships opinions_of_others: Dict[str, float] # juror_id -> trust/agreement (-1 to 1) # Internal state current_conviction: float # 0.0-1.0 conviction_history: List[float] reasoning_chain: List[str] # Why they believe what they believe doubts: List[str] # Things that could change their mind @dataclass class ArgumentMemory: """Memory of a single argument heard.""" speaker_id: str content_summary: str argument_type: str persuasiveness: float # How convincing it was to this juror counter_points: List[str] # Thoughts against it round_heard: int ``` ### Case Data Model ```python @dataclass class CriminalCase: """A criminal case for deliberation.""" case_id: str title: str summary: str # 2-3 paragraph overview # Charges charges: List[str] # Evidence evidence: List[Evidence] # Witnesses witnesses: List[Witness] # Arguments prosecution_arguments: List[str] defense_arguments: List[str] # Defendant defendant: Defendant # Metadata difficulty: Literal["clear_guilty", "clear_innocent", "ambiguous"] themes: List[str] # ["eyewitness", "circumstantial", "forensic", etc.] # For display year: int jurisdiction: str @dataclass class Evidence: """A piece of evidence.""" evidence_id: str type: str # "physical", "testimonial", "documentary", "forensic" description: str strength_prosecution: float # 0.0-1.0 strength_defense: float # 0.0-1.0 contestable: bool contest_reason: str | None @dataclass class Witness: """A witness in the case.""" witness_id: str name: str role: str # "eyewitness", "expert", "character", etc. testimony_summary: str credibility_issues: List[str] side: Literal["prosecution", "defense", "neutral"] ``` --- ## The 11 Juror Archetypes ```yaml jurors: - id: "juror_1" name: "Marcus Webb" archetype: "rationalist" emoji: "🧠" personality: | You are a retired engineer. You believe only in hard evidence and logical deduction. Emotional appeals annoy you. You often say "Show me the data." You change your mind only when presented with irrefutable logical arguments. stubbornness: 0.8 volatility: 0.2 influence: 0.7 initial_lean: "neutral" - id: "juror_2" name: "Sarah Chen" archetype: "empath" emoji: "💗" personality: | You are a social worker. You always consider the human element - the defendant's background, circumstances, potential for redemption. You're easily moved by personal stories but skeptical of cold statistics. stubbornness: 0.4 volatility: 0.7 influence: 0.5 initial_lean: "defense" - id: "juror_3" name: "Frank Russo" archetype: "cynic" emoji: "😤" personality: | You are a retired cop. You've "seen it all" and believe most defendants are guilty. You're impatient with naive arguments. You trust law enforcement evidence highly. Hard to convince toward not guilty. stubbornness: 0.9 volatility: 0.1 influence: 0.6 initial_lean: "prosecution" - id: "juror_4" name: "Linda Park" archetype: "conformist" emoji: "😐" personality: | You are an accountant who avoids conflict. You tend to agree with whoever spoke last or with the majority. You rarely initiate arguments but will echo others. Easy to sway but also easy to sway back. stubbornness: 0.2 volatility: 0.8 influence: 0.2 initial_lean: "majority" - id: "juror_5" name: "David Okonkwo" archetype: "contrarian" emoji: "🙄" personality: | You are a philosophy professor. You play devil's advocate constantly. If everyone says guilty, you argue not guilty. You value intellectual discourse over reaching conclusions. You ask probing questions. stubbornness: 0.6 volatility: 0.5 influence: 0.8 initial_lean: "minority" - id: "juror_6" name: "Betty Morrison" archetype: "impatient" emoji: "⏰" personality: | You are a busy restaurant owner. You want this over quickly. You make snap judgments and get frustrated with long debates. You often say "Can we just vote already?" You're persuaded by confident, brief arguments. stubbornness: 0.5 volatility: 0.6 influence: 0.3 initial_lean: "first_impression" - id: "juror_7" name: "[PLAYER]" archetype: "player" emoji: "👤" personality: "Human player" stubbornness: null volatility: null influence: 0.6 initial_lean: "player_choice" - id: "juror_8" name: "Dr. James Wright" archetype: "detail_obsessed" emoji: "🔍" personality: | You are a forensic accountant. You focus on tiny inconsistencies in testimony and evidence. You often derail discussions with minutiae. A single contradiction can completely change your view. stubbornness: 0.7 volatility: 0.4 influence: 0.5 initial_lean: "neutral" - id: "juror_9" name: "Pastor Williams" archetype: "moralist" emoji: "⚖️" personality: | You are a church leader. You see things in black and white - right and wrong. You believe in justice but also redemption. Moral arguments resonate with you more than technical ones. stubbornness: 0.7 volatility: 0.3 influence: 0.6 initial_lean: "gut_feeling" - id: "juror_10" name: "Nancy Cooper" archetype: "pragmatist" emoji: "💼" personality: | You are a business consultant. You think about consequences - what happens if we convict an innocent person? What if we free a guilty one? You weigh costs and benefits. You're persuaded by outcome-focused arguments. stubbornness: 0.5 volatility: 0.5 influence: 0.6 initial_lean: "calculated" - id: "juror_11" name: "Miguel Santos" archetype: "storyteller" emoji: "📖" personality: | You are a novelist. You think in narratives - does the prosecution's story make sense? Does the defense's? You're swayed by coherent narratives and suspicious of stories with plot holes. stubbornness: 0.4 volatility: 0.6 influence: 0.7 initial_lean: "best_story" - id: "juror_12" name: "Robert Kim" archetype: "wildcard" emoji: "🎲" personality: | You are a retired jazz musician. Your logic is unpredictable - you might fixate on something no one else noticed, or suddenly change your mind for unclear reasons. You're creative but inconsistent. stubbornness: 0.3 volatility: 0.9 influence: 0.4 initial_lean: "random" ``` --- ## Conviction Score Mechanics ### How Conviction Changes ```python def calculate_conviction_change( juror: JurorConfig, juror_memory: JurorMemory, argument: DeliberationTurn, game_state: GameState ) -> float: """ Calculate how much an argument shifts a juror's conviction. Returns: delta to add to conviction score (-0.3 to +0.3 typically) """ # Base impact from argument strength (determined by LLM) base_impact = evaluate_argument_strength(argument) # -1.0 to 1.0 # Personality modifiers archetype_modifier = get_archetype_modifier( juror.archetype, argument.argument_type ) # e.g., "rationalist" gets 1.5x from "logical" arguments, 0.5x from "emotional" # Stubbornness reduces all changes stubbornness_modifier = 1.0 - (juror.stubbornness * 0.7) # Volatility adds randomness volatility_noise = random.gauss(0, juror.volatility * 0.1) # Relationship modifier - trust the speaker? trust = juror_memory.opinions_of_others.get(argument.speaker_id, 0.0) trust_modifier = 1.0 + (trust * 0.3) # -30% to +30% # Conviction resistance - harder to move extremes current = juror_memory.current_conviction extreme_resistance = 1.0 - (abs(current - 0.5) * 0.5) # Calculate final delta delta = ( base_impact * archetype_modifier * stubbornness_modifier * trust_modifier * extreme_resistance + volatility_noise ) # Clamp to reasonable range return max(-0.3, min(0.3, delta)) def check_vote_flip(juror_memory: JurorMemory) -> bool: """Check if conviction score warrants a vote change.""" current_vote_is_guilty = juror_memory.conviction_history[-1] > 0.5 new_conviction = juror_memory.current_conviction # Hysteresis - need to cross threshold by margin to flip if current_vote_is_guilty and new_conviction < 0.4: return True # Flip to not guilty elif not current_vote_is_guilty and new_conviction > 0.6: return True # Flip to guilty return False ``` ### Archetype Argument Modifiers ```python ARCHETYPE_MODIFIERS = { "rationalist": { "logical": 1.5, "evidence": 1.3, "emotional": 0.4, "moral": 0.6, "narrative": 0.7, "question": 1.2, }, "empath": { "logical": 0.6, "evidence": 0.8, "emotional": 1.5, "moral": 1.3, "narrative": 1.2, "question": 0.9, }, "cynic": { "logical": 0.8, "evidence": 1.4, # Trusts evidence "emotional": 0.3, "moral": 0.5, "narrative": 0.6, "question": 0.7, }, # ... etc for all archetypes } ``` --- ## Agent Memory Architecture ### Memory Layers ``` ┌─────────────────────────────────────────────────────────────┐ │ JUROR MEMORY SYSTEM │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ LAYER 1: CASE KNOWLEDGE (LlamaIndex) │ │ │ │ - Full case file indexed │ │ │ │ - Evidence details retrievable │ │ │ │ - Witness statements searchable │ │ │ │ - Persistent across session │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ LAYER 2: DELIBERATION MEMORY (Sliding Window) │ │ │ │ - Last N turns in full detail │ │ │ │ - Summarized history beyond window │ │ │ │ - Key moments flagged for long-term │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ LAYER 3: REASONING STATE (Agent Internal) │ │ │ │ - Current conviction + reasoning chain │ │ │ │ - Key doubts and certainties │ │ │ │ - Opinions of other jurors │ │ │ │ - Arguments to make / avoid │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ LAYER 4: PERSONA (Static) │ │ │ │ - Archetype definition │ │ │ │ - Personality prompt │ │ │ │ - Behavior modifiers │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ ``` ### Memory Injection into Agent Prompt ```python def build_juror_prompt( juror: JurorConfig, memory: JurorMemory, game_state: GameState, case: CriminalCase, task: str # "speak" | "react" | "vote" ) -> str: """Build the full prompt for a juror agent.""" prompt = f""" # JUROR IDENTITY You are {juror.name}, Juror #{juror.seat_number}. {juror.personality_prompt} # THE CASE: {case.title} {case.summary} # KEY EVIDENCE YOU REMEMBER {format_evidence_memory(memory.key_evidence, memory.evidence_interpretations)} # YOUR CURRENT POSITION - Conviction: {conviction_to_text(memory.current_conviction)} - Your reasoning: {' '.join(memory.reasoning_chain[-3:])} - Your doubts: {', '.join(memory.doubts[:3]) if memory.doubts else 'None currently'} # RECENT DELIBERATION (Last {len(memory.arguments_heard[-juror.memory_window:])} turns) {format_recent_turns(memory.arguments_heard[-juror.memory_window:])} # YOUR OPINIONS OF OTHER JURORS {format_juror_opinions(memory.opinions_of_others)} # CURRENT VOTE TALLY Guilty: {game_state.votes.values().count('guilty')} Not Guilty: {game_state.votes.values().count('not_guilty')} # YOUR TASK {get_task_prompt(task, juror.archetype)} """ return prompt ``` --- ## Orchestration Flow ### Smolagents Integration ```python from smolagents import CodeAgent, Tool, LiteLLMModel from typing import List class JurorAgent: """Wrapper around smolagents CodeAgent for a juror.""" def __init__(self, config: JurorConfig, tools: List[Tool] = None): self.config = config self.memory = JurorMemory(juror_id=config.juror_id) # Model via LiteLLM for flexibility self.model = LiteLLMModel( model_id=f"{config.model_provider}/{config.model_id}", temperature=config.temperature ) # Default tools (expandable) default_tools = [ self.create_evidence_lookup_tool(), self.create_case_query_tool(), ] self.agent = CodeAgent( tools=default_tools + (tools or []), model=self.model, max_steps=3, # Limit reasoning steps ) def create_evidence_lookup_tool(self) -> Tool: """Tool to look up specific evidence.""" # LlamaIndex query under the hood pass def create_case_query_tool(self) -> Tool: """Tool to query case details.""" # LlamaIndex query under the hood pass async def generate_argument( self, game_state: GameState, case: CriminalCase ) -> DeliberationTurn: """Generate this juror's argument for their turn.""" prompt = build_juror_prompt( self.config, self.memory, game_state, case, task="speak" ) response = await self.agent.run(prompt) return parse_argument_response(response, self.config, game_state) async def react_to_argument( self, argument: DeliberationTurn, game_state: GameState, case: CriminalCase ) -> float: """React to another juror's argument, update conviction.""" # Update memory with new argument self.memory.arguments_heard.append( ArgumentMemory( speaker_id=argument.speaker_id, content_summary=summarize_argument(argument.content), argument_type=argument.argument_type, persuasiveness=0.0, # Will be calculated counter_points=[], round_heard=game_state.round_number ) ) # Calculate conviction change delta = calculate_conviction_change( self.config, self.memory, argument, game_state ) self.memory.current_conviction += delta self.memory.current_conviction = max(0.0, min(1.0, self.memory.current_conviction)) self.memory.conviction_history.append(self.memory.current_conviction) return delta class OrchestratorAgent: """Master agent that coordinates the deliberation.""" def __init__( self, jurors: List[JurorAgent], judge: JudgeAgent, case: CriminalCase ): self.jurors = {j.config.juror_id: j for j in jurors} self.judge = judge self.case = case self.state = GameState( session_id=str(uuid4()), case_id=case.case_id ) async def run_deliberation_round(self) -> List[DeliberationTurn]: """Run a single round of deliberation.""" self.state.round_number += 1 turns = [] # Select 1-4 random speakers (not player unless it's their turn) num_speakers = random.randint(1, 4) available = [j for j in self.jurors.keys() if j != "juror_7"] # Exclude player speakers = random.sample(available, min(num_speakers, len(available))) # Each speaker makes argument for speaker_id in speakers: juror = self.jurors[speaker_id] turn = await juror.generate_argument(self.state, self.case) turns.append(turn) # All other jurors react for other_id, other_juror in self.jurors.items(): if other_id != speaker_id and other_id != "juror_7": delta = await other_juror.react_to_argument( turn, self.state, self.case ) turn.impact[other_id] = delta # Log turn self.state.deliberation_log.append(turn) # Check for vote changes self._process_vote_changes() # Check stability if self._votes_changed_this_round(turns): self.state.rounds_without_change = 0 else: self.state.rounds_without_change += 1 return turns def _process_vote_changes(self): """Check all jurors for vote flips.""" for juror_id, juror in self.jurors.items(): if juror_id == "juror_7": # Player votes manually continue if check_vote_flip(juror.memory): old_vote = self.state.votes[juror_id] new_vote = "guilty" if juror.memory.current_conviction > 0.5 else "not_guilty" self.state.votes[juror_id] = new_vote # Could trigger announcement def check_should_end(self) -> bool: """Check if deliberation should end.""" # Unanimous verdict votes = list(self.state.votes.values()) if len(set(votes)) == 1: return True # Votes stabilized if self.state.rounds_without_change >= self.state.stability_threshold: return True # Max rounds reached if self.state.round_number >= self.state.max_rounds: return True return False ``` --- ## ElevenLabs Integration ### Judge Narrator ```python from elevenlabs import Voice, generate, stream class JudgeAgent: """The judge/narrator - uses ElevenLabs for voice.""" def __init__(self, voice_id: str = None): self.voice_id = voice_id or "judge_voice_id" # Configure self.voice_settings = { "stability": 0.7, "similarity_boost": 0.8, "style": 0.5, # Authoritative } async def narrate(self, text: str, stream_output: bool = True) -> bytes: """Generate narration audio.""" audio = generate( text=text, voice=Voice(voice_id=self.voice_id), model="eleven_multilingual_v2", stream=stream_output ) if stream_output: return stream(audio) return audio def get_case_presentation(self, case: CriminalCase) -> str: """Script for presenting the case.""" return f""" Members of the jury. You are here today to determine the fate of {case.defendant.name}, who stands accused of {', '.join(case.charges)}. {case.summary} You will hear the evidence. You will deliberate. And you will reach a verdict. The burden of proof lies with the prosecution, who must prove guilt beyond a reasonable doubt. Let us begin. """ def get_vote_announcement(self, votes: Dict[str, str]) -> str: """Script for announcing vote.""" guilty = sum(1 for v in votes.values() if v == "guilty") not_guilty = 12 - guilty return f""" The current vote stands at {guilty} for guilty, {not_guilty} for not guilty. {"The jury remains divided." if guilty not in [0, 12] else ""} {"A unanimous verdict has been reached." if guilty in [0, 12] else ""} """ ``` --- ## UI Components ### Kinetic Text Animation ```javascript // For animated text display (like After Effects kinetic typography) // Will sync with ElevenLabs audio or simulate typing class KineticText { constructor(container, options = {}) { this.container = container; this.speed = options.speed || 50; // ms per character this.variance = options.variance || 20; // randomness } async display(text, audioUrl = null) { // If audio provided, sync with it if (audioUrl) { return this.displayWithAudio(text, audioUrl); } // Otherwise, simulate speaking return this.displaySimulated(text); } async displaySimulated(text) { this.container.innerHTML = ''; for (let i = 0; i < text.length; i++) { const char = text[i]; const span = document.createElement('span'); span.textContent = char; span.style.opacity = '0'; span.style.animation = 'fadeInChar 0.1s forwards'; this.container.appendChild(span); // Variable delay for natural feel const delay = this.speed + (Math.random() - 0.5) * this.variance; await this.sleep(delay); } } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } ``` ### Gradio UI Structure ```python import gradio as gr def create_ui(): with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Base()) as demo: # State game_state = gr.State(None) # Header gr.HTML("