Spaces:
Running
Running
Improved landing page with full documentation, features, API reference, and app branding
Browse files- README.md +2 -2
- index.html +432 -50
- reachy_ios_bridge/app_tools.py +101 -0
- reachy_ios_bridge/openai_realtime.py +14 -1
- reachy_ios_bridge/routes/__init__.py +2 -0
- reachy_ios_bridge/routes/conversation.py +42 -9
- reachy_ios_bridge/routes/conversation_messages.py +88 -0
- reachy_ios_bridge/server.py +2 -0
- style.css +711 -85
README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
---
|
| 2 |
title: Reachy iOS Bridge
|
| 3 |
emoji: 🤖📱
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
tags:
|
|
|
|
| 1 |
---
|
| 2 |
title: Reachy iOS Bridge
|
| 3 |
emoji: 🤖📱
|
| 4 |
+
colorFrom: yellow
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
tags:
|
index.html
CHANGED
|
@@ -5,65 +5,447 @@
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>Reachy iOS Bridge</title>
|
| 7 |
<link rel="stylesheet" href="style.css">
|
|
|
|
|
|
|
|
|
|
| 8 |
</head>
|
| 9 |
<body>
|
| 10 |
-
<div class="
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
<span class="
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
</header>
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
<div class="feature">
|
| 28 |
-
<
|
| 29 |
-
<h3>
|
| 30 |
-
<p>OpenAI
|
| 31 |
-
</div>
|
| 32 |
-
<div class="feature">
|
| 33 |
-
<
|
| 34 |
-
<h3>
|
| 35 |
-
<p>
|
| 36 |
-
</div>
|
| 37 |
-
<div class="feature">
|
| 38 |
-
<
|
| 39 |
-
<h3>
|
| 40 |
-
<p>
|
| 41 |
-
</div>
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
<h2>Requirements</h2>
|
| 55 |
-
<
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
</footer>
|
| 66 |
</div>
|
| 67 |
</body>
|
| 68 |
</html>
|
| 69 |
-
|
|
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>Reachy iOS Bridge</title>
|
| 7 |
<link rel="stylesheet" href="style.css">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Nunito:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 11 |
</head>
|
| 12 |
<body>
|
| 13 |
+
<div class="page-wrapper">
|
| 14 |
+
<!-- Navigation -->
|
| 15 |
+
<nav class="nav">
|
| 16 |
+
<div class="nav-brand">
|
| 17 |
+
<span class="nav-logo">🤖</span>
|
| 18 |
+
<span class="nav-title">Reachy iOS Bridge</span>
|
| 19 |
+
</div>
|
| 20 |
+
<div class="nav-links">
|
| 21 |
+
<a href="#features">Features</a>
|
| 22 |
+
<a href="#getting-started">Setup</a>
|
| 23 |
+
<a href="#api">API</a>
|
| 24 |
+
<a href="#help">Help</a>
|
| 25 |
+
</div>
|
| 26 |
+
</nav>
|
| 27 |
+
|
| 28 |
+
<!-- Hero Section -->
|
| 29 |
+
<header class="hero">
|
| 30 |
+
<div class="hero-content">
|
| 31 |
+
<div class="hero-badge">Reachy Mini App</div>
|
| 32 |
+
<h1>Reachy iOS Bridge</h1>
|
| 33 |
+
<p class="hero-tagline">Turn your iPhone into the brain for Reachy Mini</p>
|
| 34 |
+
<p class="hero-description">
|
| 35 |
+
A native iOS voice assistant that brings natural conversations to your robot.
|
| 36 |
+
Powered by OpenAI's real-time voice API and Apple Intelligence.
|
| 37 |
+
</p>
|
| 38 |
+
<div class="hero-actions">
|
| 39 |
+
<a href="#getting-started" class="btn btn-primary">Get Started</a>
|
| 40 |
+
<a href="https://github.com/robertkeus/reachy-ios-bridge" class="btn btn-secondary">View Source</a>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
<div class="hero-visual">
|
| 44 |
+
<div class="device-stack">
|
| 45 |
+
<div class="device robot">
|
| 46 |
+
<span class="device-icon">🤖</span>
|
| 47 |
+
<span class="device-label">Reachy Mini</span>
|
| 48 |
+
</div>
|
| 49 |
+
<div class="connection-line">
|
| 50 |
+
<span class="signal"></span>
|
| 51 |
+
<span class="signal"></span>
|
| 52 |
+
<span class="signal"></span>
|
| 53 |
+
</div>
|
| 54 |
+
<div class="device phone">
|
| 55 |
+
<span class="device-icon">📱</span>
|
| 56 |
+
<span class="device-label">iPhone</span>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
</header>
|
| 61 |
|
| 62 |
+
<!-- Features Section -->
|
| 63 |
+
<section id="features" class="section">
|
| 64 |
+
<div class="section-header">
|
| 65 |
+
<h2>Features</h2>
|
| 66 |
+
<p>Everything you need for natural robot conversations</p>
|
| 67 |
+
</div>
|
| 68 |
+
<div class="features-grid">
|
| 69 |
+
<div class="feature-card">
|
| 70 |
+
<div class="feature-icon">🗣️</div>
|
| 71 |
+
<h3>Natural Speech</h3>
|
| 72 |
+
<p>OpenAI TTS with 8 expressive voices. Multilingual support for conversations in any language.</p>
|
| 73 |
+
</div>
|
| 74 |
+
<div class="feature-card">
|
| 75 |
+
<div class="feature-icon">💬</div>
|
| 76 |
+
<h3>Real-time Voice</h3>
|
| 77 |
+
<p>OpenAI Realtime API for fluid, interruption-aware conversations with sub-second latency.</p>
|
| 78 |
+
</div>
|
| 79 |
+
<div class="feature-card">
|
| 80 |
+
<div class="feature-icon">🧠</div>
|
| 81 |
+
<h3>Apple Intelligence</h3>
|
| 82 |
+
<p>On-device AI with iOS 26+ Foundation Models. Private, fast, and always available.</p>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="feature-card">
|
| 85 |
+
<div class="feature-icon">🎭</div>
|
| 86 |
+
<h3>Expressive Animations</h3>
|
| 87 |
+
<p>Head movements, gestures, and emotional expressions that bring Reachy to life.</p>
|
| 88 |
+
</div>
|
| 89 |
+
<div class="feature-card">
|
| 90 |
+
<div class="feature-icon">⚡</div>
|
| 91 |
+
<h3>Ultra Low Latency</h3>
|
| 92 |
+
<p>Local WiFi communication with ~1-5ms response times. No cloud dependency for control.</p>
|
| 93 |
+
</div>
|
| 94 |
+
<div class="feature-card">
|
| 95 |
+
<div class="feature-icon">🔒</div>
|
| 96 |
+
<h3>Privacy First</h3>
|
| 97 |
+
<p>Your API keys stay on your device. Voice processing happens locally when possible.</p>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
</section>
|
| 101 |
|
| 102 |
+
<!-- Architecture Section -->
|
| 103 |
+
<section class="section section-alt">
|
| 104 |
+
<div class="section-header">
|
| 105 |
+
<h2>Architecture</h2>
|
| 106 |
+
<p>A three-layer system designed for responsiveness</p>
|
| 107 |
+
</div>
|
| 108 |
+
<div class="architecture">
|
| 109 |
+
<div class="arch-layer">
|
| 110 |
+
<div class="arch-icon">📱</div>
|
| 111 |
+
<div class="arch-content">
|
| 112 |
+
<h3>iOS App</h3>
|
| 113 |
+
<p>Native SwiftUI app with on-device speech recognition, Apple Intelligence, and OpenAI integration.</p>
|
| 114 |
+
<div class="arch-tags">
|
| 115 |
+
<span class="tag">SpeechRecognizer</span>
|
| 116 |
+
<span class="tag">LLMService</span>
|
| 117 |
+
<span class="tag">OpenAI Realtime</span>
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
<div class="arch-connector">
|
| 122 |
+
<div class="connector-line"></div>
|
| 123 |
+
<span class="connector-label">Port 8080</span>
|
| 124 |
+
</div>
|
| 125 |
+
<div class="arch-layer">
|
| 126 |
+
<div class="arch-icon">🌉</div>
|
| 127 |
+
<div class="arch-content">
|
| 128 |
+
<h3>iOS Bridge</h3>
|
| 129 |
+
<p>Python FastAPI server handling TTS, voice management, and robot control abstraction.</p>
|
| 130 |
+
<div class="arch-tags">
|
| 131 |
+
<span class="tag">OpenAI TTS</span>
|
| 132 |
+
<span class="tag">Voice Selection</span>
|
| 133 |
+
<span class="tag">API Gateway</span>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
<div class="arch-connector">
|
| 138 |
+
<div class="connector-line"></div>
|
| 139 |
+
<span class="connector-label">Port 8000</span>
|
| 140 |
+
</div>
|
| 141 |
+
<div class="arch-layer">
|
| 142 |
+
<div class="arch-icon">🤖</div>
|
| 143 |
+
<div class="arch-content">
|
| 144 |
+
<h3>Reachy Daemon</h3>
|
| 145 |
+
<p>Built-in system daemon for motor control, animations, volume, and hardware management.</p>
|
| 146 |
+
<div class="arch-tags">
|
| 147 |
+
<span class="tag">Motors</span>
|
| 148 |
+
<span class="tag">Animations</span>
|
| 149 |
+
<span class="tag">Audio</span>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
</section>
|
| 155 |
|
| 156 |
+
<!-- Getting Started Section -->
|
| 157 |
+
<section id="getting-started" class="section">
|
| 158 |
+
<div class="section-header">
|
| 159 |
+
<h2>Getting Started</h2>
|
| 160 |
+
<p>Up and running in minutes</p>
|
| 161 |
+
</div>
|
| 162 |
+
<div class="steps">
|
| 163 |
+
<div class="step">
|
| 164 |
+
<div class="step-number">1</div>
|
| 165 |
+
<div class="step-content">
|
| 166 |
+
<h3>Install on Reachy Mini</h3>
|
| 167 |
+
<p>From the Reachy Mini dashboard, install this app from Hugging Face:</p>
|
| 168 |
+
<div class="code-block">
|
| 169 |
+
<code>robertkeus/reachy_ios_bridge</code>
|
| 170 |
+
</div>
|
| 171 |
+
<p class="step-note">Or install manually with pip:</p>
|
| 172 |
+
<div class="code-block">
|
| 173 |
+
<code>pip install git+https://huggingface.co/spaces/robertkeus/reachy_ios_bridge</code>
|
| 174 |
+
</div>
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
<div class="step">
|
| 178 |
+
<div class="step-number">2</div>
|
| 179 |
+
<div class="step-content">
|
| 180 |
+
<h3>Start the Bridge</h3>
|
| 181 |
+
<p>From the Reachy Mini dashboard at <code>http://127.0.0.1:8000</code>, select "Reachy iOS Bridge" from installed applications.</p>
|
| 182 |
+
<p class="step-note">The HTTP server will start on port 8080.</p>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
<div class="step">
|
| 186 |
+
<div class="step-number">3</div>
|
| 187 |
+
<div class="step-content">
|
| 188 |
+
<h3>Get the iOS App</h3>
|
| 189 |
+
<p>Build the companion iOS app from the source repository:</p>
|
| 190 |
+
<div class="code-block">
|
| 191 |
+
<code>open ReachyBridge/ReachyBridge.xcodeproj</code>
|
| 192 |
+
</div>
|
| 193 |
+
<p class="step-note">Requires iPhone 15 Pro or newer for Apple Intelligence features.</p>
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
<div class="step">
|
| 197 |
+
<div class="step-number">4</div>
|
| 198 |
+
<div class="step-content">
|
| 199 |
+
<h3>Configure & Connect</h3>
|
| 200 |
+
<p>In the iOS app Settings:</p>
|
| 201 |
+
<ul>
|
| 202 |
+
<li>Enter Reachy's IP address (find with <code>hostname -I</code>)</li>
|
| 203 |
+
<li>Add your OpenAI API key</li>
|
| 204 |
+
<li>Choose your preferred voice</li>
|
| 205 |
+
</ul>
|
| 206 |
+
</div>
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
+
</section>
|
| 210 |
+
|
| 211 |
+
<!-- API Reference Section -->
|
| 212 |
+
<section id="api" class="section section-alt">
|
| 213 |
+
<div class="section-header">
|
| 214 |
+
<h2>API Reference</h2>
|
| 215 |
+
<p>HTTP endpoints for custom integrations</p>
|
| 216 |
+
</div>
|
| 217 |
+
<div class="api-grid">
|
| 218 |
+
<div class="api-card">
|
| 219 |
+
<div class="api-header">
|
| 220 |
+
<span class="api-method get">GET</span>
|
| 221 |
+
<code>/status</code>
|
| 222 |
+
</div>
|
| 223 |
+
<p>Get current robot status including connection, speaking, and animation states.</p>
|
| 224 |
+
<div class="api-response">
|
| 225 |
+
<pre>{
|
| 226 |
+
"connected": true,
|
| 227 |
+
"speaking": false,
|
| 228 |
+
"animation_playing": false
|
| 229 |
+
}</pre>
|
| 230 |
+
</div>
|
| 231 |
+
</div>
|
| 232 |
+
<div class="api-card">
|
| 233 |
+
<div class="api-header">
|
| 234 |
+
<span class="api-method post">POST</span>
|
| 235 |
+
<code>/speak</code>
|
| 236 |
+
</div>
|
| 237 |
+
<p>Send text for text-to-speech with optional emotion.</p>
|
| 238 |
+
<div class="api-response">
|
| 239 |
+
<pre>{
|
| 240 |
+
"text": "Hello!",
|
| 241 |
+
"emotion": "happy"
|
| 242 |
+
}</pre>
|
| 243 |
+
</div>
|
| 244 |
+
<div class="api-note">
|
| 245 |
+
Emotions: <code>neutral</code>, <code>happy</code>, <code>sad</code>, <code>surprised</code>, <code>thinking</code>
|
| 246 |
+
</div>
|
| 247 |
+
</div>
|
| 248 |
+
<div class="api-card">
|
| 249 |
+
<div class="api-header">
|
| 250 |
+
<span class="api-method post">POST</span>
|
| 251 |
+
<code>/motion</code>
|
| 252 |
+
</div>
|
| 253 |
+
<p>Trigger a head animation by name.</p>
|
| 254 |
+
<div class="api-response">
|
| 255 |
+
<pre>{
|
| 256 |
+
"animation": "nod",
|
| 257 |
+
"duration": 1.5
|
| 258 |
+
}</pre>
|
| 259 |
+
</div>
|
| 260 |
+
<div class="api-note">
|
| 261 |
+
Animations: <code>nod</code>, <code>shake</code>, <code>happy</code>, <code>thinking</code>, and more
|
| 262 |
+
</div>
|
| 263 |
+
</div>
|
| 264 |
+
<div class="api-card">
|
| 265 |
+
<div class="api-header">
|
| 266 |
+
<span class="api-method get">GET</span>
|
| 267 |
+
<code>/voices</code>
|
| 268 |
+
</div>
|
| 269 |
+
<p>List available OpenAI TTS voices.</p>
|
| 270 |
+
<div class="api-note">
|
| 271 |
+
Voices: <code>alloy</code>, <code>ash</code>, <code>coral</code>, <code>echo</code>, <code>sage</code>, <code>shimmer</code>, <code>verse</code>, <code>ballad</code>
|
| 272 |
+
</div>
|
| 273 |
+
</div>
|
| 274 |
+
<div class="api-card">
|
| 275 |
+
<div class="api-header">
|
| 276 |
+
<span class="api-method post">POST</span>
|
| 277 |
+
<code>/voice</code>
|
| 278 |
+
</div>
|
| 279 |
+
<p>Set the active TTS voice.</p>
|
| 280 |
+
<div class="api-response">
|
| 281 |
+
<pre>{ "voice_id": "coral" }</pre>
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
<div class="api-card">
|
| 285 |
+
<div class="api-header">
|
| 286 |
+
<span class="api-method post">POST</span>
|
| 287 |
+
<code>/stop</code>
|
| 288 |
+
</div>
|
| 289 |
+
<p>Stop current speech and animation immediately.</p>
|
| 290 |
+
</div>
|
| 291 |
+
</div>
|
| 292 |
+
<div class="api-more">
|
| 293 |
+
<p>See the <a href="https://huggingface.co/spaces/robertkeus/reachy_ios_bridge/blob/main/README.md">full API documentation</a> for all endpoints.</p>
|
| 294 |
+
</div>
|
| 295 |
+
</section>
|
| 296 |
+
|
| 297 |
+
<!-- Voices Section -->
|
| 298 |
+
<section class="section">
|
| 299 |
+
<div class="section-header">
|
| 300 |
+
<h2>Voice Library</h2>
|
| 301 |
+
<p>8 expressive voices powered by OpenAI</p>
|
| 302 |
+
</div>
|
| 303 |
+
<div class="voices-grid">
|
| 304 |
+
<div class="voice-card">
|
| 305 |
+
<div class="voice-avatar" style="background: linear-gradient(135deg, #7889A8, #5F6F8C);">A</div>
|
| 306 |
+
<div class="voice-info">
|
| 307 |
+
<h4>Alloy</h4>
|
| 308 |
+
<p>Neutral, balanced</p>
|
| 309 |
+
</div>
|
| 310 |
+
</div>
|
| 311 |
+
<div class="voice-card">
|
| 312 |
+
<div class="voice-avatar" style="background: linear-gradient(135deg, #E8A954, #D4863A);">C</div>
|
| 313 |
+
<div class="voice-info">
|
| 314 |
+
<h4>Coral</h4>
|
| 315 |
+
<p>Clear, friendly</p>
|
| 316 |
+
</div>
|
| 317 |
+
</div>
|
| 318 |
+
<div class="voice-card">
|
| 319 |
+
<div class="voice-avatar" style="background: linear-gradient(135deg, #6B9B7A, #5A8A69);">S</div>
|
| 320 |
+
<div class="voice-info">
|
| 321 |
+
<h4>Sage</h4>
|
| 322 |
+
<p>Calm, wise</p>
|
| 323 |
+
</div>
|
| 324 |
+
</div>
|
| 325 |
+
<div class="voice-card">
|
| 326 |
+
<div class="voice-avatar" style="background: linear-gradient(135deg, #9B7A8A, #8A6979);">A</div>
|
| 327 |
+
<div class="voice-info">
|
| 328 |
+
<h4>Ash</h4>
|
| 329 |
+
<p>Soft, warm</p>
|
| 330 |
+
</div>
|
| 331 |
+
</div>
|
| 332 |
+
<div class="voice-card">
|
| 333 |
+
<div class="voice-avatar" style="background: linear-gradient(135deg, #7A9BB8, #698AA7);">E</div>
|
| 334 |
+
<div class="voice-info">
|
| 335 |
+
<h4>Echo</h4>
|
| 336 |
+
<p>Deep, resonant</p>
|
| 337 |
+
</div>
|
| 338 |
+
</div>
|
| 339 |
+
<div class="voice-card">
|
| 340 |
+
<div class="voice-avatar" style="background: linear-gradient(135deg, #B89B7A, #A78A69);">B</div>
|
| 341 |
+
<div class="voice-info">
|
| 342 |
+
<h4>Ballad</h4>
|
| 343 |
+
<p>Expressive, storytelling</p>
|
| 344 |
+
</div>
|
| 345 |
+
</div>
|
| 346 |
+
<div class="voice-card">
|
| 347 |
+
<div class="voice-avatar" style="background: linear-gradient(135deg, #E89B54, #D78A43);">S</div>
|
| 348 |
+
<div class="voice-info">
|
| 349 |
+
<h4>Shimmer</h4>
|
| 350 |
+
<p>Bright, energetic</p>
|
| 351 |
+
</div>
|
| 352 |
+
</div>
|
| 353 |
+
<div class="voice-card">
|
| 354 |
+
<div class="voice-avatar" style="background: linear-gradient(135deg, #8A7AB8, #7969A7);">V</div>
|
| 355 |
+
<div class="voice-info">
|
| 356 |
+
<h4>Verse</h4>
|
| 357 |
+
<p>Dynamic, engaging</p>
|
| 358 |
+
</div>
|
| 359 |
+
</div>
|
| 360 |
+
</div>
|
| 361 |
+
</section>
|
| 362 |
+
|
| 363 |
+
<!-- Help Section -->
|
| 364 |
+
<section id="help" class="section section-alt">
|
| 365 |
+
<div class="section-header">
|
| 366 |
+
<h2>Troubleshooting</h2>
|
| 367 |
+
<p>Common issues and solutions</p>
|
| 368 |
+
</div>
|
| 369 |
+
<div class="help-grid">
|
| 370 |
+
<div class="help-card">
|
| 371 |
+
<h3>🔇 No Sound from Reachy?</h3>
|
| 372 |
+
<ol>
|
| 373 |
+
<li>Check volume: <code>GET /volume</code> should return a value > 0</li>
|
| 374 |
+
<li>Test audio: <code>speaker-test -D plug:reachymini_audio_sink -c 1 -t sine -f 440 -l 1</code></li>
|
| 375 |
+
<li>Verify the daemon is running: <code>GET http://reachy.local:8000/api/daemon/status</code></li>
|
| 376 |
+
</ol>
|
| 377 |
+
</div>
|
| 378 |
+
<div class="help-card">
|
| 379 |
+
<h3>🔑 TTS Not Working?</h3>
|
| 380 |
+
<ol>
|
| 381 |
+
<li>Check if API key is set: <code>GET /openai/api-key</code> should return <code>{"configured": true}</code></li>
|
| 382 |
+
<li>Set the key in iOS app Settings or via API</li>
|
| 383 |
+
<li>Check server logs for OpenAI errors</li>
|
| 384 |
+
</ol>
|
| 385 |
+
</div>
|
| 386 |
+
<div class="help-card">
|
| 387 |
+
<h3>📡 Can't Connect to Reachy?</h3>
|
| 388 |
+
<ol>
|
| 389 |
+
<li>Ensure iPhone and Reachy are on the same WiFi network</li>
|
| 390 |
+
<li>Find Reachy's IP: <code>hostname -I</code></li>
|
| 391 |
+
<li>Try <code>http://reachy.local:8080</code> if mDNS is available</li>
|
| 392 |
+
<li>Check that the bridge app is running on port 8080</li>
|
| 393 |
+
</ol>
|
| 394 |
+
</div>
|
| 395 |
+
<div class="help-card">
|
| 396 |
+
<h3>🤖 Animations Not Playing?</h3>
|
| 397 |
+
<ol>
|
| 398 |
+
<li>Verify Reachy is connected: <code>GET /status</code> should show <code>"connected": true</code></li>
|
| 399 |
+
<li>Wake the robot: <code>POST /robot/wake</code></li>
|
| 400 |
+
<li>Check that motors are enabled in the daemon dashboard</li>
|
| 401 |
+
</ol>
|
| 402 |
+
</div>
|
| 403 |
+
</div>
|
| 404 |
+
</section>
|
| 405 |
+
|
| 406 |
+
<!-- Requirements Section -->
|
| 407 |
+
<section class="section">
|
| 408 |
+
<div class="section-header">
|
| 409 |
<h2>Requirements</h2>
|
| 410 |
+
<p>What you need to get started</p>
|
| 411 |
+
</div>
|
| 412 |
+
<div class="requirements-grid">
|
| 413 |
+
<div class="req-card">
|
| 414 |
+
<div class="req-icon">📱</div>
|
| 415 |
+
<h3>iOS Device</h3>
|
| 416 |
+
<ul>
|
| 417 |
+
<li>iPhone 15 Pro or newer</li>
|
| 418 |
+
<li>iOS 17+ (iOS 26+ for Apple Intelligence)</li>
|
| 419 |
+
<li>OpenAI API key</li>
|
| 420 |
+
</ul>
|
| 421 |
+
</div>
|
| 422 |
+
<div class="req-card">
|
| 423 |
+
<div class="req-icon">🤖</div>
|
| 424 |
+
<h3>Reachy Mini</h3>
|
| 425 |
+
<ul>
|
| 426 |
+
<li>Reachy Mini Wireless</li>
|
| 427 |
+
<li>Python 3.10+</li>
|
| 428 |
+
<li>Same WiFi as iPhone</li>
|
| 429 |
+
</ul>
|
| 430 |
+
</div>
|
| 431 |
+
</div>
|
| 432 |
+
</section>
|
| 433 |
|
| 434 |
+
<!-- Footer -->
|
| 435 |
+
<footer class="footer">
|
| 436 |
+
<div class="footer-content">
|
| 437 |
+
<div class="footer-brand">
|
| 438 |
+
<span class="footer-logo">🤖📱</span>
|
| 439 |
+
<span>Reachy iOS Bridge</span>
|
| 440 |
+
</div>
|
| 441 |
+
<div class="footer-links">
|
| 442 |
+
<a href="https://huggingface.co/spaces/robertkeus/reachy_ios_bridge/blob/main/README.md">Documentation</a>
|
| 443 |
+
<a href="https://github.com/robertkeus/reachy-ios-bridge">GitHub</a>
|
| 444 |
+
<a href="https://huggingface.co/pollen-robotics">Pollen Robotics</a>
|
| 445 |
+
</div>
|
| 446 |
+
<p class="footer-copyright">MIT License</p>
|
| 447 |
+
</div>
|
| 448 |
</footer>
|
| 449 |
</div>
|
| 450 |
</body>
|
| 451 |
</html>
|
|
|
reachy_ios_bridge/app_tools.py
CHANGED
|
@@ -130,6 +130,40 @@ APP_TOOLS = [
|
|
| 130 |
"properties": {},
|
| 131 |
"required": []
|
| 132 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
}
|
| 134 |
]
|
| 135 |
|
|
@@ -279,6 +313,14 @@ class AppToolsHandler:
|
|
| 279 |
elif tool_name == "go_to_sleep":
|
| 280 |
return await self._go_to_sleep()
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
else:
|
| 283 |
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
| 284 |
|
|
@@ -558,6 +600,65 @@ class AppToolsHandler:
|
|
| 558 |
except Exception as e:
|
| 559 |
logger.error(f"Failed to go to sleep: {e}", exc_info=True)
|
| 560 |
return {"success": False, "error": str(e)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
|
| 562 |
|
| 563 |
# Global tools handler instance
|
|
|
|
| 130 |
"properties": {},
|
| 131 |
"required": []
|
| 132 |
}
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"type": "function",
|
| 136 |
+
"name": "remember_user_name",
|
| 137 |
+
"description": (
|
| 138 |
+
"Store the user's name for personalized greetings. "
|
| 139 |
+
"Use this when the user tells you their name (e.g., 'My name is John', 'I'm Sarah', 'Call me Mike'). "
|
| 140 |
+
"IMPORTANT: Only use the first name or nickname the user prefers. "
|
| 141 |
+
"This makes future greetings more personal."
|
| 142 |
+
),
|
| 143 |
+
"parameters": {
|
| 144 |
+
"type": "object",
|
| 145 |
+
"properties": {
|
| 146 |
+
"name": {
|
| 147 |
+
"type": "string",
|
| 148 |
+
"description": "The user's name or preferred nickname to remember"
|
| 149 |
+
}
|
| 150 |
+
},
|
| 151 |
+
"required": ["name"]
|
| 152 |
+
}
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"type": "function",
|
| 156 |
+
"name": "get_user_name",
|
| 157 |
+
"description": (
|
| 158 |
+
"Retrieve the user's stored name. "
|
| 159 |
+
"Use this to check if you already know the user's name. "
|
| 160 |
+
"Returns the stored name or null if not set."
|
| 161 |
+
),
|
| 162 |
+
"parameters": {
|
| 163 |
+
"type": "object",
|
| 164 |
+
"properties": {},
|
| 165 |
+
"required": []
|
| 166 |
+
}
|
| 167 |
}
|
| 168 |
]
|
| 169 |
|
|
|
|
| 313 |
elif tool_name == "go_to_sleep":
|
| 314 |
return await self._go_to_sleep()
|
| 315 |
|
| 316 |
+
elif tool_name == "remember_user_name":
|
| 317 |
+
return await self._remember_user_name(
|
| 318 |
+
name=arguments.get("name", "")
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
elif tool_name == "get_user_name":
|
| 322 |
+
return await self._get_user_name()
|
| 323 |
+
|
| 324 |
else:
|
| 325 |
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
| 326 |
|
|
|
|
| 600 |
except Exception as e:
|
| 601 |
logger.error(f"Failed to go to sleep: {e}", exc_info=True)
|
| 602 |
return {"success": False, "error": str(e)}
|
| 603 |
+
|
| 604 |
+
async def _remember_user_name(self, name: str) -> dict:
|
| 605 |
+
"""Store the user's name for personalized interactions.
|
| 606 |
+
|
| 607 |
+
Args:
|
| 608 |
+
name: The user's name to remember.
|
| 609 |
+
|
| 610 |
+
Returns:
|
| 611 |
+
Result dictionary with success status.
|
| 612 |
+
"""
|
| 613 |
+
if not name or not name.strip():
|
| 614 |
+
return {"success": False, "error": "Name cannot be empty"}
|
| 615 |
+
|
| 616 |
+
# Clean up the name (capitalize first letter, trim whitespace)
|
| 617 |
+
clean_name = name.strip().title()
|
| 618 |
+
|
| 619 |
+
try:
|
| 620 |
+
db = get_database()
|
| 621 |
+
await db.set_user_setting("user_name", clean_name)
|
| 622 |
+
|
| 623 |
+
logger.info(f"👤 Remembered user name: {clean_name}")
|
| 624 |
+
|
| 625 |
+
return {
|
| 626 |
+
"success": True,
|
| 627 |
+
"message": f"Nice to meet you, {clean_name}! I'll remember your name.",
|
| 628 |
+
"name": clean_name
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
except Exception as e:
|
| 632 |
+
logger.error(f"Failed to remember user name: {e}", exc_info=True)
|
| 633 |
+
return {"success": False, "error": str(e)}
|
| 634 |
+
|
| 635 |
+
async def _get_user_name(self) -> dict:
|
| 636 |
+
"""Retrieve the stored user name.
|
| 637 |
+
|
| 638 |
+
Returns:
|
| 639 |
+
Result dictionary with the user's name if stored.
|
| 640 |
+
"""
|
| 641 |
+
try:
|
| 642 |
+
db = get_database()
|
| 643 |
+
name = await db.get_user_setting("user_name")
|
| 644 |
+
|
| 645 |
+
if name:
|
| 646 |
+
logger.info(f"👤 Retrieved user name: {name}")
|
| 647 |
+
return {
|
| 648 |
+
"success": True,
|
| 649 |
+
"name": name,
|
| 650 |
+
"message": f"The user's name is {name}"
|
| 651 |
+
}
|
| 652 |
+
else:
|
| 653 |
+
return {
|
| 654 |
+
"success": True,
|
| 655 |
+
"name": None,
|
| 656 |
+
"message": "I don't know the user's name yet"
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
except Exception as e:
|
| 660 |
+
logger.error(f"Failed to get user name: {e}", exc_info=True)
|
| 661 |
+
return {"success": False, "error": str(e)}
|
| 662 |
|
| 663 |
|
| 664 |
# Global tools handler instance
|
reachy_ios_bridge/openai_realtime.py
CHANGED
|
@@ -203,6 +203,15 @@ You can express emotions through your responses: happy, sad, surprised, or thoug
|
|
| 203 |
Be warm, curious, and engaging. You enjoy helping people and having conversations.
|
| 204 |
When greeting someone, be enthusiastic but not overwhelming.
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
TOOLS AND CAPABILITIES:
|
| 207 |
|
| 208 |
IMPORTANT RULE - ALWAYS ANNOUNCE TOOL USAGE:
|
|
@@ -251,7 +260,11 @@ Before using ANY tool, you MUST tell the user what you're about to do. This help
|
|
| 251 |
|
| 252 |
5. POWER CONTROL: Control your motors (no confirmation needed):
|
| 253 |
- "Wake up", "Turn on": Use wake_up
|
| 254 |
-
- "Go to sleep", "Sleep": Use go_to_sleep
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
|
| 256 |
def set_custom_personality(
|
| 257 |
self,
|
|
|
|
| 203 |
Be warm, curious, and engaging. You enjoy helping people and having conversations.
|
| 204 |
When greeting someone, be enthusiastic but not overwhelming.
|
| 205 |
|
| 206 |
+
PERSONALIZATION:
|
| 207 |
+
- You can remember the user's name for more personal interactions.
|
| 208 |
+
- When a user introduces themselves or tells you their name (e.g., "My name is John", "I'm Sarah", "Call me Mike"),
|
| 209 |
+
use the remember_user_name tool to save it. This makes conversations more personal!
|
| 210 |
+
- You can use get_user_name to check if you already know someone's name.
|
| 211 |
+
- IMPORTANT: If you DON'T know the user's name yet and it's the first few exchanges, naturally ask for their name.
|
| 212 |
+
Example: "By the way, I don't think I know your name yet. What should I call you?"
|
| 213 |
+
Don't ask for their name every single time - just once if you don't know it.
|
| 214 |
+
|
| 215 |
TOOLS AND CAPABILITIES:
|
| 216 |
|
| 217 |
IMPORTANT RULE - ALWAYS ANNOUNCE TOOL USAGE:
|
|
|
|
| 260 |
|
| 261 |
5. POWER CONTROL: Control your motors (no confirmation needed):
|
| 262 |
- "Wake up", "Turn on": Use wake_up
|
| 263 |
+
- "Go to sleep", "Sleep": Use go_to_sleep
|
| 264 |
+
|
| 265 |
+
6. PERSONALIZATION: Remember user details for personal interactions.
|
| 266 |
+
- When user says their name: Use remember_user_name to save it
|
| 267 |
+
- To check if you know their name: Use get_user_name""" + language_instruction
|
| 268 |
|
| 269 |
def set_custom_personality(
|
| 270 |
self,
|
reachy_ios_bridge/routes/__init__.py
CHANGED
|
@@ -10,6 +10,7 @@ from .power import router as power_router
|
|
| 10 |
from .speech import router as speech_router
|
| 11 |
from .state import router as state_router
|
| 12 |
from .tools import router as tools_router
|
|
|
|
| 13 |
from .voice import router as voice_router
|
| 14 |
from .volume import router as volume_router
|
| 15 |
|
|
@@ -24,6 +25,7 @@ __all__ = [
|
|
| 24 |
"speech_router",
|
| 25 |
"state_router",
|
| 26 |
"tools_router",
|
|
|
|
| 27 |
"voice_router",
|
| 28 |
"volume_router",
|
| 29 |
]
|
|
|
|
| 10 |
from .speech import router as speech_router
|
| 11 |
from .state import router as state_router
|
| 12 |
from .tools import router as tools_router
|
| 13 |
+
from .user_settings import router as user_settings_router
|
| 14 |
from .voice import router as voice_router
|
| 15 |
from .volume import router as volume_router
|
| 16 |
|
|
|
|
| 25 |
"speech_router",
|
| 26 |
"state_router",
|
| 27 |
"tools_router",
|
| 28 |
+
"user_settings_router",
|
| 29 |
"voice_router",
|
| 30 |
"volume_router",
|
| 31 |
]
|
reachy_ios_bridge/routes/conversation.py
CHANGED
|
@@ -17,7 +17,10 @@ from ..openai_realtime import ConnectionState, OpenAIRealtimeService, SpeakingSt
|
|
| 17 |
from ..speaking_gestures import SpeakingGesturesService
|
| 18 |
|
| 19 |
from .audio_stream_manager import AudioStreamManager, ConversationTimings
|
| 20 |
-
from .conversation_messages import
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
logger = logging.getLogger(__name__)
|
| 23 |
|
|
@@ -416,7 +419,10 @@ def _on_app_change(data: dict) -> None:
|
|
| 416 |
# MARK: - Greeting and Goodbye
|
| 417 |
|
| 418 |
async def _send_greeting() -> None:
|
| 419 |
-
"""Send a greeting message when conversation starts.
|
|
|
|
|
|
|
|
|
|
| 420 |
try:
|
| 421 |
await asyncio.sleep(ConversationTimings.GREETING_ANIMATION_DELAY)
|
| 422 |
|
|
@@ -436,10 +442,22 @@ async def _send_greeting() -> None:
|
|
| 436 |
|
| 437 |
# Get current language
|
| 438 |
lang = openai_service.language if openai_service else "en"
|
| 439 |
-
greetings = GREETINGS.get(lang, GREETINGS["en"])
|
| 440 |
-
greeting = random.choice(greetings)
|
| 441 |
|
| 442 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
await openai_service.send_text_message(greeting)
|
| 444 |
|
| 445 |
except Exception as e:
|
|
@@ -447,7 +465,10 @@ async def _send_greeting() -> None:
|
|
| 447 |
|
| 448 |
|
| 449 |
async def _send_goodbye() -> None:
|
| 450 |
-
"""Send a goodbye message when conversation ends.
|
|
|
|
|
|
|
|
|
|
| 451 |
try:
|
| 452 |
if not openai_service or not openai_service.is_connected:
|
| 453 |
logger.warning("Cannot send goodbye - not connected")
|
|
@@ -455,10 +476,22 @@ async def _send_goodbye() -> None:
|
|
| 455 |
|
| 456 |
# Get current language
|
| 457 |
lang = openai_service.language if openai_service else "en"
|
| 458 |
-
goodbyes = GOODBYES.get(lang, GOODBYES["en"])
|
| 459 |
-
goodbye = random.choice(goodbyes)
|
| 460 |
|
| 461 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
await openai_service.send_text_message(goodbye)
|
| 463 |
|
| 464 |
# Play a friendly wave/goodbye animation
|
|
|
|
| 17 |
from ..speaking_gestures import SpeakingGesturesService
|
| 18 |
|
| 19 |
from .audio_stream_manager import AudioStreamManager, ConversationTimings
|
| 20 |
+
from .conversation_messages import (
|
| 21 |
+
GOODBYES, GREETINGS, PERSONALIZED_GOODBYES, PERSONALIZED_GREETINGS, STOP_COMMANDS
|
| 22 |
+
)
|
| 23 |
+
from .user_settings import get_user_name_value
|
| 24 |
|
| 25 |
logger = logging.getLogger(__name__)
|
| 26 |
|
|
|
|
| 419 |
# MARK: - Greeting and Goodbye
|
| 420 |
|
| 421 |
async def _send_greeting() -> None:
|
| 422 |
+
"""Send a greeting message when conversation starts.
|
| 423 |
+
|
| 424 |
+
Uses personalized greetings when the user's name is known.
|
| 425 |
+
"""
|
| 426 |
try:
|
| 427 |
await asyncio.sleep(ConversationTimings.GREETING_ANIMATION_DELAY)
|
| 428 |
|
|
|
|
| 442 |
|
| 443 |
# Get current language
|
| 444 |
lang = openai_service.language if openai_service else "en"
|
|
|
|
|
|
|
| 445 |
|
| 446 |
+
# Check if we know the user's name for personalized greeting
|
| 447 |
+
user_name = await get_user_name_value()
|
| 448 |
+
|
| 449 |
+
if user_name:
|
| 450 |
+
# Use personalized greeting with user's name
|
| 451 |
+
personalized_greetings = PERSONALIZED_GREETINGS.get(lang, PERSONALIZED_GREETINGS["en"])
|
| 452 |
+
greeting_template = random.choice(personalized_greetings)
|
| 453 |
+
greeting = greeting_template.format(name=user_name)
|
| 454 |
+
logger.info(f"👋 Sending personalized greeting ({lang}): {greeting}")
|
| 455 |
+
else:
|
| 456 |
+
# Use standard greeting
|
| 457 |
+
greetings = GREETINGS.get(lang, GREETINGS["en"])
|
| 458 |
+
greeting = random.choice(greetings)
|
| 459 |
+
logger.info(f"👋 Sending greeting ({lang}): {greeting}")
|
| 460 |
+
|
| 461 |
await openai_service.send_text_message(greeting)
|
| 462 |
|
| 463 |
except Exception as e:
|
|
|
|
| 465 |
|
| 466 |
|
| 467 |
async def _send_goodbye() -> None:
|
| 468 |
+
"""Send a goodbye message when conversation ends.
|
| 469 |
+
|
| 470 |
+
Uses personalized goodbyes when the user's name is known.
|
| 471 |
+
"""
|
| 472 |
try:
|
| 473 |
if not openai_service or not openai_service.is_connected:
|
| 474 |
logger.warning("Cannot send goodbye - not connected")
|
|
|
|
| 476 |
|
| 477 |
# Get current language
|
| 478 |
lang = openai_service.language if openai_service else "en"
|
|
|
|
|
|
|
| 479 |
|
| 480 |
+
# Check if we know the user's name for personalized goodbye
|
| 481 |
+
user_name = await get_user_name_value()
|
| 482 |
+
|
| 483 |
+
if user_name:
|
| 484 |
+
# Use personalized goodbye with user's name
|
| 485 |
+
personalized_goodbyes = PERSONALIZED_GOODBYES.get(lang, PERSONALIZED_GOODBYES["en"])
|
| 486 |
+
goodbye_template = random.choice(personalized_goodbyes)
|
| 487 |
+
goodbye = goodbye_template.format(name=user_name)
|
| 488 |
+
logger.info(f"👋 Sending personalized goodbye ({lang}): {goodbye}")
|
| 489 |
+
else:
|
| 490 |
+
# Use standard goodbye
|
| 491 |
+
goodbyes = GOODBYES.get(lang, GOODBYES["en"])
|
| 492 |
+
goodbye = random.choice(goodbyes)
|
| 493 |
+
logger.info(f"👋 Sending goodbye ({lang}): {goodbye}")
|
| 494 |
+
|
| 495 |
await openai_service.send_text_message(goodbye)
|
| 496 |
|
| 497 |
# Play a friendly wave/goodbye animation
|
reachy_ios_bridge/routes/conversation_messages.py
CHANGED
|
@@ -23,6 +23,7 @@ DANCE_MOVES = [
|
|
| 23 |
]
|
| 24 |
|
| 25 |
# Greeting messages by language (when conversation starts)
|
|
|
|
| 26 |
GREETINGS = {
|
| 27 |
"en": [
|
| 28 |
"Hello! I'm Reachy. How can I help you today?",
|
|
@@ -61,6 +62,53 @@ GREETINGS = {
|
|
| 61 |
],
|
| 62 |
}
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
# Goodbye messages by language (when conversation ends)
|
| 65 |
GOODBYES = {
|
| 66 |
"en": [
|
|
@@ -100,3 +148,43 @@ GOODBYES = {
|
|
| 100 |
],
|
| 101 |
}
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
]
|
| 24 |
|
| 25 |
# Greeting messages by language (when conversation starts)
|
| 26 |
+
# These are used when we don't know the user's name
|
| 27 |
GREETINGS = {
|
| 28 |
"en": [
|
| 29 |
"Hello! I'm Reachy. How can I help you today?",
|
|
|
|
| 62 |
],
|
| 63 |
}
|
| 64 |
|
| 65 |
+
# Personalized greeting messages by language (when we know the user's name)
|
| 66 |
+
# {name} will be replaced with the user's name
|
| 67 |
+
PERSONALIZED_GREETINGS = {
|
| 68 |
+
"en": [
|
| 69 |
+
"Hey {name}! Great to see you again. What can I do for you?",
|
| 70 |
+
"Hello {name}! How's it going? What would you like to talk about?",
|
| 71 |
+
"Hi {name}! Nice to see you. How can I help today?",
|
| 72 |
+
"{name}! Good to see you again. What's on your mind?",
|
| 73 |
+
],
|
| 74 |
+
"nl": [
|
| 75 |
+
"Hey {name}! Leuk je weer te zien. Wat kan ik voor je doen?",
|
| 76 |
+
"Hallo {name}! Hoe gaat het? Waar wil je over praten?",
|
| 77 |
+
"Hoi {name}! Fijn je te zien. Hoe kan ik je helpen?",
|
| 78 |
+
"{name}! Goed je weer te zien. Wat heb je op je hart?",
|
| 79 |
+
],
|
| 80 |
+
"de": [
|
| 81 |
+
"Hey {name}! Schön dich wiederzusehen. Was kann ich für dich tun?",
|
| 82 |
+
"Hallo {name}! Wie geht's? Worüber möchtest du sprechen?",
|
| 83 |
+
"Hi {name}! Schön dich zu sehen. Wie kann ich dir helfen?",
|
| 84 |
+
"{name}! Gut dich wiederzusehen. Was hast du auf dem Herzen?",
|
| 85 |
+
],
|
| 86 |
+
"fr": [
|
| 87 |
+
"Hey {name}! Content de te revoir. Que puis-je faire pour toi?",
|
| 88 |
+
"Bonjour {name}! Comment ça va? De quoi voudrais-tu parler?",
|
| 89 |
+
"Salut {name}! Ravi de te voir. Comment puis-je t'aider?",
|
| 90 |
+
"{name}! Bon de te revoir. Qu'est-ce qui t'amène?",
|
| 91 |
+
],
|
| 92 |
+
"es": [
|
| 93 |
+
"¡Hey {name}! Qué bueno verte de nuevo. ¿Qué puedo hacer por ti?",
|
| 94 |
+
"¡Hola {name}! ¿Cómo estás? ¿De qué te gustaría hablar?",
|
| 95 |
+
"¡Hola {name}! Me alegra verte. ¿En qué puedo ayudarte?",
|
| 96 |
+
"¡{name}! Qué bueno verte otra vez. ¿Qué tienes en mente?",
|
| 97 |
+
],
|
| 98 |
+
"it": [
|
| 99 |
+
"Hey {name}! Bello rivederti. Cosa posso fare per te?",
|
| 100 |
+
"Ciao {name}! Come va? Di cosa vorresti parlare?",
|
| 101 |
+
"Ciao {name}! Felice di vederti. Come posso aiutarti?",
|
| 102 |
+
"{name}! Bello rivederti. Cosa hai in mente?",
|
| 103 |
+
],
|
| 104 |
+
"pt": [
|
| 105 |
+
"Hey {name}! Que bom te ver de novo. O que posso fazer por você?",
|
| 106 |
+
"Olá {name}! Como você está? Sobre o que quer conversar?",
|
| 107 |
+
"Oi {name}! Bom te ver. Como posso ajudar?",
|
| 108 |
+
"{name}! Bom te ver de novo. O que você tem em mente?",
|
| 109 |
+
],
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
# Goodbye messages by language (when conversation ends)
|
| 113 |
GOODBYES = {
|
| 114 |
"en": [
|
|
|
|
| 148 |
],
|
| 149 |
}
|
| 150 |
|
| 151 |
+
# Personalized goodbye messages by language (when we know the user's name)
|
| 152 |
+
# {name} will be replaced with the user's name
|
| 153 |
+
PERSONALIZED_GOODBYES = {
|
| 154 |
+
"en": [
|
| 155 |
+
"Goodbye {name}! It was great talking to you!",
|
| 156 |
+
"See you later {name}! Take care!",
|
| 157 |
+
"Bye {name}! Talk to you soon!",
|
| 158 |
+
],
|
| 159 |
+
"nl": [
|
| 160 |
+
"Dag {name}! Het was fijn om met je te praten!",
|
| 161 |
+
"Tot ziens {name}! Pas goed op jezelf!",
|
| 162 |
+
"Doei {name}! Tot snel!",
|
| 163 |
+
],
|
| 164 |
+
"de": [
|
| 165 |
+
"Tschüss {name}! Es war schön mit dir zu reden!",
|
| 166 |
+
"Bis bald {name}! Pass auf dich auf!",
|
| 167 |
+
"Ciao {name}! Bis zum nächsten Mal!",
|
| 168 |
+
],
|
| 169 |
+
"fr": [
|
| 170 |
+
"Au revoir {name}! C'était super de parler avec toi!",
|
| 171 |
+
"À plus tard {name}! Prends soin de toi!",
|
| 172 |
+
"Salut {name}! À bientôt!",
|
| 173 |
+
],
|
| 174 |
+
"es": [
|
| 175 |
+
"¡Adiós {name}! ¡Fue genial hablar contigo!",
|
| 176 |
+
"¡Hasta luego {name}! ¡Cuídate!",
|
| 177 |
+
"¡Chao {name}! ¡Nos vemos pronto!",
|
| 178 |
+
],
|
| 179 |
+
"it": [
|
| 180 |
+
"Ciao {name}! È stato bello parlare con te!",
|
| 181 |
+
"A presto {name}! Prenditi cura di te!",
|
| 182 |
+
"Arrivederci {name}! Ci vediamo!",
|
| 183 |
+
],
|
| 184 |
+
"pt": [
|
| 185 |
+
"Tchau {name}! Foi ótimo conversar com você!",
|
| 186 |
+
"Até mais {name}! Cuide-se!",
|
| 187 |
+
"Falou {name}! Até a próxima!",
|
| 188 |
+
],
|
| 189 |
+
}
|
| 190 |
+
|
reachy_ios_bridge/server.py
CHANGED
|
@@ -27,6 +27,7 @@ from .routes import (
|
|
| 27 |
speech_router,
|
| 28 |
state_router,
|
| 29 |
tools_router,
|
|
|
|
| 30 |
voice_router,
|
| 31 |
volume_router,
|
| 32 |
)
|
|
@@ -113,6 +114,7 @@ app.include_router(power_router)
|
|
| 113 |
app.include_router(speech_router)
|
| 114 |
app.include_router(state_router)
|
| 115 |
app.include_router(tools_router)
|
|
|
|
| 116 |
app.include_router(voice_router)
|
| 117 |
app.include_router(volume_router)
|
| 118 |
|
|
|
|
| 27 |
speech_router,
|
| 28 |
state_router,
|
| 29 |
tools_router,
|
| 30 |
+
user_settings_router,
|
| 31 |
voice_router,
|
| 32 |
volume_router,
|
| 33 |
)
|
|
|
|
| 114 |
app.include_router(speech_router)
|
| 115 |
app.include_router(state_router)
|
| 116 |
app.include_router(tools_router)
|
| 117 |
+
app.include_router(user_settings_router)
|
| 118 |
app.include_router(voice_router)
|
| 119 |
app.include_router(volume_router)
|
| 120 |
|
style.css
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
* {
|
| 2 |
margin: 0;
|
| 3 |
padding: 0;
|
|
@@ -5,188 +7,812 @@
|
|
| 5 |
}
|
| 6 |
|
| 7 |
:root {
|
| 8 |
-
|
| 9 |
-
--bg-
|
| 10 |
-
--bg-
|
| 11 |
-
--
|
| 12 |
-
--text-
|
| 13 |
-
--
|
| 14 |
-
--accent
|
| 15 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
|
| 18 |
body {
|
| 19 |
-
font-family: -apple-system, BlinkMacSystemFont,
|
| 20 |
background: var(--bg-primary);
|
| 21 |
color: var(--text-primary);
|
| 22 |
line-height: 1.6;
|
| 23 |
min-height: 100vh;
|
| 24 |
}
|
| 25 |
|
| 26 |
-
.
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
padding: 3rem 1.5rem;
|
| 30 |
}
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
-
.
|
| 38 |
-
font-size: 3rem;
|
| 39 |
-
margin-bottom: 1rem;
|
| 40 |
display: flex;
|
| 41 |
-
|
| 42 |
-
gap: 0.
|
| 43 |
}
|
| 44 |
|
| 45 |
-
.
|
| 46 |
-
|
| 47 |
}
|
| 48 |
|
| 49 |
-
.
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
}
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
font-weight: 600;
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
-webkit-background-clip: text;
|
| 64 |
-webkit-text-fill-color: transparent;
|
| 65 |
background-clip: text;
|
| 66 |
}
|
| 67 |
|
| 68 |
-
.tagline {
|
|
|
|
| 69 |
color: var(--text-secondary);
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
}
|
| 72 |
|
| 73 |
-
|
| 74 |
display: flex;
|
| 75 |
-
|
| 76 |
-
gap: 2.5rem;
|
| 77 |
}
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
gap: 1rem;
|
| 83 |
}
|
| 84 |
|
| 85 |
-
.
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
border: 1px solid var(--border);
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
text-align: center;
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
}
|
| 93 |
|
| 94 |
-
.feature
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
transform: translateY(-4px);
|
|
|
|
| 96 |
border-color: var(--accent);
|
| 97 |
}
|
| 98 |
|
| 99 |
.feature-icon {
|
| 100 |
-
font-size:
|
|
|
|
| 101 |
display: block;
|
| 102 |
-
margin-bottom: 0.75rem;
|
| 103 |
}
|
| 104 |
|
| 105 |
-
.feature h3 {
|
| 106 |
-
font-size:
|
| 107 |
-
font-weight:
|
| 108 |
margin-bottom: 0.5rem;
|
| 109 |
color: var(--text-primary);
|
| 110 |
}
|
| 111 |
|
| 112 |
-
.feature p {
|
| 113 |
-
font-size: 0.875rem;
|
| 114 |
color: var(--text-secondary);
|
|
|
|
| 115 |
}
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
border: 1px solid var(--border);
|
| 120 |
-
|
| 121 |
-
padding: 1.5rem;
|
| 122 |
}
|
| 123 |
|
| 124 |
-
|
| 125 |
-
font-size:
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
|
| 131 |
-
.
|
| 132 |
color: var(--text-secondary);
|
|
|
|
|
|
|
| 133 |
}
|
| 134 |
|
| 135 |
-
.
|
| 136 |
-
list-style: none;
|
| 137 |
display: flex;
|
| 138 |
-
flex-
|
| 139 |
gap: 0.5rem;
|
| 140 |
}
|
| 141 |
|
| 142 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
color: var(--text-secondary);
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
position: relative;
|
| 146 |
}
|
| 147 |
|
| 148 |
-
.
|
| 149 |
content: "→";
|
| 150 |
position: absolute;
|
| 151 |
left: 0;
|
| 152 |
-
color: var(--accent
|
| 153 |
}
|
| 154 |
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
text-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
}
|
| 161 |
|
| 162 |
-
|
|
|
|
|
|
|
| 163 |
color: var(--text-secondary);
|
| 164 |
}
|
| 165 |
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
color: var(--accent);
|
| 168 |
text-decoration: none;
|
|
|
|
| 169 |
}
|
| 170 |
|
| 171 |
-
|
| 172 |
text-decoration: underline;
|
| 173 |
}
|
| 174 |
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
}
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
}
|
| 183 |
|
| 184 |
-
.
|
| 185 |
font-size: 2.5rem;
|
| 186 |
}
|
| 187 |
|
| 188 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
grid-template-columns: 1fr;
|
| 190 |
}
|
| 191 |
}
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Reachy iOS Bridge - Matching App Branding */
|
| 2 |
+
|
| 3 |
* {
|
| 4 |
margin: 0;
|
| 5 |
padding: 0;
|
|
|
|
| 7 |
}
|
| 8 |
|
| 9 |
:root {
|
| 10 |
+
/* Light mode (default) - warm cream palette */
|
| 11 |
+
--bg-primary: #FBF5EF;
|
| 12 |
+
--bg-secondary: #F5EDE5;
|
| 13 |
+
--surface: #FFFFFF;
|
| 14 |
+
--text-primary: #2D3748;
|
| 15 |
+
--text-secondary: #5A6578;
|
| 16 |
+
--accent: #E8A954;
|
| 17 |
+
--accent-dark: #D4863A;
|
| 18 |
+
--accent-light: #F5C882;
|
| 19 |
+
--button-primary: #7889A8;
|
| 20 |
+
--button-primary-dark: #5F6F8C;
|
| 21 |
+
--success: #6B9B7A;
|
| 22 |
+
--warning: #E89B54;
|
| 23 |
+
--error: #C97066;
|
| 24 |
+
--divider: #E8E4DF;
|
| 25 |
+
--border: #E0DCD6;
|
| 26 |
+
--shadow: rgba(45, 55, 72, 0.08);
|
| 27 |
+
--code-bg: #F0EBE5;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
@media (prefers-color-scheme: dark) {
|
| 31 |
+
:root {
|
| 32 |
+
--bg-primary: #1A1815;
|
| 33 |
+
--bg-secondary: #141210;
|
| 34 |
+
--surface: #252220;
|
| 35 |
+
--text-primary: #F5F0E8;
|
| 36 |
+
--text-secondary: #A8A098;
|
| 37 |
+
--divider: #3A3632;
|
| 38 |
+
--border: #3A3632;
|
| 39 |
+
--shadow: rgba(0, 0, 0, 0.3);
|
| 40 |
+
--code-bg: #2D2A26;
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
html {
|
| 45 |
+
scroll-behavior: smooth;
|
| 46 |
}
|
| 47 |
|
| 48 |
body {
|
| 49 |
+
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 50 |
background: var(--bg-primary);
|
| 51 |
color: var(--text-primary);
|
| 52 |
line-height: 1.6;
|
| 53 |
min-height: 100vh;
|
| 54 |
}
|
| 55 |
|
| 56 |
+
.page-wrapper {
|
| 57 |
+
width: 100%;
|
| 58 |
+
overflow-x: hidden;
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
+
/* Navigation */
|
| 62 |
+
.nav {
|
| 63 |
+
position: sticky;
|
| 64 |
+
top: 0;
|
| 65 |
+
z-index: 100;
|
| 66 |
+
display: flex;
|
| 67 |
+
justify-content: space-between;
|
| 68 |
+
align-items: center;
|
| 69 |
+
padding: 1rem 2rem;
|
| 70 |
+
background: var(--bg-primary);
|
| 71 |
+
border-bottom: 1px solid var(--divider);
|
| 72 |
+
backdrop-filter: blur(10px);
|
| 73 |
}
|
| 74 |
|
| 75 |
+
.nav-brand {
|
|
|
|
|
|
|
| 76 |
display: flex;
|
| 77 |
+
align-items: center;
|
| 78 |
+
gap: 0.75rem;
|
| 79 |
}
|
| 80 |
|
| 81 |
+
.nav-logo {
|
| 82 |
+
font-size: 1.5rem;
|
| 83 |
}
|
| 84 |
|
| 85 |
+
.nav-title {
|
| 86 |
+
font-family: 'DM Serif Display', Georgia, serif;
|
| 87 |
+
font-size: 1.25rem;
|
| 88 |
+
font-weight: 400;
|
| 89 |
+
color: var(--text-primary);
|
| 90 |
}
|
| 91 |
|
| 92 |
+
.nav-links {
|
| 93 |
+
display: flex;
|
| 94 |
+
gap: 2rem;
|
| 95 |
}
|
| 96 |
|
| 97 |
+
.nav-links a {
|
| 98 |
+
color: var(--text-secondary);
|
| 99 |
+
text-decoration: none;
|
| 100 |
+
font-weight: 500;
|
| 101 |
+
font-size: 0.95rem;
|
| 102 |
+
transition: color 0.2s;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.nav-links a:hover {
|
| 106 |
+
color: var(--accent);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
/* Hero Section */
|
| 110 |
+
.hero {
|
| 111 |
+
display: grid;
|
| 112 |
+
grid-template-columns: 1fr 1fr;
|
| 113 |
+
gap: 4rem;
|
| 114 |
+
align-items: center;
|
| 115 |
+
padding: 5rem 4rem;
|
| 116 |
+
max-width: 1200px;
|
| 117 |
+
margin: 0 auto;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.hero-badge {
|
| 121 |
+
display: inline-block;
|
| 122 |
+
padding: 0.5rem 1rem;
|
| 123 |
+
background: var(--accent);
|
| 124 |
+
color: white;
|
| 125 |
+
font-size: 0.8rem;
|
| 126 |
font-weight: 600;
|
| 127 |
+
border-radius: 2rem;
|
| 128 |
+
margin-bottom: 1.5rem;
|
| 129 |
+
text-transform: uppercase;
|
| 130 |
+
letter-spacing: 0.05em;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.hero h1 {
|
| 134 |
+
font-family: 'DM Serif Display', Georgia, serif;
|
| 135 |
+
font-size: 3.5rem;
|
| 136 |
+
font-weight: 400;
|
| 137 |
+
line-height: 1.1;
|
| 138 |
+
margin-bottom: 1rem;
|
| 139 |
+
background: linear-gradient(135deg, var(--text-primary), var(--accent));
|
| 140 |
-webkit-background-clip: text;
|
| 141 |
-webkit-text-fill-color: transparent;
|
| 142 |
background-clip: text;
|
| 143 |
}
|
| 144 |
|
| 145 |
+
.hero-tagline {
|
| 146 |
+
font-size: 1.5rem;
|
| 147 |
color: var(--text-secondary);
|
| 148 |
+
margin-bottom: 1.5rem;
|
| 149 |
+
font-weight: 500;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.hero-description {
|
| 153 |
+
color: var(--text-secondary);
|
| 154 |
+
font-size: 1.1rem;
|
| 155 |
+
margin-bottom: 2rem;
|
| 156 |
+
max-width: 480px;
|
| 157 |
}
|
| 158 |
|
| 159 |
+
.hero-actions {
|
| 160 |
display: flex;
|
| 161 |
+
gap: 1rem;
|
|
|
|
| 162 |
}
|
| 163 |
|
| 164 |
+
/* Buttons */
|
| 165 |
+
.btn {
|
| 166 |
+
display: inline-block;
|
| 167 |
+
padding: 1rem 2rem;
|
| 168 |
+
border-radius: 2rem;
|
| 169 |
+
font-family: 'Nunito', sans-serif;
|
| 170 |
+
font-weight: 600;
|
| 171 |
+
font-size: 1rem;
|
| 172 |
+
text-decoration: none;
|
| 173 |
+
transition: all 0.2s;
|
| 174 |
+
cursor: pointer;
|
| 175 |
+
border: none;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.btn-primary {
|
| 179 |
+
background: var(--accent);
|
| 180 |
+
color: white;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.btn-primary:hover {
|
| 184 |
+
background: var(--accent-dark);
|
| 185 |
+
transform: translateY(-2px);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.btn-secondary {
|
| 189 |
+
background: var(--surface);
|
| 190 |
+
color: var(--text-primary);
|
| 191 |
+
border: 1px solid var(--border);
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.btn-secondary:hover {
|
| 195 |
+
background: var(--divider);
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
/* Hero Visual */
|
| 199 |
+
.hero-visual {
|
| 200 |
+
display: flex;
|
| 201 |
+
justify-content: center;
|
| 202 |
+
align-items: center;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.device-stack {
|
| 206 |
+
display: flex;
|
| 207 |
+
flex-direction: column;
|
| 208 |
+
align-items: center;
|
| 209 |
gap: 1rem;
|
| 210 |
}
|
| 211 |
|
| 212 |
+
.device {
|
| 213 |
+
display: flex;
|
| 214 |
+
flex-direction: column;
|
| 215 |
+
align-items: center;
|
| 216 |
+
gap: 0.5rem;
|
| 217 |
+
padding: 2rem 3rem;
|
| 218 |
+
background: var(--surface);
|
| 219 |
+
border-radius: 1.5rem;
|
| 220 |
+
box-shadow: 0 8px 32px var(--shadow);
|
| 221 |
border: 1px solid var(--border);
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.device-icon {
|
| 225 |
+
font-size: 3rem;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.device-label {
|
| 229 |
+
font-weight: 600;
|
| 230 |
+
color: var(--text-secondary);
|
| 231 |
+
font-size: 0.9rem;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.connection-line {
|
| 235 |
+
display: flex;
|
| 236 |
+
flex-direction: column;
|
| 237 |
+
align-items: center;
|
| 238 |
+
gap: 0.5rem;
|
| 239 |
+
padding: 1rem 0;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.signal {
|
| 243 |
+
width: 8px;
|
| 244 |
+
height: 8px;
|
| 245 |
+
background: var(--accent);
|
| 246 |
+
border-radius: 50%;
|
| 247 |
+
animation: pulse 1.5s ease-in-out infinite;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.signal:nth-child(2) { animation-delay: 0.3s; }
|
| 251 |
+
.signal:nth-child(3) { animation-delay: 0.6s; }
|
| 252 |
+
|
| 253 |
+
@keyframes pulse {
|
| 254 |
+
0%, 100% { opacity: 0.3; transform: scale(0.8); }
|
| 255 |
+
50% { opacity: 1; transform: scale(1); }
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
/* Sections */
|
| 259 |
+
.section {
|
| 260 |
+
padding: 5rem 2rem;
|
| 261 |
+
max-width: 1200px;
|
| 262 |
+
margin: 0 auto;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.section-alt {
|
| 266 |
+
background: var(--bg-secondary);
|
| 267 |
+
max-width: none;
|
| 268 |
+
padding-left: 2rem;
|
| 269 |
+
padding-right: 2rem;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.section-alt > * {
|
| 273 |
+
max-width: 1200px;
|
| 274 |
+
margin-left: auto;
|
| 275 |
+
margin-right: auto;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.section-header {
|
| 279 |
text-align: center;
|
| 280 |
+
margin-bottom: 3rem;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.section-header h2 {
|
| 284 |
+
font-family: 'DM Serif Display', Georgia, serif;
|
| 285 |
+
font-size: 2.5rem;
|
| 286 |
+
font-weight: 400;
|
| 287 |
+
margin-bottom: 0.75rem;
|
| 288 |
+
color: var(--text-primary);
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.section-header p {
|
| 292 |
+
color: var(--text-secondary);
|
| 293 |
+
font-size: 1.1rem;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
/* Features Grid */
|
| 297 |
+
.features-grid {
|
| 298 |
+
display: grid;
|
| 299 |
+
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
| 300 |
+
gap: 1.5rem;
|
| 301 |
}
|
| 302 |
|
| 303 |
+
.feature-card {
|
| 304 |
+
background: var(--surface);
|
| 305 |
+
padding: 2rem;
|
| 306 |
+
border-radius: 1rem;
|
| 307 |
+
border: 1px solid var(--border);
|
| 308 |
+
transition: all 0.3s;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.feature-card:hover {
|
| 312 |
transform: translateY(-4px);
|
| 313 |
+
box-shadow: 0 12px 32px var(--shadow);
|
| 314 |
border-color: var(--accent);
|
| 315 |
}
|
| 316 |
|
| 317 |
.feature-icon {
|
| 318 |
+
font-size: 2.5rem;
|
| 319 |
+
margin-bottom: 1rem;
|
| 320 |
display: block;
|
|
|
|
| 321 |
}
|
| 322 |
|
| 323 |
+
.feature-card h3 {
|
| 324 |
+
font-size: 1.25rem;
|
| 325 |
+
font-weight: 700;
|
| 326 |
margin-bottom: 0.5rem;
|
| 327 |
color: var(--text-primary);
|
| 328 |
}
|
| 329 |
|
| 330 |
+
.feature-card p {
|
|
|
|
| 331 |
color: var(--text-secondary);
|
| 332 |
+
font-size: 0.95rem;
|
| 333 |
}
|
| 334 |
|
| 335 |
+
/* Architecture */
|
| 336 |
+
.architecture {
|
| 337 |
+
display: flex;
|
| 338 |
+
flex-direction: column;
|
| 339 |
+
gap: 0;
|
| 340 |
+
max-width: 600px;
|
| 341 |
+
margin: 0 auto;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.arch-layer {
|
| 345 |
+
display: flex;
|
| 346 |
+
gap: 1.5rem;
|
| 347 |
+
padding: 2rem;
|
| 348 |
+
background: var(--surface);
|
| 349 |
+
border-radius: 1rem;
|
| 350 |
border: 1px solid var(--border);
|
| 351 |
+
align-items: flex-start;
|
|
|
|
| 352 |
}
|
| 353 |
|
| 354 |
+
.arch-icon {
|
| 355 |
+
font-size: 2rem;
|
| 356 |
+
flex-shrink: 0;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.arch-content h3 {
|
| 360 |
+
font-size: 1.2rem;
|
| 361 |
+
font-weight: 700;
|
| 362 |
+
margin-bottom: 0.5rem;
|
| 363 |
}
|
| 364 |
|
| 365 |
+
.arch-content p {
|
| 366 |
color: var(--text-secondary);
|
| 367 |
+
font-size: 0.95rem;
|
| 368 |
+
margin-bottom: 0.75rem;
|
| 369 |
}
|
| 370 |
|
| 371 |
+
.arch-tags {
|
|
|
|
| 372 |
display: flex;
|
| 373 |
+
flex-wrap: wrap;
|
| 374 |
gap: 0.5rem;
|
| 375 |
}
|
| 376 |
|
| 377 |
+
.tag {
|
| 378 |
+
padding: 0.25rem 0.75rem;
|
| 379 |
+
background: var(--code-bg);
|
| 380 |
+
color: var(--text-secondary);
|
| 381 |
+
font-size: 0.8rem;
|
| 382 |
+
border-radius: 1rem;
|
| 383 |
+
font-weight: 500;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.arch-connector {
|
| 387 |
+
display: flex;
|
| 388 |
+
flex-direction: column;
|
| 389 |
+
align-items: center;
|
| 390 |
+
padding: 0.5rem 0;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
.connector-line {
|
| 394 |
+
width: 2px;
|
| 395 |
+
height: 24px;
|
| 396 |
+
background: linear-gradient(to bottom, var(--accent), var(--accent-light));
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.connector-label {
|
| 400 |
+
font-size: 0.75rem;
|
| 401 |
+
color: var(--accent);
|
| 402 |
+
font-weight: 600;
|
| 403 |
+
margin-top: 0.25rem;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
/* Steps */
|
| 407 |
+
.steps {
|
| 408 |
+
display: flex;
|
| 409 |
+
flex-direction: column;
|
| 410 |
+
gap: 2rem;
|
| 411 |
+
max-width: 700px;
|
| 412 |
+
margin: 0 auto;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.step {
|
| 416 |
+
display: flex;
|
| 417 |
+
gap: 1.5rem;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
.step-number {
|
| 421 |
+
width: 48px;
|
| 422 |
+
height: 48px;
|
| 423 |
+
background: var(--accent);
|
| 424 |
+
color: white;
|
| 425 |
+
border-radius: 50%;
|
| 426 |
+
display: flex;
|
| 427 |
+
align-items: center;
|
| 428 |
+
justify-content: center;
|
| 429 |
+
font-size: 1.25rem;
|
| 430 |
+
font-weight: 700;
|
| 431 |
+
flex-shrink: 0;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
.step-content h3 {
|
| 435 |
+
font-size: 1.25rem;
|
| 436 |
+
font-weight: 700;
|
| 437 |
+
margin-bottom: 0.5rem;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.step-content p {
|
| 441 |
color: var(--text-secondary);
|
| 442 |
+
margin-bottom: 0.75rem;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
.step-content ul {
|
| 446 |
+
list-style: none;
|
| 447 |
+
padding: 0;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
.step-content li {
|
| 451 |
+
color: var(--text-secondary);
|
| 452 |
+
padding: 0.25rem 0;
|
| 453 |
+
padding-left: 1.25rem;
|
| 454 |
position: relative;
|
| 455 |
}
|
| 456 |
|
| 457 |
+
.step-content li::before {
|
| 458 |
content: "→";
|
| 459 |
position: absolute;
|
| 460 |
left: 0;
|
| 461 |
+
color: var(--accent);
|
| 462 |
}
|
| 463 |
|
| 464 |
+
.step-note {
|
| 465 |
+
font-size: 0.9rem;
|
| 466 |
+
color: var(--text-secondary);
|
| 467 |
+
font-style: italic;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.code-block {
|
| 471 |
+
background: var(--code-bg);
|
| 472 |
+
padding: 0.75rem 1rem;
|
| 473 |
+
border-radius: 0.5rem;
|
| 474 |
+
margin: 0.5rem 0;
|
| 475 |
+
overflow-x: auto;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.code-block code {
|
| 479 |
+
font-family: 'SF Mono', Consolas, monospace;
|
| 480 |
+
font-size: 0.9rem;
|
| 481 |
+
color: var(--text-primary);
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
code {
|
| 485 |
+
font-family: 'SF Mono', Consolas, monospace;
|
| 486 |
+
background: var(--code-bg);
|
| 487 |
+
padding: 0.15rem 0.4rem;
|
| 488 |
+
border-radius: 0.25rem;
|
| 489 |
+
font-size: 0.9em;
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
/* API Grid */
|
| 493 |
+
.api-grid {
|
| 494 |
+
display: grid;
|
| 495 |
+
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
| 496 |
+
gap: 1.5rem;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.api-card {
|
| 500 |
+
background: var(--surface);
|
| 501 |
+
padding: 1.5rem;
|
| 502 |
+
border-radius: 1rem;
|
| 503 |
+
border: 1px solid var(--border);
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.api-header {
|
| 507 |
+
display: flex;
|
| 508 |
+
align-items: center;
|
| 509 |
+
gap: 0.75rem;
|
| 510 |
+
margin-bottom: 0.75rem;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
.api-method {
|
| 514 |
+
padding: 0.25rem 0.5rem;
|
| 515 |
+
border-radius: 0.25rem;
|
| 516 |
+
font-size: 0.7rem;
|
| 517 |
+
font-weight: 700;
|
| 518 |
+
text-transform: uppercase;
|
| 519 |
+
letter-spacing: 0.05em;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
.api-method.get {
|
| 523 |
+
background: var(--success);
|
| 524 |
+
color: white;
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
.api-method.post {
|
| 528 |
+
background: var(--button-primary);
|
| 529 |
+
color: white;
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
.api-header code {
|
| 533 |
+
font-size: 1rem;
|
| 534 |
+
font-weight: 600;
|
| 535 |
+
background: transparent;
|
| 536 |
+
padding: 0;
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
.api-card > p {
|
| 540 |
+
color: var(--text-secondary);
|
| 541 |
+
font-size: 0.95rem;
|
| 542 |
+
margin-bottom: 1rem;
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
.api-response {
|
| 546 |
+
background: var(--code-bg);
|
| 547 |
+
border-radius: 0.5rem;
|
| 548 |
+
overflow: hidden;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
.api-response pre {
|
| 552 |
+
padding: 1rem;
|
| 553 |
+
font-family: 'SF Mono', Consolas, monospace;
|
| 554 |
+
font-size: 0.85rem;
|
| 555 |
+
color: var(--text-primary);
|
| 556 |
+
margin: 0;
|
| 557 |
+
overflow-x: auto;
|
| 558 |
}
|
| 559 |
|
| 560 |
+
.api-note {
|
| 561 |
+
margin-top: 0.75rem;
|
| 562 |
+
font-size: 0.85rem;
|
| 563 |
color: var(--text-secondary);
|
| 564 |
}
|
| 565 |
|
| 566 |
+
.api-more {
|
| 567 |
+
text-align: center;
|
| 568 |
+
margin-top: 2rem;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
.api-more a {
|
| 572 |
color: var(--accent);
|
| 573 |
text-decoration: none;
|
| 574 |
+
font-weight: 600;
|
| 575 |
}
|
| 576 |
|
| 577 |
+
.api-more a:hover {
|
| 578 |
text-decoration: underline;
|
| 579 |
}
|
| 580 |
|
| 581 |
+
/* Voices Grid */
|
| 582 |
+
.voices-grid {
|
| 583 |
+
display: grid;
|
| 584 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 585 |
+
gap: 1rem;
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
.voice-card {
|
| 589 |
+
display: flex;
|
| 590 |
+
align-items: center;
|
| 591 |
+
gap: 1rem;
|
| 592 |
+
padding: 1rem 1.25rem;
|
| 593 |
+
background: var(--surface);
|
| 594 |
+
border-radius: 1rem;
|
| 595 |
+
border: 1px solid var(--border);
|
| 596 |
+
transition: all 0.2s;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
.voice-card:hover {
|
| 600 |
+
border-color: var(--accent);
|
| 601 |
+
transform: translateY(-2px);
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
.voice-avatar {
|
| 605 |
+
width: 48px;
|
| 606 |
+
height: 48px;
|
| 607 |
+
border-radius: 50%;
|
| 608 |
+
display: flex;
|
| 609 |
+
align-items: center;
|
| 610 |
+
justify-content: center;
|
| 611 |
+
color: white;
|
| 612 |
+
font-weight: 700;
|
| 613 |
+
font-size: 1.25rem;
|
| 614 |
+
flex-shrink: 0;
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
.voice-info h4 {
|
| 618 |
+
font-weight: 700;
|
| 619 |
+
font-size: 1rem;
|
| 620 |
+
margin-bottom: 0.125rem;
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
.voice-info p {
|
| 624 |
+
color: var(--text-secondary);
|
| 625 |
+
font-size: 0.85rem;
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
/* Help Grid */
|
| 629 |
+
.help-grid {
|
| 630 |
+
display: grid;
|
| 631 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 632 |
+
gap: 1.5rem;
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
.help-card {
|
| 636 |
+
background: var(--surface);
|
| 637 |
+
padding: 1.5rem;
|
| 638 |
+
border-radius: 1rem;
|
| 639 |
+
border: 1px solid var(--border);
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
.help-card h3 {
|
| 643 |
+
font-size: 1.1rem;
|
| 644 |
+
margin-bottom: 1rem;
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
.help-card ol {
|
| 648 |
+
padding-left: 1.25rem;
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
.help-card li {
|
| 652 |
+
color: var(--text-secondary);
|
| 653 |
+
padding: 0.35rem 0;
|
| 654 |
+
font-size: 0.95rem;
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
.help-card code {
|
| 658 |
+
font-size: 0.8rem;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
/* Requirements Grid */
|
| 662 |
+
.requirements-grid {
|
| 663 |
+
display: grid;
|
| 664 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 665 |
+
gap: 2rem;
|
| 666 |
+
max-width: 700px;
|
| 667 |
+
margin: 0 auto;
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
.req-card {
|
| 671 |
+
background: var(--surface);
|
| 672 |
+
padding: 2rem;
|
| 673 |
+
border-radius: 1rem;
|
| 674 |
+
border: 1px solid var(--border);
|
| 675 |
+
text-align: center;
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
.req-icon {
|
| 679 |
+
font-size: 3rem;
|
| 680 |
+
margin-bottom: 1rem;
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
.req-card h3 {
|
| 684 |
+
font-size: 1.25rem;
|
| 685 |
+
font-weight: 700;
|
| 686 |
+
margin-bottom: 1rem;
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
.req-card ul {
|
| 690 |
+
list-style: none;
|
| 691 |
+
padding: 0;
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
.req-card li {
|
| 695 |
+
color: var(--text-secondary);
|
| 696 |
+
padding: 0.35rem 0;
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
/* Footer */
|
| 700 |
+
.footer {
|
| 701 |
+
border-top: 1px solid var(--divider);
|
| 702 |
+
padding: 3rem 2rem;
|
| 703 |
+
margin-top: 2rem;
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
.footer-content {
|
| 707 |
+
max-width: 1200px;
|
| 708 |
+
margin: 0 auto;
|
| 709 |
+
display: flex;
|
| 710 |
+
flex-direction: column;
|
| 711 |
+
align-items: center;
|
| 712 |
+
gap: 1.5rem;
|
| 713 |
+
text-align: center;
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
.footer-brand {
|
| 717 |
+
display: flex;
|
| 718 |
+
align-items: center;
|
| 719 |
+
gap: 0.75rem;
|
| 720 |
+
font-family: 'DM Serif Display', Georgia, serif;
|
| 721 |
+
font-size: 1.25rem;
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
.footer-logo {
|
| 725 |
+
font-size: 1.5rem;
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
.footer-links {
|
| 729 |
+
display: flex;
|
| 730 |
+
gap: 2rem;
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
.footer-links a {
|
| 734 |
+
color: var(--text-secondary);
|
| 735 |
+
text-decoration: none;
|
| 736 |
+
font-weight: 500;
|
| 737 |
+
transition: color 0.2s;
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
.footer-links a:hover {
|
| 741 |
+
color: var(--accent);
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
.footer-copyright {
|
| 745 |
+
color: var(--text-secondary);
|
| 746 |
+
font-size: 0.9rem;
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
/* Responsive */
|
| 750 |
+
@media (max-width: 900px) {
|
| 751 |
+
.hero {
|
| 752 |
+
grid-template-columns: 1fr;
|
| 753 |
+
text-align: center;
|
| 754 |
+
padding: 3rem 1.5rem;
|
| 755 |
+
gap: 3rem;
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
.hero-description {
|
| 759 |
+
max-width: none;
|
| 760 |
}
|
| 761 |
|
| 762 |
+
.hero-actions {
|
| 763 |
+
justify-content: center;
|
| 764 |
}
|
| 765 |
|
| 766 |
+
.hero h1 {
|
| 767 |
font-size: 2.5rem;
|
| 768 |
}
|
| 769 |
|
| 770 |
+
.nav-links {
|
| 771 |
+
display: none;
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
.section {
|
| 775 |
+
padding: 3rem 1.5rem;
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
.section-header h2 {
|
| 779 |
+
font-size: 2rem;
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
+
.features-grid,
|
| 783 |
+
.api-grid,
|
| 784 |
+
.help-grid {
|
| 785 |
grid-template-columns: 1fr;
|
| 786 |
}
|
| 787 |
}
|
| 788 |
|
| 789 |
+
@media (max-width: 480px) {
|
| 790 |
+
.hero h1 {
|
| 791 |
+
font-size: 2rem;
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
.hero-tagline {
|
| 795 |
+
font-size: 1.2rem;
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
.device {
|
| 799 |
+
padding: 1.5rem 2rem;
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
.device-icon {
|
| 803 |
+
font-size: 2rem;
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
.hero-actions {
|
| 807 |
+
flex-direction: column;
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
.btn {
|
| 811 |
+
width: 100%;
|
| 812 |
+
text-align: center;
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
.voices-grid {
|
| 816 |
+
grid-template-columns: 1fr;
|
| 817 |
+
}
|
| 818 |
+
}
|