openvoiceui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/.env.example +104 -0
  2. package/Dockerfile +30 -0
  3. package/LICENSE +21 -0
  4. package/README.md +638 -0
  5. package/SETUP.md +360 -0
  6. package/app.py +232 -0
  7. package/auto-approve-devices.js +111 -0
  8. package/cli/index.js +372 -0
  9. package/config/__init__.py +4 -0
  10. package/config/default.yaml +43 -0
  11. package/config/flags.yaml +67 -0
  12. package/config/loader.py +203 -0
  13. package/config/providers.yaml +71 -0
  14. package/config/speech_normalization.yaml +182 -0
  15. package/config/theme.json +4 -0
  16. package/data/greetings.json +25 -0
  17. package/default-pages/ai-image-creator.html +915 -0
  18. package/default-pages/bulk-image-uploader.html +492 -0
  19. package/default-pages/desktop.html +2865 -0
  20. package/default-pages/file-explorer.html +854 -0
  21. package/default-pages/interactive-map.html +655 -0
  22. package/default-pages/style-guide.html +1005 -0
  23. package/default-pages/website-setup.html +1623 -0
  24. package/deploy/openclaw/Dockerfile +46 -0
  25. package/deploy/openvoiceui.service +30 -0
  26. package/deploy/setup-nginx.sh +50 -0
  27. package/deploy/setup-sudo.sh +306 -0
  28. package/deploy/skill-runner/Dockerfile +19 -0
  29. package/deploy/skill-runner/requirements.txt +14 -0
  30. package/deploy/skill-runner/server.py +269 -0
  31. package/deploy/supertonic/Dockerfile +22 -0
  32. package/deploy/supertonic/server.py +79 -0
  33. package/docker-compose.pinokio.yml +11 -0
  34. package/docker-compose.yml +59 -0
  35. package/greetings.json +25 -0
  36. package/index.html +65 -0
  37. package/inject-device-identity.js +142 -0
  38. package/package.json +82 -0
  39. package/profiles/default.json +114 -0
  40. package/profiles/manager.py +354 -0
  41. package/profiles/schema.json +337 -0
  42. package/prompts/voice-system-prompt.md +149 -0
  43. package/providers/__init__.py +39 -0
  44. package/providers/base.py +63 -0
  45. package/providers/llm/__init__.py +12 -0
  46. package/providers/llm/base.py +71 -0
  47. package/providers/llm/clawdbot_provider.py +112 -0
  48. package/providers/llm/zai_provider.py +115 -0
  49. package/providers/registry.py +320 -0
  50. package/providers/stt/__init__.py +12 -0
  51. package/providers/stt/base.py +58 -0
  52. package/providers/stt/webspeech_provider.py +49 -0
  53. package/providers/stt/whisper_provider.py +100 -0
  54. package/providers/tts/__init__.py +20 -0
  55. package/providers/tts/base.py +91 -0
  56. package/providers/tts/groq_provider.py +74 -0
  57. package/providers/tts/supertonic_provider.py +72 -0
  58. package/requirements.txt +38 -0
  59. package/routes/__init__.py +10 -0
  60. package/routes/admin.py +515 -0
  61. package/routes/canvas.py +1315 -0
  62. package/routes/chat.py +51 -0
  63. package/routes/conversation.py +2158 -0
  64. package/routes/elevenlabs_hybrid.py +306 -0
  65. package/routes/greetings.py +98 -0
  66. package/routes/icons.py +279 -0
  67. package/routes/image_gen.py +364 -0
  68. package/routes/instructions.py +190 -0
  69. package/routes/music.py +838 -0
  70. package/routes/onboarding.py +43 -0
  71. package/routes/pi.py +62 -0
  72. package/routes/profiles.py +215 -0
  73. package/routes/report_issue.py +68 -0
  74. package/routes/static_files.py +533 -0
  75. package/routes/suno.py +664 -0
  76. package/routes/theme.py +81 -0
  77. package/routes/transcripts.py +199 -0
  78. package/routes/vision.py +348 -0
  79. package/routes/workspace.py +288 -0
  80. package/server.py +1510 -0
  81. package/services/__init__.py +1 -0
  82. package/services/auth.py +143 -0
  83. package/services/canvas_versioning.py +239 -0
  84. package/services/db_pool.py +107 -0
  85. package/services/gateway.py +16 -0
  86. package/services/gateway_manager.py +333 -0
  87. package/services/gateways/__init__.py +12 -0
  88. package/services/gateways/base.py +110 -0
  89. package/services/gateways/compat.py +264 -0
  90. package/services/gateways/openclaw.py +1134 -0
  91. package/services/health.py +100 -0
  92. package/services/memory_client.py +455 -0
  93. package/services/paths.py +26 -0
  94. package/services/speech_normalizer.py +285 -0
  95. package/services/tts.py +270 -0
  96. package/setup-config.js +262 -0
  97. package/sounds/air_horn.mp3 +0 -0
  98. package/sounds/bruh.mp3 +0 -0
  99. package/sounds/crowd_cheer.mp3 +0 -0
  100. package/sounds/gunshot.mp3 +0 -0
  101. package/sounds/impact.mp3 +0 -0
  102. package/sounds/lets_go.mp3 +0 -0
  103. package/sounds/record_stop.mp3 +0 -0
  104. package/sounds/rewind.mp3 +0 -0
  105. package/sounds/sad_trombone.mp3 +0 -0
  106. package/sounds/scratch_long.mp3 +0 -0
  107. package/sounds/yeah.mp3 +0 -0
  108. package/src/adapters/ClawdBotAdapter.js +264 -0
  109. package/src/adapters/_template.js +133 -0
  110. package/src/adapters/elevenlabs-classic.js +841 -0
  111. package/src/adapters/elevenlabs-hybrid.js +812 -0
  112. package/src/adapters/hume-evi.js +676 -0
  113. package/src/admin.html +1339 -0
  114. package/src/app.js +8802 -0
  115. package/src/core/Config.js +173 -0
  116. package/src/core/EmotionEngine.js +307 -0
  117. package/src/core/EventBridge.js +180 -0
  118. package/src/core/EventBus.js +117 -0
  119. package/src/core/VoiceSession.js +607 -0
  120. package/src/face/BaseFace.js +259 -0
  121. package/src/face/EyeFace.js +208 -0
  122. package/src/face/HaloSmokeFace.js +509 -0
  123. package/src/face/manifest.json +27 -0
  124. package/src/face/previews/eyes.svg +16 -0
  125. package/src/face/previews/orb.svg +29 -0
  126. package/src/features/MusicPlayer.js +620 -0
  127. package/src/features/Soundboard.js +128 -0
  128. package/src/providers/DeepgramSTT.js +472 -0
  129. package/src/providers/DeepgramStreamingSTT.js +766 -0
  130. package/src/providers/GroqSTT.js +559 -0
  131. package/src/providers/TTSPlayer.js +323 -0
  132. package/src/providers/WebSpeechSTT.js +479 -0
  133. package/src/providers/tts/BaseTTSProvider.js +81 -0
  134. package/src/providers/tts/HumeProvider.js +77 -0
  135. package/src/providers/tts/SupertonicProvider.js +174 -0
  136. package/src/providers/tts/index.js +140 -0
  137. package/src/shell/adapter-registry.js +154 -0
  138. package/src/shell/caller-bridge.js +35 -0
  139. package/src/shell/camera-bridge.js +28 -0
  140. package/src/shell/canvas-bridge.js +32 -0
  141. package/src/shell/commercial-bridge.js +44 -0
  142. package/src/shell/face-bridge.js +44 -0
  143. package/src/shell/music-bridge.js +60 -0
  144. package/src/shell/orchestrator.js +233 -0
  145. package/src/shell/profile-discovery.js +303 -0
  146. package/src/shell/sounds-bridge.js +28 -0
  147. package/src/shell/transcript-bridge.js +61 -0
  148. package/src/shell/waveform-bridge.js +33 -0
  149. package/src/styles/base.css +2862 -0
  150. package/src/styles/face.css +417 -0
  151. package/src/styles/pi-overrides.css +89 -0
  152. package/src/styles/theme-dark.css +67 -0
  153. package/src/test-tts.html +175 -0
  154. package/src/ui/AppShell.js +544 -0
  155. package/src/ui/ProfileSwitcher.js +228 -0
  156. package/src/ui/SessionControl.js +240 -0
  157. package/src/ui/face/FacePicker.js +195 -0
  158. package/src/ui/face/FaceRenderer.js +309 -0
  159. package/src/ui/settings/PlaylistEditor.js +366 -0
  160. package/src/ui/settings/SettingsPanel.css +684 -0
  161. package/src/ui/settings/SettingsPanel.js +419 -0
  162. package/src/ui/settings/TTSVoicePreview.js +210 -0
  163. package/src/ui/themes/ThemeManager.js +213 -0
  164. package/src/ui/visualizers/BaseVisualizer.js +29 -0
  165. package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
  166. package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
  167. package/static/emulators/jsdos/js-dos.css +1 -0
  168. package/static/emulators/jsdos/js-dos.js +22 -0
  169. package/static/favicon.svg +55 -0
  170. package/static/icons/apple-touch-icon.png +0 -0
  171. package/static/icons/favicon-32.png +0 -0
  172. package/static/icons/icon-192.png +0 -0
  173. package/static/icons/icon-512.png +0 -0
  174. package/static/install.html +449 -0
  175. package/static/manifest.json +26 -0
  176. package/static/sw.js +21 -0
  177. package/tts_providers/__init__.py +136 -0
  178. package/tts_providers/base_provider.py +319 -0
  179. package/tts_providers/groq_provider.py +155 -0
  180. package/tts_providers/hume_provider.py +226 -0
  181. package/tts_providers/providers_config.json +119 -0
  182. package/tts_providers/qwen3_provider.py +371 -0
  183. package/tts_providers/resemble_provider.py +315 -0
  184. package/tts_providers/supertonic_provider.py +557 -0
  185. package/tts_providers/supertonic_tts.py +399 -0
@@ -0,0 +1,55 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
2
+ <defs>
3
+ <radialGradient id="eyeL" cx="45%" cy="38%" r="55%">
4
+ <stop offset="0%" stop-color="#ffffff"/>
5
+ <stop offset="65%" stop-color="#e0e0e0"/>
6
+ <stop offset="100%" stop-color="#c0c0c0"/>
7
+ </radialGradient>
8
+ <radialGradient id="eyeR" cx="45%" cy="38%" r="55%">
9
+ <stop offset="0%" stop-color="#ffffff"/>
10
+ <stop offset="65%" stop-color="#e0e0e0"/>
11
+ <stop offset="100%" stop-color="#c0c0c0"/>
12
+ </radialGradient>
13
+ <radialGradient id="pupilL" cx="35%" cy="32%" r="55%">
14
+ <stop offset="0%" stop-color="#2a2a2a"/>
15
+ <stop offset="100%" stop-color="#000000"/>
16
+ </radialGradient>
17
+ <radialGradient id="pupilR" cx="35%" cy="32%" r="55%">
18
+ <stop offset="0%" stop-color="#2a2a2a"/>
19
+ <stop offset="100%" stop-color="#000000"/>
20
+ </radialGradient>
21
+ <filter id="blueGlow" x="-30%" y="-30%" width="160%" height="160%">
22
+ <feGaussianBlur stdDeviation="1.8" result="blur"/>
23
+ <feFlood flood-color="#0088ff" flood-opacity="0.5" result="color"/>
24
+ <feComposite in="color" in2="blur" operator="in" result="glow"/>
25
+ <feMerge><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge>
26
+ </filter>
27
+ </defs>
28
+
29
+ <!-- App icon background -->
30
+ <rect width="32" height="32" rx="7" fill="#050508"/>
31
+
32
+ <!-- Left eye glow -->
33
+ <ellipse cx="10" cy="17" rx="7" ry="8.5" fill="rgba(0,136,255,0.12)" filter="url(#blueGlow)"/>
34
+ <!-- Left eye white -->
35
+ <ellipse cx="10" cy="17" rx="5.5" ry="7" fill="url(#eyeL)"/>
36
+ <!-- Left eyelid cap (top arc) -->
37
+ <path d="M 4.5 13 Q 10 10.5 15.5 13" stroke="#050508" stroke-width="2.5" fill="none" stroke-linecap="round"/>
38
+ <!-- Left pupil -->
39
+ <circle cx="10" cy="18" r="2.8" fill="url(#pupilL)"/>
40
+ <!-- Left pupil highlight -->
41
+ <circle cx="8.8" cy="16.5" r="1.1" fill="rgba(255,255,255,0.85)"/>
42
+ <circle cx="11.5" cy="19.2" r="0.6" fill="rgba(255,255,255,0.35)"/>
43
+
44
+ <!-- Right eye glow -->
45
+ <ellipse cx="22" cy="17" rx="7" ry="8.5" fill="rgba(0,136,255,0.12)" filter="url(#blueGlow)"/>
46
+ <!-- Right eye white -->
47
+ <ellipse cx="22" cy="17" rx="5.5" ry="7" fill="url(#eyeR)"/>
48
+ <!-- Right eyelid cap (top arc) -->
49
+ <path d="M 16.5 13 Q 22 10.5 27.5 13" stroke="#050508" stroke-width="2.5" fill="none" stroke-linecap="round"/>
50
+ <!-- Right pupil -->
51
+ <circle cx="22" cy="18" r="2.8" fill="url(#pupilR)"/>
52
+ <!-- Right pupil highlight -->
53
+ <circle cx="20.8" cy="16.5" r="1.1" fill="rgba(255,255,255,0.85)"/>
54
+ <circle cx="23.5" cy="19.2" r="0.6" fill="rgba(255,255,255,0.35)"/>
55
+ </svg>
Binary file
Binary file
Binary file
@@ -0,0 +1,449 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6
+ <title>Install OpenVoiceUI</title>
7
+ <meta name="theme-color" content="#060610">
8
+ <style>
9
+ * { box-sizing: border-box; margin: 0; padding: 0; }
10
+
11
+ body {
12
+ background: #060610;
13
+ color: #e0e8ff;
14
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
15
+ min-height: 100dvh;
16
+ display: flex;
17
+ flex-direction: column;
18
+ align-items: center;
19
+ justify-content: center;
20
+ padding: 32px 24px 48px;
21
+ text-align: center;
22
+ }
23
+
24
+ /* ── App Identity ── */
25
+ .icon {
26
+ width: 120px;
27
+ height: 120px;
28
+ border-radius: 28px;
29
+ overflow: hidden;
30
+ margin-bottom: 20px;
31
+ box-shadow: 0 0 40px rgba(0, 180, 255, 0.35);
32
+ }
33
+ .icon img { width: 100%; height: 100%; display: block; }
34
+
35
+ h1 {
36
+ font-size: 2rem;
37
+ font-weight: 700;
38
+ letter-spacing: -0.5px;
39
+ color: #fff;
40
+ margin-bottom: 8px;
41
+ }
42
+
43
+ .tagline {
44
+ font-size: 1rem;
45
+ color: #7090b0;
46
+ margin-bottom: 40px;
47
+ line-height: 1.5;
48
+ }
49
+
50
+ /* ── Cards ── */
51
+ .card {
52
+ background: rgba(255,255,255,0.04);
53
+ border: 1px solid rgba(255,255,255,0.08);
54
+ border-radius: 20px;
55
+ padding: 28px 24px;
56
+ width: 100%;
57
+ max-width: 400px;
58
+ margin-bottom: 16px;
59
+ }
60
+
61
+ /* ── Big install button (Android) ── */
62
+ .btn-install {
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ gap: 10px;
67
+ width: 100%;
68
+ padding: 18px;
69
+ background: linear-gradient(135deg, #0088ff, #00c8ff);
70
+ color: #fff;
71
+ font-size: 1.1rem;
72
+ font-weight: 600;
73
+ border: none;
74
+ border-radius: 14px;
75
+ cursor: pointer;
76
+ text-decoration: none;
77
+ transition: opacity 0.15s, transform 0.1s;
78
+ -webkit-tap-highlight-color: transparent;
79
+ }
80
+ .btn-install:active { opacity: 0.85; transform: scale(0.98); }
81
+
82
+ .btn-secondary {
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ gap: 8px;
87
+ width: 100%;
88
+ padding: 16px;
89
+ background: rgba(255,255,255,0.06);
90
+ color: #c0d8f0;
91
+ font-size: 1rem;
92
+ font-weight: 500;
93
+ border: 1px solid rgba(255,255,255,0.12);
94
+ border-radius: 14px;
95
+ cursor: pointer;
96
+ text-decoration: none;
97
+ transition: background 0.15s;
98
+ -webkit-tap-highlight-color: transparent;
99
+ }
100
+ .btn-secondary:active { background: rgba(255,255,255,0.1); }
101
+
102
+ /* ── iOS Steps ── */
103
+ .steps { display: flex; flex-direction: column; gap: 20px; text-align: left; }
104
+
105
+ .step {
106
+ display: flex;
107
+ align-items: flex-start;
108
+ gap: 16px;
109
+ }
110
+
111
+ .step-num {
112
+ flex-shrink: 0;
113
+ width: 32px;
114
+ height: 32px;
115
+ background: linear-gradient(135deg, #0066cc, #0099ff);
116
+ border-radius: 50%;
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ font-size: 0.9rem;
121
+ font-weight: 700;
122
+ color: #fff;
123
+ }
124
+
125
+ .step-body { flex: 1; padding-top: 4px; }
126
+
127
+ .step-body strong {
128
+ display: block;
129
+ font-size: 0.95rem;
130
+ font-weight: 600;
131
+ color: #fff;
132
+ margin-bottom: 3px;
133
+ }
134
+
135
+ .step-body span {
136
+ font-size: 0.85rem;
137
+ color: #7090b0;
138
+ line-height: 1.4;
139
+ }
140
+
141
+ .step-icon {
142
+ display: inline-flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ background: rgba(255,255,255,0.1);
146
+ border-radius: 8px;
147
+ padding: 4px 8px;
148
+ margin-top: 6px;
149
+ }
150
+
151
+ /* ── URL copy row ── */
152
+ .url-row {
153
+ display: flex;
154
+ gap: 8px;
155
+ margin-top: 16px;
156
+ }
157
+
158
+ .url-display {
159
+ flex: 1;
160
+ background: rgba(255,255,255,0.06);
161
+ border: 1px solid rgba(255,255,255,0.12);
162
+ border-radius: 10px;
163
+ padding: 10px 12px;
164
+ font-size: 0.8rem;
165
+ color: #7090b0;
166
+ word-break: break-all;
167
+ text-align: left;
168
+ }
169
+
170
+ .btn-copy {
171
+ flex-shrink: 0;
172
+ padding: 10px 16px;
173
+ background: rgba(0, 136, 255, 0.2);
174
+ border: 1px solid rgba(0, 136, 255, 0.4);
175
+ border-radius: 10px;
176
+ color: #00aaff;
177
+ font-size: 0.85rem;
178
+ font-weight: 600;
179
+ cursor: pointer;
180
+ -webkit-tap-highlight-color: transparent;
181
+ }
182
+ .btn-copy.copied { background: rgba(0,200,100,0.2); border-color: rgba(0,200,100,0.4); color: #00cc66; }
183
+
184
+ .section-title {
185
+ font-size: 0.75rem;
186
+ font-weight: 600;
187
+ letter-spacing: 0.08em;
188
+ text-transform: uppercase;
189
+ color: #405070;
190
+ margin-bottom: 18px;
191
+ }
192
+
193
+ /* ── Already installed ── */
194
+ .installed-badge {
195
+ display: inline-flex;
196
+ align-items: center;
197
+ gap: 6px;
198
+ background: rgba(0,200,100,0.12);
199
+ border: 1px solid rgba(0,200,100,0.25);
200
+ border-radius: 20px;
201
+ padding: 6px 14px;
202
+ font-size: 0.8rem;
203
+ color: #00cc66;
204
+ margin-bottom: 24px;
205
+ }
206
+
207
+ /* ── Hidden by default, JS shows correct section ── */
208
+ .section { display: none; }
209
+ .section.visible { display: block; }
210
+
211
+ .divider {
212
+ width: 40px;
213
+ height: 2px;
214
+ background: rgba(255,255,255,0.08);
215
+ border-radius: 2px;
216
+ margin: 32px auto;
217
+ }
218
+
219
+ footer {
220
+ margin-top: 40px;
221
+ font-size: 0.75rem;
222
+ color: #304050;
223
+ }
224
+ </style>
225
+ </head>
226
+ <body>
227
+
228
+ <div class="icon">
229
+ <img src="/static/icons/icon-192.png" alt="OpenVoiceUI icon">
230
+ </div>
231
+
232
+ <h1>OpenVoiceUI</h1>
233
+ <p class="tagline">AI voice agent — always ready,<br>no browser required.</p>
234
+
235
+ <!-- ── Already installed ── -->
236
+ <div id="section-installed" class="section">
237
+ <div class="installed-badge">
238
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="7" stroke="#00cc66" stroke-width="1.5"/><path d="M5 8l2 2 4-4" stroke="#00cc66" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
239
+ Already installed
240
+ </div>
241
+ <a id="open-app-btn" href="/" class="btn-install" style="max-width:400px;text-decoration:none;">
242
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12l9-9 9 9"/><path d="M9 21V9h6v12"/></svg>
243
+ Open OpenVoiceUI
244
+ </a>
245
+ </div>
246
+
247
+ <!-- ── Android: one-tap install ── -->
248
+ <div id="section-android" class="section">
249
+ <div class="card">
250
+ <p class="section-title">Install the app</p>
251
+ <button id="android-install-btn" class="btn-install">
252
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M12 16V4"/><path d="M8 8l4-4 4 4"/><rect x="3" y="16" width="18" height="4" rx="2"/></svg>
253
+ Add to Home Screen
254
+ </button>
255
+ </div>
256
+ </div>
257
+
258
+ <!-- ── iOS Safari: step guide ── -->
259
+ <div id="section-ios-safari" class="section">
260
+ <div class="card">
261
+ <p class="section-title">Install in 3 taps</p>
262
+ <div class="steps">
263
+
264
+ <div class="step">
265
+ <div class="step-num">1</div>
266
+ <div class="step-body">
267
+ <strong>Tap the Share button</strong>
268
+ <span>It's the icon at the bottom of Safari</span>
269
+ <div class="step-icon">
270
+ <!-- Safari share icon -->
271
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#0099ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
272
+ <path d="M8 6H6a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V8a2 2 0 00-2-2h-2"/>
273
+ <path d="M12 2v10M8 6l4-4 4 4"/>
274
+ </svg>
275
+ </div>
276
+ </div>
277
+ </div>
278
+
279
+ <div class="step">
280
+ <div class="step-num">2</div>
281
+ <div class="step-body">
282
+ <strong>Tap "Add to Home Screen"</strong>
283
+ <span>Scroll down in the share sheet to find it</span>
284
+ <div class="step-icon">
285
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#0099ff" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M12 8v8M8 12h8"/></svg>
286
+ <span style="color:#7090b0;font-size:0.75rem;margin-left:6px;">Add to Home Screen</span>
287
+ </div>
288
+ </div>
289
+ </div>
290
+
291
+ <div class="step">
292
+ <div class="step-num">3</div>
293
+ <div class="step-body">
294
+ <strong>Tap "Add"</strong>
295
+ <span>OpenVoiceUI will appear on your home screen like any app</span>
296
+ </div>
297
+ </div>
298
+
299
+ </div>
300
+ </div>
301
+ </div>
302
+
303
+ <!-- ── iOS Chrome / other browser ── -->
304
+ <div id="section-ios-other" class="section">
305
+ <div class="card">
306
+ <p class="section-title">Open in Safari to install</p>
307
+ <p style="font-size:0.9rem;color:#7090b0;margin-bottom:20px;line-height:1.6;">
308
+ iPhone only lets you install apps through Safari.<br>
309
+ Copy the link below, open Safari, and paste it.
310
+ </p>
311
+ <div class="url-row">
312
+ <div class="url-display" id="current-url"></div>
313
+ <button class="btn-copy" id="copy-btn" onclick="copyUrl()">Copy</button>
314
+ </div>
315
+ <div style="margin-top:12px;">
316
+ <a id="safari-link" href="#" class="btn-secondary" style="margin-top:8px;">
317
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 3v1M12 20v1M3 12h1M20 12h1"/><path d="M15 9l-3 3-3-3" opacity="0.4"/><path d="M12 12l3 3" opacity="0.4"/></svg>
318
+ Open in Safari
319
+ </a>
320
+ </div>
321
+ </div>
322
+
323
+ <div class="divider"></div>
324
+
325
+ <div class="card">
326
+ <p class="section-title">Then follow these steps in Safari</p>
327
+ <div class="steps">
328
+ <div class="step">
329
+ <div class="step-num">1</div>
330
+ <div class="step-body">
331
+ <strong>Tap the Share button</strong>
332
+ <span>Bottom of Safari — the box with an arrow</span>
333
+ </div>
334
+ </div>
335
+ <div class="step">
336
+ <div class="step-num">2</div>
337
+ <div class="step-body">
338
+ <strong>Tap "Add to Home Screen"</strong>
339
+ <span>Scroll down in the share sheet</span>
340
+ </div>
341
+ </div>
342
+ <div class="step">
343
+ <div class="step-num">3</div>
344
+ <div class="step-body">
345
+ <strong>Tap "Add"</strong>
346
+ <span>Done — OpenVoiceUI is now installed</span>
347
+ </div>
348
+ </div>
349
+ </div>
350
+ </div>
351
+ </div>
352
+
353
+ <!-- ── Desktop ── -->
354
+ <div id="section-desktop" class="section">
355
+ <div class="card">
356
+ <p class="section-title">On your phone</p>
357
+ <p style="font-size:0.9rem;color:#7090b0;line-height:1.6;margin-bottom:20px;">
358
+ Open this link on your phone to install OpenVoiceUI as an app.
359
+ </p>
360
+ <div class="url-row">
361
+ <div class="url-display" id="desktop-url"></div>
362
+ <button class="btn-copy" id="desktop-copy-btn" onclick="copyUrlDesktop()">Copy</button>
363
+ </div>
364
+ </div>
365
+ <div style="margin-top:16px;">
366
+ <a href="/" class="btn-secondary" style="max-width:400px;">Open in browser anyway</a>
367
+ </div>
368
+ </div>
369
+
370
+ <footer>OpenVoiceUI Voice Agent</footer>
371
+
372
+ <script>
373
+ // ── Detection ──────────────────────────────────────────────────────────────
374
+ const ua = navigator.userAgent;
375
+ const isIOS = /iPad|iPhone|iPod/.test(ua) && !window.MSStream;
376
+ const isAndroid = /Android/.test(ua);
377
+ const isMobile = isIOS || isAndroid;
378
+ const isSafari = isIOS && /Safari/.test(ua) && !/CriOS|FxiOS|EdgiOS|OPiOS/.test(ua);
379
+ const isIOSOther = isIOS && !isSafari;
380
+ const isStandalone = window.matchMedia('(display-mode: standalone)').matches
381
+ || window.navigator.standalone === true;
382
+
383
+ const installUrl = window.location.origin + '/install';
384
+
385
+ // ── Show correct section ───────────────────────────────────────────────────
386
+ function show(id) {
387
+ document.getElementById(id).classList.add('visible');
388
+ }
389
+
390
+ if (isStandalone) {
391
+ show('section-installed');
392
+ } else if (isAndroid) {
393
+ show('section-android');
394
+ } else if (isSafari) {
395
+ show('section-ios-safari');
396
+ } else if (isIOSOther) {
397
+ document.getElementById('current-url').textContent = installUrl;
398
+ document.getElementById('safari-link').href = 'x-web-search://?q=' + encodeURIComponent(installUrl);
399
+ show('section-ios-other');
400
+ } else {
401
+ // Desktop
402
+ document.getElementById('desktop-url').textContent = installUrl;
403
+ show('section-desktop');
404
+ }
405
+
406
+ // ── Android install prompt ─────────────────────────────────────────────────
407
+ let _deferredPrompt = null;
408
+
409
+ window.addEventListener('beforeinstallprompt', (e) => {
410
+ e.preventDefault();
411
+ _deferredPrompt = e;
412
+ });
413
+
414
+ document.getElementById('android-install-btn')?.addEventListener('click', async () => {
415
+ if (_deferredPrompt) {
416
+ _deferredPrompt.prompt();
417
+ const { outcome } = await _deferredPrompt.userChoice;
418
+ _deferredPrompt = null;
419
+ if (outcome === 'accepted') {
420
+ document.getElementById('section-android').classList.remove('visible');
421
+ show('section-installed');
422
+ }
423
+ } else {
424
+ // Prompt not available yet — send them to the app directly
425
+ window.location.href = '/';
426
+ }
427
+ });
428
+
429
+ // ── Copy helpers ───────────────────────────────────────────────────────────
430
+ function copyUrl() {
431
+ navigator.clipboard.writeText(installUrl).then(() => {
432
+ const btn = document.getElementById('copy-btn');
433
+ btn.textContent = 'Copied!';
434
+ btn.classList.add('copied');
435
+ setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 2000);
436
+ });
437
+ }
438
+
439
+ function copyUrlDesktop() {
440
+ navigator.clipboard.writeText(installUrl).then(() => {
441
+ const btn = document.getElementById('desktop-copy-btn');
442
+ btn.textContent = 'Copied!';
443
+ btn.classList.add('copied');
444
+ setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 2000);
445
+ });
446
+ }
447
+ </script>
448
+ </body>
449
+ </html>
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "OpenVoiceUI",
3
+ "short_name": "OpenVoiceUI",
4
+ "description": "AI voice agent — open source voice UI platform",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "orientation": "portrait-primary",
8
+ "background_color": "#060610",
9
+ "theme_color": "#060610",
10
+ "icons": [
11
+ {
12
+ "src": "/static/icons/icon-192.png",
13
+ "sizes": "192x192",
14
+ "type": "image/png",
15
+ "purpose": "any maskable"
16
+ },
17
+ {
18
+ "src": "/static/icons/icon-512.png",
19
+ "sizes": "512x512",
20
+ "type": "image/png",
21
+ "purpose": "any maskable"
22
+ }
23
+ ],
24
+ "categories": ["utilities", "productivity"],
25
+ "permissions": ["microphone"]
26
+ }
package/static/sw.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Service Worker — minimal implementation required for PWA installability
3
+ * Handles install/activate lifecycle. No offline caching (the app needs live network).
4
+ */
5
+
6
+ const VERSION = 'v1';
7
+
8
+ self.addEventListener('install', (event) => {
9
+ console.log('[SW] Installing', VERSION);
10
+ self.skipWaiting();
11
+ });
12
+
13
+ self.addEventListener('activate', (event) => {
14
+ console.log('[SW] Activated', VERSION);
15
+ event.waitUntil(self.clients.claim());
16
+ });
17
+
18
+ // Pass all fetch requests through — no caching, app requires live network
19
+ self.addEventListener('fetch', (event) => {
20
+ event.respondWith(fetch(event.request));
21
+ });