# app.py — Affection 👁️ (Hugging Face Space) import os import gradio as gr import matplotlib.pyplot as plt os.environ["GRADIO_ANALYTICS_ENABLED"] = "False" os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1" os.environ["SPACES_DISABLE_RELOAD"] = "1" from utils.presets import EMOTION_PRESETS from utils.drama import apply_drama from utils.color_model import infer_color, render_color # ------------------------------------------------------------ # Passion (Radial Amplification) # ------------------------------------------------------------ def apply_passion(raw: dict, passion: float) -> dict: passion = max(0.0, min(3.5, float(passion))) out = {} for k, v in raw.items(): v = float(v) if k in ("V", "A", "D"): delta = v - 0.5 magnitude = abs(delta) gain = 1.0 + passion * magnitude out[k] = max(0.0, min(1.0, 0.5 + delta * gain)) else: out[k] = max(0.0, min(1.0, v)) return out # ------------------------------------------------------------ # Valence–Arousal Visualization (2D Projection) # ------------------------------------------------------------ def generate_scatter(raw, amplified, cinematic, target, target_name, passion, drama): fig, ax = plt.subplots(figsize=(6, 7)) # slightly taller plt.subplots_adjust(right=0.75) # leave room for legend # ---------------------------------- # Background Anchors # ---------------------------------- for name, preset in EMOTION_PRESETS.items(): t = preset["target"] ax.scatter(t["V"], t["A"], alpha=0.06, s=90, color="#DDDDDD") # ---------------------------------- # Trajectory Points (Styled) # ---------------------------------- # 1️⃣ Natural — light grey thin border ax.scatter( raw["V"], raw["A"], s=180, facecolor="#F0F0F0", edgecolor="#CCCCCC", linewidth=1, label="Natural" ) # 2️⃣ After Passion — medium grey ax.scatter( amplified["V"], amplified["A"], s=180, facecolor="#9E9E9E", edgecolor="#666666", linewidth=1.5, label="After Passion" ) # 3️⃣ After Drama — dark grey thin border ax.scatter( cinematic["V"], cinematic["A"], s=220, facecolor="#2F2F2F", edgecolor="black", linewidth=1, label="After Drama" ) # Cinematic Anchor ax.scatter( target["V"], target["A"], s=180, marker="X", color="#E74C3C", edgecolor="black", linewidth=1.2, label=f"Anchor ({target_name})" ) # ---------------------------------- # Dynamic Zoom (20% padded) # ---------------------------------- xs = [raw["V"], amplified["V"], cinematic["V"], target["V"]] ys = [raw["A"], amplified["A"], cinematic["A"], target["A"]] min_x, max_x = min(xs), max(xs) min_y, max_y = min(ys), max(ys) span_x = max_x - min_x span_y = max_y - min_y span = max(span_x, span_y) span = max(span, 0.05) padding = span * 0.20 center_x = (min_x + max_x) / 2 center_y = (min_y + max_y) / 2 # shift center slightly upward center_y += span * 0.10 half_range = (span / 2) + padding ax.set_xlim(center_x - half_range, center_x + half_range) ax.set_ylim(center_y - half_range, center_y + half_range) ax.set_aspect('equal', adjustable='box') # ---------------------------------- # Proportional Arrows # ---------------------------------- arrow_head = span * 0.035 ax.arrow( raw["V"], raw["A"], amplified["V"] - raw["V"], amplified["A"] - raw["A"], head_width=arrow_head, length_includes_head=True, color="#888888", linestyle="--", linewidth=1.8, alpha=0.7 ) ax.arrow( amplified["V"], amplified["A"], cinematic["V"] - amplified["V"], cinematic["A"] - amplified["A"], head_width=arrow_head, length_includes_head=True, color="#444444", linestyle="-", linewidth=2, alpha=0.9 ) # ---------------------------------- # Labels & Legend # ---------------------------------- ax.set_xlabel("Valence") ax.set_ylabel("Arousal") ax.set_title(f"{target_name}\nPassion={round(passion,2)} | Drama={round(drama,2)}") ax.grid(alpha=0.12) # Move legend outside plot ax.legend(loc="center left", bbox_to_anchor=(1.02, 0.5), frameon=False) plt.tight_layout() return fig # ------------------------------------------------------------ # Fast-Loop Simulation # ------------------------------------------------------------ def run_pipeline(preset_name, passion, drama): preset = EMOTION_PRESETS[preset_name] text = preset["text"] natural = preset["raw"] target = preset["target"] amplified = apply_passion(natural, passion) cinematic = apply_drama(amplified, target, drama) color_params = infer_color(cinematic) color_block = render_color(color_params) fig = generate_scatter( natural, amplified, cinematic, target, preset_name, passion, drama ) return ( text, natural, amplified, cinematic, color_params, color_block, fig ) # ------------------------------------------------------------ # UI # ------------------------------------------------------------ with gr.Blocks(title="Affection 👁️ — Edge Emotional Intelligence") as demo: gr.Markdown("# Affection 👁️") gr.Markdown("## Simulation Layer for an Edge AI Emotional Robotics System") gr.Markdown("### 🗣 Robot Speech") preset_selector = gr.Radio( choices=list(EMOTION_PRESETS.keys()), label="Select Transcript Sample", value=list(EMOTION_PRESETS.keys())[0], ) transcript_output = gr.Textbox(label="Input Transcript", interactive=False) gr.Markdown("---") gr.Markdown("### ⚡ Edge Affect Processing") with gr.Row(): passion = gr.Slider(0.0, 3.0, value=2.25, step=0.1, label="Passion") drama = gr.Slider(0.0, 1.5, value=0.65, step=0.05, label="Drama") with gr.Row(): natural_output = gr.JSON(label="Natural") amplified_output = gr.JSON(label="After Passion") cinematic_output = gr.JSON(label="After Drama") scatter_output = gr.Plot(label="Valence–Arousal Projection") gr.Markdown("---") gr.Markdown("### 💡 Emotional Expression") rgb_output = gr.JSON(label="Model Output") color_display = gr.HTML(label="Rendered Expression") outputs = [ transcript_output, natural_output, amplified_output, cinematic_output, rgb_output, color_display, scatter_output ] preset_selector.change(fn=run_pipeline, inputs=[preset_selector, passion, drama], outputs=outputs) passion.change(fn=run_pipeline, inputs=[preset_selector, passion, drama], outputs=outputs) drama.change(fn=run_pipeline, inputs=[preset_selector, passion, drama], outputs=outputs) demo.load(fn=run_pipeline, inputs=[preset_selector, passion, drama], outputs=outputs) demo.launch(server_name="0.0.0.0", server_port=7860)