Spaces:
Running
Running
| 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""" | |
| <main class="main"> | |
| <section class="hero"> | |
| <div> | |
| <p class="eyebrow">Reachy Mini + OpenWebUI</p> | |
| <h1>Reachy OpenWebUI</h1> | |
| <p class="lead"> | |
| 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. | |
| </p> | |
| </div> | |
| <aside class="hero-card"> | |
| <img class="app-mark" src="{ICON_URI}" alt=""> | |
| <h2>Project Page</h2> | |
| <p> | |
| This Space is a lightweight overview. The real app runs in the Reachy Mini | |
| app environment where it can access robot audio, camera, speaker, and motion. | |
| </p> | |
| <div class="chips"> | |
| <span class="chip">Phone UI</span> | |
| <span class="chip">OpenWebUI TTS</span> | |
| <span class="chip">Robot tools</span> | |
| <span class="chip">Attitudes</span> | |
| </div> | |
| </aside> | |
| </section> | |
| <h2 class="section-title">What It Does</h2> | |
| <section class="grid"> | |
| <div class="panel"> | |
| <h2>Contacts</h2> | |
| <p>OpenWebUI models and workspaces appear as phone contacts with cached avatars.</p> | |
| </div> | |
| <div class="panel"> | |
| <h2>Conversations</h2> | |
| <p>Saved OpenWebUI chat ids can be selected from a phone-style conversation list.</p> | |
| </div> | |
| <div class="panel"> | |
| <h2>Motion</h2> | |
| <p>Speech-reactive head movement blends with Reachy-side dances and recorded emotions.</p> | |
| </div> | |
| </section> | |
| <h2 class="section-title">Runtime Flow</h2> | |
| <div class="flow" aria-label="Runtime flow"> | |
| <span>Reachy microphone</span> | |
| <span>-> VAD and OpenWebUI transcription</span> | |
| <span>-> OpenWebUI chat completion with Reachy tool specs</span> | |
| <span>-> optional local Reachy tool execution</span> | |
| <span>-> OpenWebUI speech</span> | |
| <span>-> Reachy speaker output plus motion</span> | |
| </div> | |
| <p class="footer-note"> | |
| Install from the repository and launch through Reachy Mini Apps for the full robot runtime. | |
| This hosted Space intentionally does not attempt to control hardware. | |
| </p> | |
| </main> | |
| """ | |
| with gr.Blocks(title="Reachy OpenWebUI") as demo: | |
| gr.HTML(HTML) | |
| if __name__ == "__main__": | |
| demo.launch(css=CSS) | |