danielritchie/generative-affect-engine
0
1# app.py — Affection 👁️ (Hugging Face Space)2 3import os4import gradio as gr5import matplotlib.pyplot as plt6 7os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"8os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"9os.environ["SPACES_DISABLE_RELOAD"] = "1"10 11from utils.presets import EMOTION_PRESETS12from utils.drama import apply_drama13from utils.color_model import infer_color, render_color14 15 16# ------------------------------------------------------------17# Passion (Radial Amplification)18# ------------------------------------------------------------19def apply_passion(raw: dict, passion: float) -> dict:20 passion = max(0.0, min(3.5, float(passion)))21 out = {}22 23 for k, v in raw.items():24 v = float(v)25 if k in ("V", "A", "D"):26 delta = v - 0.527 magnitude = abs(delta)28 gain = 1.0 + passion * magnitude29 out[k] = max(0.0, min(1.0, 0.5 + delta * gain))30 else:31 out[k] = max(0.0, min(1.0, v))32 33 return out34 35 36# ------------------------------------------------------------37# Valence–Arousal Visualization (2D Projection)38# ------------------------------------------------------------39def generate_scatter(raw, amplified, cinematic, target, target_name, passion, drama):40 41 fig, ax = plt.subplots(figsize=(6, 7)) # slightly taller42 plt.subplots_adjust(right=0.75) # leave room for legend43 44 # ----------------------------------45 # Background Anchors46 # ----------------------------------47 for name, preset in EMOTION_PRESETS.items():48 t = preset["target"]49 ax.scatter(t["V"], t["A"], alpha=0.06, s=90, color="#DDDDDD")50 51 # ----------------------------------52 # Trajectory Points (Styled)53 # ----------------------------------54 55 # 1️⃣ Natural — light grey thin border56 ax.scatter(57 raw["V"], raw["A"],58 s=180,59 facecolor="#F0F0F0",60 edgecolor="#CCCCCC",61 linewidth=1,62 label="Natural"63 )64 65 # 2️⃣ After Passion — medium grey66 ax.scatter(67 amplified["V"], amplified["A"],68 s=180,69 facecolor="#9E9E9E",70 edgecolor="#666666",71 linewidth=1.5,72 label="After Passion"73 )74 75 # 3️⃣ After Drama — dark grey thin border76 ax.scatter(77 cinematic["V"], cinematic["A"],78 s=220,79 facecolor="#2F2F2F",80 edgecolor="black",81 linewidth=1,82 label="After Drama"83 )84 85 # Cinematic Anchor86 ax.scatter(87 target["V"],88 target["A"],89 s=180,90 marker="X",91 color="#E74C3C",92 edgecolor="black",93 linewidth=1.2,94 label=f"Anchor ({target_name})"95 )96 97 # ----------------------------------98 # Dynamic Zoom (20% padded)99 # ----------------------------------100 xs = [raw["V"], amplified["V"], cinematic["V"], target["V"]]101 ys = [raw["A"], amplified["A"], cinematic["A"], target["A"]]102 103 min_x, max_x = min(xs), max(xs)104 min_y, max_y = min(ys), max(ys)105 106 span_x = max_x - min_x107 span_y = max_y - min_y108 span = max(span_x, span_y)109 span = max(span, 0.05)110 111 padding = span * 0.20112 center_x = (min_x + max_x) / 2113 center_y = (min_y + max_y) / 2114 115 # shift center slightly upward116 center_y += span * 0.10117 118 half_range = (span / 2) + padding119 120 ax.set_xlim(center_x - half_range, center_x + half_range)121 ax.set_ylim(center_y - half_range, center_y + half_range)122 123 ax.set_aspect('equal', adjustable='box')124 125 # ----------------------------------126 # Proportional Arrows127 # ----------------------------------128 arrow_head = span * 0.035129 130 ax.arrow(131 raw["V"], raw["A"],132 amplified["V"] - raw["V"],133 amplified["A"] - raw["A"],134 head_width=arrow_head,135 length_includes_head=True,136 color="#888888",137 linestyle="--",138 linewidth=1.8,139 alpha=0.7140 )141 142 ax.arrow(143 amplified["V"], amplified["A"],144 cinematic["V"] - amplified["V"],145 cinematic["A"] - amplified["A"],146 head_width=arrow_head,147 length_includes_head=True,148 color="#444444",149 linestyle="-",150 linewidth=2,151 alpha=0.9152 )153 154 # ----------------------------------155 # Labels & Legend156 # ----------------------------------157 ax.set_xlabel("Valence")158 ax.set_ylabel("Arousal")159 ax.set_title(f"{target_name}\nPassion={round(passion,2)} | Drama={round(drama,2)}")160 161 ax.grid(alpha=0.12)162 163 # Move legend outside plot164 ax.legend(loc="center left", bbox_to_anchor=(1.02, 0.5), frameon=False)165 166 plt.tight_layout()167 return fig168 169 170# ------------------------------------------------------------171# Fast-Loop Simulation172# ------------------------------------------------------------173def run_pipeline(preset_name, passion, drama):174 175 preset = EMOTION_PRESETS[preset_name]176 177 text = preset["text"]178 natural = preset["raw"]179 target = preset["target"]180 181 amplified = apply_passion(natural, passion)182 cinematic = apply_drama(amplified, target, drama)183 184 color_params = infer_color(cinematic)185 color_block = render_color(color_params)186 187 fig = generate_scatter(188 natural,189 amplified,190 cinematic,191 target,192 preset_name,193 passion,194 drama195 )196 197 return (198 text,199 natural,200 amplified,201 cinematic,202 color_params,203 color_block,204 fig205 )206 207 208# ------------------------------------------------------------209# UI210# ------------------------------------------------------------211with gr.Blocks(title="Affection 👁️ — Edge Emotional Intelligence") as demo:212 213 gr.Markdown("# Affection 👁️")214 gr.Markdown("## Simulation Layer for an Edge AI Emotional Robotics System")215 216 gr.Markdown("### 🗣 Robot Speech")217 218 preset_selector = gr.Radio(219 choices=list(EMOTION_PRESETS.keys()),220 label="Select Transcript Sample",221 value=list(EMOTION_PRESETS.keys())[0],222 )223 224 transcript_output = gr.Textbox(label="Input Transcript", interactive=False)225 226 gr.Markdown("---")227 228 gr.Markdown("### ⚡ Edge Affect Processing")229 230 with gr.Row():231 passion = gr.Slider(0.0, 3.0, value=2.25, step=0.1, label="Passion")232 drama = gr.Slider(0.0, 1.5, value=0.65, step=0.05, label="Drama")233 234 with gr.Row():235 natural_output = gr.JSON(label="Natural")236 amplified_output = gr.JSON(label="After Passion")237 cinematic_output = gr.JSON(label="After Drama")238 239 scatter_output = gr.Plot(label="Valence–Arousal Projection")240 241 gr.Markdown("---")242 243 gr.Markdown("### 💡 Emotional Expression")244 245 rgb_output = gr.JSON(label="Model Output")246 color_display = gr.HTML(label="Rendered Expression")247 248 outputs = [249 transcript_output,250 natural_output,251 amplified_output,252 cinematic_output,253 rgb_output,254 color_display,255 scatter_output256 ]257 258 preset_selector.change(fn=run_pipeline, inputs=[preset_selector, passion, drama], outputs=outputs)259 passion.change(fn=run_pipeline, inputs=[preset_selector, passion, drama], outputs=outputs)260 drama.change(fn=run_pipeline, inputs=[preset_selector, passion, drama], outputs=outputs)261 262 demo.load(fn=run_pipeline, inputs=[preset_selector, passion, drama], outputs=outputs)263 264demo.launch(server_name="0.0.0.0", server_port=7860)265 