import base64 from pathlib import Path import gradio as gr CSS = """ :root { color-scheme: dark; } .gradio-container { min-height: 100vh; background: radial-gradient(circle at 18% 8%, rgba(0, 122, 204, 0.20), transparent 28rem), radial-gradient(circle at 90% 16%, rgba(80, 180, 150, 0.12), transparent 24rem), linear-gradient(145deg, #0b0f14 0%, #121821 48%, #070a0f 100%) !important; color: #eef3f8; font-family: Inter, Segoe UI, Arial, sans-serif; } .main { max-width: 1120px; margin: 0 auto; padding: 36px 20px 44px; } .hero { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.75fr); gap: 28px; align-items: center; padding: 24px 0 34px; } .eyebrow { margin: 0 0 12px; color: #7bb9e8; font-size: 0.78rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; } .hero h1 { margin: 0; color: #ffffff; font-size: clamp(2.5rem, 7vw, 5.4rem); line-height: 0.94; letter-spacing: 0; } .lead { max-width: 680px; margin: 22px 0 0; color: #b8c6d5; font-size: 1.08rem; line-height: 1.65; } .hero-card, .panel { border: 1px solid rgba(180, 198, 213, 0.16); border-radius: 8px; background: rgba(13, 19, 28, 0.74); box-shadow: 0 22px 70px rgba(0, 0, 0, 0.36); } .hero-card { display: grid; gap: 16px; padding: 22px; } .app-mark { width: 90px; height: 90px; border-radius: 22px; background: #0f1620; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); } .hero-card h2, .panel h2 { margin: 0; color: #ffffff; font-size: 1.15rem; } .hero-card p, .panel p { margin: 0; color: #b8c6d5; line-height: 1.55; } .chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; } .chip { border: 1px solid rgba(123, 185, 232, 0.28); border-radius: 999px; padding: 6px 10px; color: #d9ecfb; background: rgba(0, 122, 204, 0.14); font-size: 0.86rem; } .grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin-top: 18px; } .panel { padding: 18px; } .section-title { margin: 36px 0 12px; color: #ffffff; font-size: 1.35rem; } .flow { display: grid; gap: 8px; padding: 18px; border: 1px solid rgba(180, 198, 213, 0.14); border-radius: 8px; background: rgba(255, 255, 255, 0.045); color: #d5e2ec; font-family: Consolas, Monaco, monospace; font-size: 0.92rem; } .flow span { display: block; } .footer-note { margin-top: 28px; color: #8393a2; font-size: 0.92rem; } @media (max-width: 820px) { .hero, .grid { grid-template-columns: 1fr; } } """ def _image_data_uri(path: str) -> str: image_path = Path(path) if not image_path.exists(): return "" encoded = base64.b64encode(image_path.read_bytes()).decode("ascii") return f"data:image/png;base64,{encoded}" ICON_URI = _image_data_uri("src/Reachy_OpenWebUI/static/assets/reachy-conversation-app.png") HTML = f"""

Reachy Mini + OpenWebUI

Reachy OpenWebUI

A phone-style conversation app for Reachy Mini. OpenWebUI handles the model, transcription, user context, and TTS; the Reachy app handles the robot runtime, settings UI, local tools, avatars, chat state, and movement.

What It Does

Contacts

OpenWebUI models and workspaces appear as phone contacts with cached avatars.

Conversations

Saved OpenWebUI chat ids can be selected from a phone-style conversation list.

Motion

Speech-reactive head movement blends with Reachy-side dances and recorded emotions.

Runtime Flow

Reachy microphone -> VAD and OpenWebUI transcription -> OpenWebUI chat completion with Reachy tool specs -> optional local Reachy tool execution -> OpenWebUI speech -> Reachy speaker output plus motion
""" with gr.Blocks(title="Reachy OpenWebUI") as demo: gr.HTML(HTML) if __name__ == "__main__": demo.launch(css=CSS)