Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import yaml | |
| import json | |
| import os | |
| import traceback | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| import tempfile | |
| import speech_recognition as sr | |
| from TTS.api import TTS | |
| tts_model = TTS("tts_models/en/ljspeech/tacotron2-DDC") | |
| #persona_voices = { | |
| # "Maya": "p225", # younger female | |
| # "Robert": "p240", # older male | |
| # "Angela": "p231", # middle-aged female | |
| # "Jack": "p260" # younger male | |
| #} | |
| from engine.loader import load_persona | |
| from engine.drift import apply_context_shift | |
| from engine.responder import generate_response, generate_response_local | |
| from engine.utils import safe_log | |
| from engine.logger import log_interaction | |
| # Paths | |
| persona_dir = "./personas" | |
| contexts_path = "./contexts/scenarios.json" | |
| error_log_path = "./ot_simulator_errors.log" | |
| # Load available personas | |
| def get_persona_choices(): | |
| return [f for f in os.listdir(persona_dir) if f.endswith(".yml")] | |
| # Load available contextual scenarios | |
| def get_scenario_choices(): | |
| try: | |
| with open(contexts_path, "r") as f: | |
| scenarios = json.load(f) | |
| return [s["scenario"] for s in scenarios] | |
| except Exception as e: | |
| safe_log("Scenarios load error", str(e)) | |
| return [] | |
| # Generate radar chart for emotional/behavioral states | |
| def plot_state(state, persona_name): | |
| if persona_name == "Jack": | |
| metrics = ["anxiety", "trust", "openness", "physical_discomfort"] | |
| colors = ["#e74c3c", "#3498db", "#2ecc71", "#f39c12"] | |
| elif persona_name == "Maya": | |
| metrics = ["anxiety", "trust", "creative_engagement", "occupational_balance"] | |
| colors = ["#e74c3c", "#3498db", "#9b59b6", "#1abc9c"] | |
| else: | |
| metrics = ["anxiety", "trust", "openness", "engagement"] | |
| colors = ["#e74c3c", "#3498db", "#2ecc71", "#95a5a6"] | |
| values = [state.get(m, 0.0) for m in metrics] | |
| angles = np.linspace(0, 2 * np.pi, len(metrics), endpoint=False).tolist() | |
| values += values[:1] | |
| angles += angles[:1] | |
| fig, ax = plt.subplots(figsize=(5, 5), subplot_kw=dict(polar=True)) | |
| ax.plot(angles, values, color=colors[0], linewidth=2) | |
| ax.fill(angles, values, color=colors[0], alpha=0.25) | |
| ax.set_xticks(angles[:-1]) | |
| ax.set_xticklabels([m.replace('_', ' ').title() for m in metrics]) | |
| ax.set_ylim(0, 1) | |
| ax.set_yticklabels(['0.0', '0.2', '0.4', '0.6', '0.8', '1.0']) | |
| ax.set_title(f"{persona_name}'s Emotional State", fontsize=14, pad=20) | |
| ax.grid(True) | |
| fig.tight_layout() | |
| chart_path = f"./state_chart_{persona_name}.png" | |
| fig.savefig(chart_path, dpi=100, bbox_inches='tight') | |
| plt.close(fig) | |
| return chart_path | |
| # Generate interaction history visualization | |
| def plot_interaction_history(history): | |
| if not history or len(history) < 2: | |
| return None | |
| fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 6)) | |
| interactions = list(range(1, len(history) + 1)) | |
| anxiety_vals = [h.get('anxiety', 0) for h in history] | |
| trust_vals = [h.get('trust', 0) for h in history] | |
| ax1.plot(interactions, anxiety_vals, marker='o', color='#e74c3c', linewidth=2, label='Anxiety') | |
| ax1.set_ylabel('Anxiety Level', fontsize=10) | |
| ax1.set_ylim(0, 1) | |
| ax1.grid(True, alpha=0.3) | |
| ax1.legend(loc='upper right') | |
| ax2.plot(interactions, trust_vals, marker='o', color='#3498db', linewidth=2, label='Trust') | |
| ax2.set_xlabel('Interaction Number', fontsize=10) | |
| ax2.set_ylabel('Trust Level', fontsize=10) | |
| ax2.set_ylim(0, 1) | |
| ax2.grid(True, alpha=0.3) | |
| ax2.legend(loc='upper right') | |
| fig.suptitle('Therapeutic Relationship Over Time', fontsize=14) | |
| fig.tight_layout() | |
| history_path = "./interaction_history.png" | |
| fig.savefig(history_path, dpi=100, bbox_inches='tight') | |
| plt.close(fig) | |
| return history_path | |
| # Download session transcript | |
| def download_session(conversation_history, state_history, selected_persona_file): | |
| """Generate downloadable transcript file.""" | |
| if not conversation_history: | |
| return None | |
| try: | |
| persona_path = os.path.join(persona_dir, selected_persona_file) | |
| persona = load_persona(persona_path) | |
| from datetime import datetime | |
| timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") | |
| name = persona.get("persona_name", "Character") | |
| # Header | |
| transcript = f"""================================================================ | |
| LIT DIGITAL TWIN - INTERACTION TRANSCRIPT | |
| ================================================================ | |
| Character: {name} | |
| Date: {timestamp} | |
| Number of Turns: {len(conversation_history)} | |
| ================================================================ | |
| CONVERSATION | |
| ================================================================ | |
| """ | |
| # Conversation history | |
| for i, turn in enumerate(conversation_history, 1): | |
| transcript += f"\n[Turn {i}]\n" | |
| if 'scenario' in turn: | |
| transcript += f"Scenario: {turn['scenario']}\n\n" | |
| transcript += f"Interviewer: {turn.get('student', '')}\n\n" | |
| transcript += f"{name}: {turn.get('client', '')}\n\n" | |
| transcript += "-" * 63 + "\n" | |
| # State progression | |
| if state_history: | |
| transcript += f"""================================================================ | |
| STATE PROGRESSION | |
| ================================================================ | |
| Initial State: | |
| Anxiety: {state_history[0].get('anxiety', 0):.2f} | |
| Trust: {state_history[0].get('trust', 0):.2f} | |
| Openness: {state_history[0].get('openness', 0):.2f} | |
| Final State: | |
| Anxiety: {state_history[-1].get('anxiety', 0):.2f} | |
| Trust: {state_history[-1].get('trust', 0):.2f} | |
| Openness: {state_history[-1].get('openness', 0):.2f} | |
| Change: | |
| Anxiety: {state_history[-1].get('anxiety', 0) - state_history[0].get('anxiety', 0):+.2f} | |
| Trust: {state_history[-1].get('trust', 0) - state_history[0].get('trust', 0):+.2f} | |
| Openness: {state_history[-1].get('openness', 0) - state_history[0].get('openness', 0):+.2f} | |
| ================================================================ | |
| """ | |
| # Save to file | |
| filename = f"{name}_{timestamp}.txt" | |
| filepath = os.path.join("lit_transcripts", filename) | |
| os.makedirs("lit_transcripts", exist_ok=True) | |
| with open(filepath, "w", encoding="utf-8") as f: | |
| f.write(transcript) | |
| return filepath | |
| except Exception as e: | |
| safe_log("Download error", str(e)) | |
| return None | |
| def simulate(prompt, selected_event, selected_persona_file, ai_mode, | |
| conversation_history, state_history, use_fast_mode): | |
| try: | |
| # Normalize inputs | |
| if hasattr(prompt, 'value'): | |
| prompt = prompt.value | |
| prompt = str(prompt) if prompt else "" | |
| if hasattr(selected_event, 'value'): | |
| selected_event = selected_event.value | |
| if hasattr(selected_persona_file, 'value'): | |
| selected_persona_file = selected_persona_file.value | |
| if hasattr(ai_mode, 'value'): | |
| ai_mode = ai_mode.value | |
| # Initialize histories | |
| conversation_history = conversation_history or [] | |
| state_history = state_history or [] | |
| # Load persona | |
| persona_path = os.path.join(persona_dir, selected_persona_file) | |
| persona = load_persona(persona_path) | |
| # Load contextual scenario | |
| with open(contexts_path, "r") as f: | |
| scenarios = json.load(f) | |
| scenario = next((s for s in scenarios if s["scenario"] == selected_event), None) | |
| if scenario: | |
| persona = apply_context_shift(persona, scenario) | |
| context_note = f"**Context:** {scenario.get('description', selected_event)}\n\n" | |
| else: | |
| context_note = "" | |
| # Generate response | |
| response, updated_state, teaching_note = generate_response( | |
| prompt, | |
| persona, | |
| conversation_history, | |
| force_mode=ai_mode, | |
| use_fast_mode=use_fast_mode | |
| ) | |
| # Update histories | |
| conversation_history.append({ | |
| "student": prompt, | |
| "client": response, | |
| "scenario": selected_event | |
| }) | |
| state_history.append(updated_state.copy()) | |
| # Conversation display (now uses Interlocutor) | |
| conversation_display = "" | |
| for i, turn in enumerate(conversation_history, 1): | |
| conversation_display += f"**Turn {i}**\n" | |
| if 'scenario' in turn: | |
| conversation_display += f"*[Scenario: {turn['scenario']}]*\n" | |
| conversation_display += f"**Interviewer:** {turn['student']}\n\n" | |
| conversation_display += f"**{persona['persona_name']}:** {turn['client']}\n\n" | |
| conversation_display += "---\n\n" | |
| # Visualizations | |
| state_yaml = yaml.dump(updated_state, sort_keys=False) | |
| current_chart = plot_state(updated_state, persona['persona_name']) | |
| history_chart = plot_interaction_history(state_history) | |
| # Teaching feedback | |
| teaching_feedback = f"**Note:**\n{teaching_note}\n\n" | |
| teaching_feedback += f"**Current State:**\n" | |
| for k, v in updated_state.items(): | |
| if isinstance(v, (int, float)): | |
| teaching_feedback += f"- {k.capitalize()}: {v:.2f}\n" | |
| if 'emotional_memory' in updated_state and updated_state['emotional_memory']: | |
| teaching_feedback += f"\n**Memory:** {updated_state['emotional_memory'][-1]}\n" | |
| # Log interaction | |
| transcript_path = log_interaction( | |
| persona, | |
| prompt, | |
| selected_event, | |
| response, | |
| updated_state, | |
| teaching_note, | |
| ) | |
| return ( | |
| conversation_display, | |
| teaching_feedback, | |
| state_yaml, | |
| current_chart, | |
| history_chart, | |
| conversation_history, | |
| state_history | |
| ) | |
| except Exception as e: | |
| error_msg = traceback.format_exc() | |
| safe_log("Simulation error", error_msg) | |
| print(f"ERROR: {error_msg}") | |
| return ( | |
| "[ERROR] Simulation failed. Check logs.", | |
| "Error occurred", | |
| "", | |
| None, | |
| None, | |
| conversation_history, | |
| state_history | |
| ) | |
| # Save to file | |
| filename = f"{name}_{timestamp}.txt" | |
| filepath = os.path.join("lit_transcripts", filename) | |
| os.makedirs("lit_transcripts", exist_ok=True) | |
| with open(filepath, "w", encoding="utf-8") as f: | |
| f.write(transcript) | |
| return filepath | |
| except Exception as e: | |
| safe_log("Download error", str(e)) | |
| return None | |
| # --- Speech-to-Text helper --- | |
| def speech_to_text(audio_file): | |
| recognizer = sr.Recognizer() | |
| with sr.AudioFile(audio_file) as source: | |
| audio = recognizer.record(source) | |
| try: | |
| text = recognizer.recognize_google(audio) | |
| except sr.UnknownValueError: | |
| text = "" | |
| return text | |
| # --- Wrapper with STT + TTS --- | |
| def simulate_with_voice(audio_in, scenario, persona, mode, conversation_state, state_history): | |
| student_prompt = speech_to_text(audio_in) if audio_in else "" | |
| conversation_display, teaching_feedback, state_yaml, current_chart, history_chart, conversation_state, state_history = simulate( | |
| student_prompt, scenario, persona, mode, conversation_state, state_history | |
| ) | |
| persona_response = conversation_display | |
| tmpfile = tempfile.NamedTemporaryFile(delete=False, suffix=".wav") | |
| tts_model.tts_to_file(text=persona_response, file_path=tmpfile.name) | |
| return ( | |
| conversation_display, | |
| teaching_feedback, | |
| state_yaml, | |
| current_chart, | |
| history_chart, | |
| conversation_state, | |
| state_history, | |
| tmpfile.name | |
| ) | |
| import gradio as gr | |
| import os | |
| with gr.Blocks( | |
| title="Literary Character Simulator", | |
| theme=gr.themes.Soft() | |
| ) as ui: | |
| # Header | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Image("NYIT.png", show_label=False, height=60, width=180) | |
| with gr.Column(scale=8): | |
| gr.Markdown("## Literary Character Simulator") | |
| # Intro text | |
| gr.Markdown( | |
| """ | |
| ### Learn from characters in fiction | |
| Ask questions pertaining to the story. | |
| """ | |
| ) | |
| # State management | |
| conversation_state = gr.State([]) | |
| state_history = gr.State([]) | |
| # Main row: left column (selectors + info + mode) and right column (conversation) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| persona_files = get_persona_choices() | |
| default_persona = persona_files[0] if persona_files else None | |
| persona_selector = gr.Dropdown( | |
| label="Select Character", | |
| choices=persona_files, | |
| value=default_persona, | |
| allow_custom_value=False, | |
| info="Choose which character you'll be working with" | |
| ) | |
| scenario_choices = get_scenario_choices() | |
| scenario_selector = gr.Dropdown( | |
| label="Contextual Scenario", | |
| choices=scenario_choices, | |
| value=scenario_choices[0] if scenario_choices else None, | |
| info="What's happening in the character's life right now?" | |
| ) | |
| gr.Markdown("---") | |
| with gr.Accordion("Character Information", open=False): | |
| gr.Markdown( | |
| """**Jimmy Markum (38)** – Corner store owner and ex-convict driven by grief and vengeance after his daughter's murder. Uses street-level intuition and intimidation to investigate outside the law. | |
| **Sean Devine (38)** – Homicide detective torn between duty and personal history. Methodical and emotionally guarded, investigating the same case that haunts his childhood friendships. | |
| **Hamlet (28)** – Prince of Denmark consumed by grief, betrayal, and existential doubt. Philosophical and emotionally volatile, struggling to avenge his father’s death. | |
| **Jane (30)** – Narrator of *The Yellow Wallpaper*, confined and unraveling under the rest cure. Perceptive and imaginative, she descends into madness while observing the wallpaper’s patterns. | |
| **Harold Krebs (25)** – War veteran from *Soldier’s Home*, emotionally numb and disconnected from postwar life. Struggles with reintegration, honesty, and societal expectations. | |
| **Louise Mallard (28)** – Grieving wife from *The Story of an Hour*, who experiences a fleeting sense of freedom upon hearing of her husband’s death. Emotionally complex and physically fragile. | |
| **Eveline (19)** – Young woman from Joyce’s *Eveline*, torn between duty to her family and the promise of escape. Paralyzed by fear and memory at the moment of decision. | |
| **Uncle Ben (50)** – Civil servant from Achebe’s *Uncle Ben’s Choice*, navigating moral compromise in postcolonial Nigeria. Pragmatic and ethically conflicted, choosing silence over confrontation. | |
| **John Keegan (47)** – Deputy Inspector in the NYPD and subject of a hit TV series. Known as “Keegan the Destroyer,” he’s a controversial figure haunted by past cases, a fractured marriage, and the death of his partner. Struggles with emotional suppression and guilt. | |
| **Creon (50)** – Advisor and brother-in-law to Oedipus. Rational and loyal, he is wrongly accused of conspiracy but remains composed. Becomes ruler after Oedipus’s fall, representing justice and restraint. | |
| **Tiresias (70)** – Blind prophet of Thebes. Cryptic and wise, he reveals truths others fear to hear. Burdened by divine insight, he speaks with solemn clarity and moral weight. | |
| **Jocasta (56)** – Queen of Thebes, mother and wife to Oedipus. Emotionally intuitive and protective, she tries to suppress the truth that will destroy her family, ultimately taking her own life. | |
| **Laertes (25)** – Nobleman of Denmark, son of Polonius. Passionate and impulsive, he seeks revenge for his family’s destruction, ultimately dying in a duel with Hamlet. | |
| **Arianna Nunez (32)** – Ambitious NYPD Detective and digital expert. Balances Keegan and Lavin’s chaos with wit and precision. Known for undercover work, sharp comebacks, and a drive to rise in the ranks. | |
| **Karl Lavin (58)** – NYPD Lieutenant and Keegan’s loyal, sarcastic partner. A veteran of Vice, he uses crude humor and streetwise tactics to protect his team. Planning retirement, but still Keegan’s “babysitter.” | |
| **Ellie Williams (19)** – Immune survivor in a post-apocalyptic world. Fierce, sarcastic, and emotionally scarred, she carries the weight of loss and the burden of being a symbol. Loyal but guarded, she fights to define her own path. | |
| **Joel Miller (52)** – Survivor of a global outbreak and father figure to Ellie. Sorrowful but guarded, his emotional wounds have scarred over. Loyal and pragmatic, he makes morally complex choices to protect those he loves. | |
| **Oedipus (40)** – King of Thebes and tragic seeker of truth. Determined to end a plague, he uncovers his own guilt and fulfills a devastating prophecy, blinding himself in remorse. | |
| **Pauline McCrory-Keegan (45)** – NYPD officer on leave, seeking identity and purpose after years of emotional strain and marital separation. Investigating a missing child case in Maryland while confronting her past and redefining her future. | |
| """ | |
| ) | |
| ai_mode_selector = gr.Radio( | |
| label="Response Mode", | |
| choices=["AI (HuggingFace)", "Templates (Local)"], | |
| value="AI (HuggingFace)", | |
| info="Choose how responses are generated" | |
| ) | |
| use_fast_mode_toggle = gr.Checkbox( | |
| label="Use Fast Mode (Local AI)", | |
| value=False, | |
| info="Enable faster responses using local model" | |
| ) | |
| with gr.Column(scale=2): | |
| conversation_display = gr.Markdown( | |
| label="Conversation History", | |
| value="*Conversation will appear here...*" | |
| ) | |
| #with gr.Row(): | |
| # audio_in = gr.Audio(type="filepath", label="🎤 Speak to the character", interactive=True) | |
| # voice_submit_btn = gr.Button("Send Voice Input") | |
| # audio_out = gr.Audio(label="Character Voice", type="filepath") | |
| # User input row | |
| with gr.Row(): | |
| with gr.Column(): | |
| student_prompt = gr.Textbox( | |
| label="Your Input", | |
| lines=3, | |
| placeholder="Ask the character something...", | |
| value="" | |
| ) | |
| # Buttons row | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| send_btn = gr.Button("Send", variant="primary") | |
| with gr.Column(scale=1): | |
| download_btn = gr.Button("Download Session") | |
| with gr.Column(scale=1): | |
| reset_btn = gr.Button("Reset Conversation") | |
| # Feedback + technical state row | |
| with gr.Row(): | |
| with gr.Column(): | |
| teaching_output = gr.Markdown(label="Narrative Feedback") | |
| with gr.Column(): | |
| state_output = gr.Textbox( | |
| label="Technical State (for debugging)", | |
| lines=8, | |
| visible=False | |
| ) | |
| # Charts row | |
| with gr.Row(): | |
| with gr.Column(): | |
| current_state_chart = gr.Image(label="Current State") | |
| with gr.Column(): | |
| history_chart = gr.Image(label="Progress Over Time") | |
| # Hidden file output for downloads | |
| download_file = gr.File( | |
| label="Your session transcript (right-click to save)", | |
| visible=True | |
| ) | |
| # Instructor guide | |
| with gr.Accordion("Instructor Guide", open=False): | |
| gr.Markdown( | |
| """This simulator allows you to interact with fictional characters | |
| in different scenarios. Use it to explore dialogue, context, and | |
| character development in a safe environment.""" | |
| ) | |
| # Button actions | |
| send_btn.click( | |
| fn=simulate, | |
| inputs=[ | |
| student_prompt, | |
| scenario_selector, | |
| persona_selector, | |
| ai_mode_selector, | |
| conversation_state, | |
| state_history, | |
| use_fast_mode_toggle | |
| ], | |
| outputs=[ | |
| conversation_display, | |
| teaching_output, | |
| state_output, | |
| current_state_chart, | |
| history_chart, | |
| conversation_state, | |
| state_history | |
| ] | |
| ) | |
| download_btn.click( | |
| fn=download_session, | |
| inputs=[ | |
| conversation_state, | |
| state_history, | |
| persona_selector | |
| ], | |
| outputs=download_file | |
| ) | |
| # voice_submit_btn.click( | |
| # fn=simulate_with_voice, | |
| # inputs=[ | |
| # persona_selector, | |
| # ai_mode_selector, | |
| # conversation_state, | |
| # state_history | |
| # ], | |
| # outputs=[ | |
| # conversation_display, | |
| # teaching_output, | |
| # state_output, | |
| # current_state_chart, | |
| # history_chart, | |
| # conversation_state, | |
| # state_history, | |
| # audio_out | |
| # ] | |
| # ) | |
| def reset_conversation(): | |
| return ( | |
| "*Conversation will appear here...*", | |
| "", | |
| "", | |
| None, | |
| None, | |
| [], | |
| [] | |
| ) | |
| reset_btn.click( | |
| fn=reset_conversation, | |
| inputs=[], | |
| outputs=[ | |
| conversation_display, | |
| teaching_output, | |
| state_output, | |
| current_state_chart, | |
| history_chart, | |
| conversation_state, | |
| state_history | |
| ] | |
| ) | |
| if __name__ == "__main__": | |
| os.makedirs("personas", exist_ok=True) | |
| os.makedirs("contexts", exist_ok=True) | |
| os.makedirs("transcripts", exist_ok=True) | |
| os.makedirs("engine", exist_ok=True) | |
| ui.launch(share=True, server_name="0.0.0.0", server_port=7860) |