TTS prototype

TTS with default browser API

frdel committed Nov 10, 2024 at 23:44 UTC 92d94b4d86ef477b0d19e7a7e7ef27f981826a31
8 files changed +272 -705
webui/index.css
+2
@@ -446,6 +446,8 @@ pre {
446 padding-bottom: 0;*/
447 padding: var(--spacing-sm) var(--spacing-md);
448 padding-bottom: 0;
449 + display: flex;
450 + justify-content: space-between;
451 }
452
453 #progress-bar-h {
webui/index.html
+171 -137
@@ -9,7 +9,7 @@
9 <link rel="stylesheet" href="toast.css">
10 <link rel="stylesheet" href="settings.css">
11 <link rel="stylesheet" href="speech.css">
12 -
12 +
13 <script>
14 window.safeCall = function (name, ...args) {
15 if (window[name]) window[name](...args)
@@ -21,7 +21,8 @@
21
22 <!-- KaTeX javascript -->
23 <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js" crossorigin="anonymous"></script>
24 - <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js" crossorigin="anonymous"></script>
24 + <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js"
25 + crossorigin="anonymous"></script>
26
27 <script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
28 <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
@@ -39,23 +40,24 @@
40 <button id="toggle-sidebar" class="toggle-sidebar-button" aria-label="Toggle Sidebar" aria-expanded="false">
41 <span aria-hidden="true">
42 <!-- Hamburger Icon -->
42 - <svg id="sidebar-hamburger-svg" xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="CurrentColor">
43 - <path d="M3 13h18v-2H3v2zm0 4h18v-2H3v2zm0-8h18V7H3v2z"></path>
43 + <svg id="sidebar-hamburger-svg" xmlns="http://www.w3.org/2000/svg" width="22" height="22"
44 + viewBox="0 0 24 24" fill="CurrentColor">
45 + <path d="M3 13h18v-2H3v2zm0 4h18v-2H3v2zm0-8h18V7H3v2z"></path>
46 </svg>
47 </span>
48 </button>
47 -
48 - <div id="logo-container">
49 +
50 + <div id="logo-container">
51 <a href="https://github.com/frdel/agent-zero" target="_blank" rel="noopener noreferrer">
52 <img src="splash.jpg" alt="a0" width="22" height="22">
53 </a>
54 + </div>
55 </div>
53 - </div>
54 -
55 - <div id="left-panel" class="panel">
56 - <!--Sidebar upper elements-->
57 - <div class="left-panel-top">
58 - <div class="config-section" id="status-section" x-data="{ connected: true }">
56 +
57 + <div id="left-panel" class="panel">
58 + <!--Sidebar upper elements-->
59 + <div class="left-panel-top">
60 + <div class="config-section" id="status-section" x-data="{ connected: true }">
61 <h3>Status</h3>
62 <h4 class="connected" x-show="connected">&#10004; Connected</h4>
63 <h4 class="disconnected" x-show="!connected">&#10008; Disconnected</h4>
@@ -72,85 +74,95 @@
74 </div>
75 <!-- Chats List -->
76 <div class="config-section" id="chats-section" x-data="{ contexts: [], selected: '' }"
75 - x-show="contexts.length > 0">
76 - <h3>Chats</h3>
77 - <div class="chats-list-container">
78 - <ul class="config-list">
79 - <template x-for="context in contexts">
80 - <li>
81 - <span :class="{'chat-list-button': true, 'font-bold': context.id === selected}"
82 - @click="selected = context.id; selectChat(context.id)">
83 - Chat #<span x-text="context.no"></span>
84 - </span>
85 - <button class="edit-button" @click="killChat(context.id)">X</button>
86 - </li>
87 - </template>
88 - </ul>
77 + x-show="contexts.length > 0">
78 + <h3>Chats</h3>
79 + <div class="chats-list-container">
80 + <ul class="config-list">
81 + <template x-for="context in contexts">
82 + <li>
83 + <span :class="{'chat-list-button': true, 'font-bold': context.id === selected}"
84 + @click="selected = context.id; selectChat(context.id)">
85 + Chat #<span x-text="context.no"></span>
86 + </span>
87 + <button class="edit-button" @click="killChat(context.id)">X</button>
88 + </li>
89 + </template>
90 + </ul>
91 + </div>
92 </div>
93 </div>
91 - </div>
92 - <!--Sidebar lower elements-->
93 - <div class="left-panel-bottom">
94 - <!-- Preferences -->
95 - <div class="pref-section" x-data="{ prefOpen: true }">
96 - <span><h3 class="pref-header" @click="prefOpen = !prefOpen">
97 - Preferences
98 - <svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
99 - fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"
100 - :class="{'rotated': !prefOpen}">
101 - <path d="M8 4l8 8-8 8"/>
102 - </svg>
103 - </h3>
104 - <ul class="config-list" x-show="prefOpen" x-collapse x-transition>
105 - <!-- Preferences Items -->
106 - <li x-data="{ autoScroll: true }">
107 - <span>Autoscroll</span>
108 - <label class="switch">
109 - <input id="auto-scroll-switch" type="checkbox" x-model="autoScroll"
110 - x-effect="window.safeCall('toggleAutoScroll',autoScroll)">
111 - <span class="slider"></span>
112 - </label>
113 - </li>
114 - <li x-data="{ darkMode: localStorage.getItem('darkMode') != 'false' }"
115 - x-init="$watch('darkMode', val => toggleDarkMode(val))">
116 - <span class="switch-label">Dark mode</span>
117 - <label class="switch">
118 - <input type="checkbox" x-model="darkMode">
119 - <span class="slider"></span>
120 - </label>
121 - </li>
122 - <li x-data="{ showThoughts: true }">
123 - <span>Show thoughts</span>
124 - <label class="switch">
125 - <input type="checkbox" x-model="showThoughts"
126 - x-effect="window.safeCall('toggleThoughts',showThoughts)">
127 - <span class="slider"></span>
128 - </label>
129 - </li>
130 - <li x-data="{ showJson: false }">
131 - <span>Show JSON</span>
132 - <label class="switch">
133 - <input type="checkbox" x-model="showJson" x-effect="window.safeCall('toggleJson',showJson)">
134 - <span class="slider"></span>
135 - </label>
136 - </li>
137 - <li x-data="{ showUtils: false }">
138 - <span>Show utility messages</span>
139 - <label class="switch">
140 - <input type="checkbox" x-model="showUtils"
141 - x-effect="window.safeCall('toggleUtils',showUtils)">
142 - <span class="slider"></span>
143 - </label>
144 - </li>
145 - </ul>
146 - </span>
147 - </div>
148 - <!-- Version Info -->
149 - <div class="version-info">
150 - <span id="a0version">Agent Zero 0.7.2<br>built on 2024-11-04</span>
94 + <!--Sidebar lower elements-->
95 + <div class="left-panel-bottom">
96 + <!-- Preferences -->
97 + <div class="pref-section" x-data="{ prefOpen: true }">
98 + <span>
99 + <h3 class="pref-header" @click="prefOpen = !prefOpen">
100 + Preferences
101 + <svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
102 + stroke="currentColor" stroke-width="2" width="16" height="16"
103 + :class="{'rotated': !prefOpen}">
104 + <path d="M8 4l8 8-8 8" />
105 + </svg>
106 + </h3>
107 + <ul class="config-list" x-show="prefOpen" x-collapse x-transition>
108 + <!-- Preferences Items -->
109 + <li x-data="{ autoScroll: true }">
110 + <span>Autoscroll</span>
111 + <label class="switch">
112 + <input id="auto-scroll-switch" type="checkbox" x-model="autoScroll"
113 + x-effect="window.safeCall('toggleAutoScroll',autoScroll)">
114 + <span class="slider"></span>
115 + </label>
116 + </li>
117 + <li x-data="{ darkMode: localStorage.getItem('darkMode') != 'false' }"
118 + x-init="$watch('darkMode', val => toggleDarkMode(val))">
119 + <span class="switch-label">Dark mode</span>
120 + <label class="switch">
121 + <input type="checkbox" x-model="darkMode">
122 + <span class="slider"></span>
123 + </label>
124 + </li>
125 + <li x-data="{ speech: localStorage.getItem('speech') == 'true' }"
126 + x-init="$watch('speech', val => toggleSpeech(val))">
127 + <span class="switch-label">Speech</span>
128 + <label class="switch">
129 + <input type="checkbox" x-model="speech">
130 + <span class="slider"></span>
131 + </label>
132 + </li>
133 + <li x-data="{ showThoughts: true }">
134 + <span>Show thoughts</span>
135 + <label class="switch">
136 + <input type="checkbox" x-model="showThoughts"
137 + x-effect="window.safeCall('toggleThoughts',showThoughts)">
138 + <span class="slider"></span>
139 + </label>
140 + </li>
141 + <li x-data="{ showJson: false }">
142 + <span>Show JSON</span>
143 + <label class="switch">
144 + <input type="checkbox" x-model="showJson"
145 + x-effect="window.safeCall('toggleJson',showJson)">
146 + <span class="slider"></span>
147 + </label>
148 + </li>
149 + <li x-data="{ showUtils: false }">
150 + <span>Show utility messages</span>
151 + <label class="switch">
152 + <input type="checkbox" x-model="showUtils"
153 + x-effect="window.safeCall('toggleUtils',showUtils)">
154 + <span class="slider"></span>
155 + </label>
156 + </li>
157 + </ul>
158 + </span>
159 + </div>
160 + <!-- Version Info -->
161 + <div class="version-info">
162 + <span id="a0version">Agent Zero 0.7.2<br>built on 2024-11-04</span>
163 + </div>
164 </div>
165 </div>
153 - </div>
166 <div id="right-panel" class="panel">
167 <!--Chat-->
168 <div id="time-date"></div>
@@ -161,8 +173,16 @@
173 <button class="toast__copy">Copy</button>
174 <button class="toast__close">Close</button>
175 </div>
164 - <div id="progress-bar-box">
165 - <h4 id="progress-bar-h"><span id="progress-bar-i">|></span><span id="progress-bar"></span></h4>
176 + <div id="progress-bar-box" x-data="{ isSpeaking: false }" x-init="
177 + // Watch the speech synthesis status to update isSpeaking
178 + setInterval(() => isSpeaking = window.speech.isSpeaking(), 100)
179 + ">
180 + <h4 id="progress-bar-h">
181 + <span id="progress-bar-i">|></span><span id="progress-bar"></span>
182 + </h4>
183 + <h4 id="progress-bar-stop-speech" x-show="isSpeaking">
184 + <span id="stop-speech" @click="window.speech.stop()" style="cursor: pointer">Stop Speech</span>
185 + </h4>
186 </div>
187 <div id="input-section" x-data="{
188 paused: false,
@@ -197,8 +217,8 @@
217 <template x-for="(image, index) in attachments" :key="index">
218 <div class="preview-item">
219 <img :src="image.url" alt="Preview">
200 - <button @click="attachments.splice(index, 1); hasAttachments = attachments.length > 0"
201 - class="remove-image">&times;</button>
220 + <button @click="attachments.splice(index, 1); hasAttachments = attachments.length > 0"
221 + class="remove-image">&times;</button>
222 </div>
223 </template>
224 </div>
@@ -207,65 +227,80 @@
227 <div class="input-row">
228 <!-- Attachment icon with tooltip -->
229 <div class="attachment-wrapper" x-data="{ showTooltip: false }">
210 - <label for="file-input" class="attachment-icon"
211 - @mouseover="showTooltip = true"
212 - @mouseleave="showTooltip = false">
213 - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
214 - <path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z"/>
230 + <label for="file-input" class="attachment-icon" @mouseover="showTooltip = true"
231 + @mouseleave="showTooltip = false">
232 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
233 + fill="currentColor">
234 + <path
235 + d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" />
236 </svg>
237 </label>
217 - <input type="file" id="file-input"
218 - accept="png, jpg, jpeg, txt, pdf, csv, html, json, md"
219 - multiple
220 - style="display: none"
221 - @change="handleFileUpload($event)">
238 + <input type="file" id="file-input" accept="png, jpg, jpeg, txt, pdf, csv, html, json, md"
239 + multiple style="display: none" @change="handleFileUpload($event)">
240
241 <div x-show="showTooltip" class="tooltip">
242 Limit: 4 attachments per message
243 </div>
244 </div>
227 -
245 +
246 <!-- Text input -->
247 <textarea id="chat-input" placeholder="Type your message here..." rows="1"></textarea>
230 -
248 +
249 <div id="chat-buttons-wrapper">
232 - <!-- Send button -->
233 - <button class="chat-button" id="send-button" aria-label="Send message">
234 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
235 - <path d="M25 20 L75 50 L25 80" fill="none" stroke="currentColor" stroke-width="15"></path>
236 - </svg>
237 - </button>
238 -
239 - <!-- Microphone button -->
240 - <button class="chat-button mic-inactive" id="microphone-button" aria-label="Start/Stop recording">
241 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 18" fill="currentColor" width="24" height="24">
242 - <path d="m8,12c1.66,0,3-1.34,3-3V3c0-1.66-1.34-3-3-3s-3,1.34-3,3v6c0,1.66,1.34,3,3,3Zm-1,1.9c-2.7-.4-4.8-2.6-5-5.4H0c.2,3.8,3.1,6.9,7,7.5v2h2v-2c3.9-.6,6.8-3.7,7-7.5h-2c-.2,2.8-2.3,5-5,5.4h-2Z"/>
243 - </svg>
250 + <!-- Send button -->
251 + <button class="chat-button" id="send-button" aria-label="Send message">
252 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
253 + <path d="M25 20 L75 50 L25 80" fill="none" stroke="currentColor" stroke-width="15">
254 + </path>
255 + </svg>
256 + </button>
257
245 - </button>
258 + <!-- Microphone button -->
259 + <button class="chat-button mic-inactive" id="microphone-button"
260 + aria-label="Start/Stop recording">
261 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 18" fill="currentColor" width="24"
262 + height="24">
263 + <path
264 + d="m8,12c1.66,0,3-1.34,3-3V3c0-1.66-1.34-3-3-3s-3,1.34-3,3v6c0,1.66,1.34,3,3,3Zm-1,1.9c-2.7-.4-4.8-2.6-5-5.4H0c.2,3.8,3.1,6.9,7,7.5v2h2v-2c3.9-.6,6.8-3.7,7-7.5h-2c-.2,2.8-2.3,5-5,5.4h-2Z" />
265 + </svg>
266 +
267 + </button>
268
269 </div>
270 </div>
249 -
271 +
272 <!-- Bottom row with text buttons -->
273 <div class="text-buttons-row">
252 - <button class="text-button" @click="loadKnowledge()"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"></path></svg>Import knowledge</button>
274 + <button class="text-button" @click="loadKnowledge()"><svg xmlns="http://www.w3.org/2000/svg"
275 + fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
276 + <path stroke-linecap="round" stroke-linejoin="round"
277 + d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5">
278 + </path>
279 + </svg>Import knowledge</button>
280 <button class="text-button" @click="workDirModalProxy.openModal()">
281 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 123.37 92.59">
255 - <path d="m5.72,11.5l-3.93,8.73h119.77s-3.96-8.73-3.96-8.73h-60.03c-1.59,0-2.88-1.29-2.88-2.88V1.75H13.72v6.87c0,1.59-1.29,2.88-2.88,2.88h-5.12Z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="5"></path>
256 - <path d="m6.38,20.23H1.75l7.03,67.03c.11,1.07.55,2.02,1.2,2.69.55.55,1.28.89,2.11.89h97.1c.82,0,1.51-.33,2.05-.87.68-.68,1.13-1.67,1.28-2.79l9.1-66.94H6.38Z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="5"></path>
257 - </svg>work_dir Browser</button>
258 -
282 + <path
283 + d="m5.72,11.5l-3.93,8.73h119.77s-3.96-8.73-3.96-8.73h-60.03c-1.59,0-2.88-1.29-2.88-2.88V1.75H13.72v6.87c0,1.59-1.29,2.88-2.88,2.88h-5.12Z"
284 + fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="5"></path>
285 + <path
286 + d="m6.38,20.23H1.75l7.03,67.03c.11,1.07.55,2.02,1.2,2.69.55.55,1.28.89,2.11.89h97.1c.82,0,1.51-.33,2.05-.87.68-.68,1.13-1.67,1.28-2.79l9.1-66.94H6.38Z"
287 + fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="5"></path>
288 + </svg>work_dir Browser</button>
289 +
290 <button class="text-button" @click="pauseAgent(!paused)">
260 - <!-- Dynamic path that switches between pause and play icons -->
261 - <template x-if="!paused">
262 - <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="14" height="14">
263 - <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path></svg>
264 - </template>
265 - <template x-if="paused">
266 - <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="14" height="14">
267 - <path d="M8 5v14l11-7z"></path></svg>
268 - </template>
291 + <!-- Dynamic path that switches between pause and play icons -->
292 + <template x-if="!paused">
293 + <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"
294 + stroke-width="1.5" stroke="currentColor" width="14" height="14">
295 + <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path>
296 + </svg>
297 + </template>
298 + <template x-if="paused">
299 + <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"
300 + stroke-width="1.5" stroke="currentColor" width="14" height="14">
301 + <path d="M8 5v14l11-7z"></path>
302 + </svg>
303 + </template>
304 </svg>
305 <span x-text="paused ? 'Resume Agent' : 'Pause Agent'"></span>
306 </button>
@@ -275,8 +310,7 @@
310 </div>
311 <div id="settingsModal" x-data="settingsModalProxy">
312 <template x-teleport="body">
278 - <div x-show="isOpen" class="modal-overlay" @click.self="handleCancel()"
279 - x-transition>
313 + <div x-show="isOpen" class="modal-overlay" @click.self="handleCancel()" x-transition>
314 <div class="modal-container">
315 <div class="modal-header">
316 <h2 x-text="settings.title"></h2>
@@ -369,10 +403,10 @@
403
404 <div class="modal-footer">
405 <div id="buttons-container">
372 - <template x-for="button in settings.buttons" :key="button.id">
373 - <button :class="button.classes" @click="handleButton(button.id)"
374 - x-text="button.title"></button>
375 - </template>
406 + <template x-for="button in settings.buttons" :key="button.id">
407 + <button :class="button.classes" @click="handleButton(button.id)"
408 + x-text="button.title"></button>
409 + </template>
410 </div>
411 </div>
412 </div>
webui/index.js
+47 -17
@@ -1,4 +1,5 @@
1 import * as msgs from "./messages.js"
2 +import { speech } from "./speech.js";
3
4 const leftPanel = document.getElementById('left-panel');
5 const rightPanel = document.getElementById('right-panel');
@@ -59,7 +60,7 @@ function setupSidebarToggle() {
60 // Make sure to call this function
61 document.addEventListener('DOMContentLoaded', setupSidebarToggle);
62
62 - // index.js
63 +// index.js
64 export async function sendMessage() {
65 try {
66 const message = chatInput.value.trim();
@@ -77,7 +78,7 @@ export async function sendMessage() {
78 ...attachment,
79 url: URL.createObjectURL(attachment.file)
80 }));
80 -
81 +
82 // Only render if there's text content or it's an image-only message
83 setMessage(messageId, 'user', '', message, false, {
84 attachments: attachmentsWithUrls
@@ -98,10 +99,10 @@ export async function sendMessage() {
99 });
100 } else {
101 // For text-only messages, let polling handle the rendering
101 - const data = {
102 - text: message,
102 + const data = {
103 + text: message,
104 context,
104 - message_id: messageId
105 + message_id: messageId
106 };
107 response = await fetch('/msg', {
108 method: 'POST',
@@ -149,16 +150,16 @@ sendButton.addEventListener('click', sendMessage);
150
151 export function updateChatInput(text) {
152 console.log('updateChatInput called with:', text);
152 -
153 +
154 // Append text with proper spacing
155 const currentValue = chatInput.value;
156 const needsSpace = currentValue.length > 0 && !currentValue.endsWith(' ');
157 chatInput.value = currentValue + (needsSpace ? ' ' : '') + text + ' ';
157 -
158 +
159 // Adjust height and trigger input event
160 adjustTextareaHeight();
161 chatInput.dispatchEvent(new Event('input'));
161 -
162 +
163 console.log('Updated chat input value:', chatInput.value);
164 }
165
@@ -235,7 +236,7 @@ async function handleFileUpload(event) {
236 }
237
238
238 -window.loadKnowledge = async function() {
239 +window.loadKnowledge = async function () {
240 const input = document.createElement('input');
241 input.type = 'file';
242 input.accept = '.txt,.pdf,.csv,.html,.json,.md';
@@ -289,17 +290,17 @@ const workDirModalProxy = {
290 };
291
292 // Make the proxy available globally
292 -window.workDirModalProxy = workDirModalProxy;
293 +window.workDirModalProxy = workDirModalProxy;
294
295 // Ensure correct setup for Alpine.js x-data.
295 -window.workDirModal = function() {
296 +window.workDirModal = function () {
297 return workDirModalProxy; // Returns the proxy object for the Work Dir modal
298 }
299
300
301 document.addEventListener('alpine:init', () => {
302 // Make workDirModalProxy available as an Alpine component/store
302 - Alpine.data('workDirModal', workDirModal);
303 + Alpine.data('workDirModal', workDirModal);
304 });
305
306
@@ -334,6 +335,7 @@ function generateGUID() {
335
336 let lastLogVersion = 0;
337 let lastLogGuid = ""
338 +let lastSpokenNo = 0
339
340 async function poll() {
341 let updated = false
@@ -356,6 +358,7 @@ async function poll() {
358 for (const log of response.logs) {
359 setMessage(log.no, log.type, log.heading, log.content, log.temp, log.kvps);
360 }
361 + afterMessagesUpdate(response.logs)
362 }
363
364 updateProgress(response.log_progress)
@@ -383,6 +386,26 @@ async function poll() {
386 return updated
387 }
388
389 +function afterMessagesUpdate(logs) {
390 + if(localStorage.getItem('speech') == 'true') {
391 + speakMessages(logs)
392 + }
393 +}
394 +
395 +function speakMessages(logs) {
396 + // log.no, log.type, log.heading, log.content
397 + for (let i = logs.length - 1; i >= 0; i--) {
398 + const log = logs[i]
399 + if (log.type == "response") {
400 + if (log.no > lastSpokenNo) {
401 + lastSpokenNo = log.no
402 + speech.speak(log.content)
403 + return
404 + }
405 + }
406 + }
407 +}
408 +
409 function updateProgress(progress) {
410 if (!progress) progress = "Waiting for input"
411
@@ -453,6 +476,7 @@ const setContext = function (id) {
476 context = id
477 lastLogGuid = ""
478 lastLogVersion = 0
479 + lastSpokenNo = 0
480 const chatsAD = Alpine.$data(chatsSection);
481 chatsAD.selected = id
482 }
@@ -488,6 +512,12 @@ window.toggleDarkMode = function (isDark) {
512 localStorage.setItem('darkMode', isDark);
513 };
514
515 +window.toggleSpeech = function (isOn) {
516 + console.log("Speech:", isOn);
517 + localStorage.setItem('speech', isOn);
518 + if(!isOn) speech.stop()
519 +};
520 +
521 // Modify this part
522 document.addEventListener('DOMContentLoaded', () => {
523 const isDarkMode = localStorage.getItem('darkMode') !== 'false';
@@ -576,20 +606,20 @@ window.saveChat = async function () {
606 function downloadFile(filename, content) {
607 // Create a Blob with the content to save
608 const blob = new Blob([content], { type: 'application/json' });
579 -
609 +
610 // Create a link element
611 const link = document.createElement('a');
582 -
612 +
613 // Create a URL for the Blob
614 const url = URL.createObjectURL(blob);
615 link.href = url;
586 -
616 +
617 // Set the file name for download
618 link.download = filename;
589 -
619 +
620 // Programmatically click the link to trigger the download
621 link.click();
592 -
622 +
623 // Clean up by revoking the object URL
624 setTimeout(() => {
625 URL.revokeObjectURL(url);
webui/messages.js
+9 -9
@@ -62,10 +62,10 @@ export function _drawMessage(messageContainer, heading, content, temp, followUp,
62 if (window.renderMathInElement) {
63 renderMathInElement(spanElement, {
64 delimiters: [
65 - {left: "$", right: "$", display: true},
66 - {left: "\\$", right: "\\$", display: true},
67 - {left: "$", right: "$", display: false},
68 - {left: "\\$", right: "\\$", display: false}
65 + { left: "$", right: "$", display: true },
66 + { left: "\\$", right: "\\$", display: true },
67 + { left: "$", right: "$", display: false },
68 + { left: "\\$", right: "\\$", display: false }
69 ],
70 throwOnError: false // Prevent KaTeX from throwing errors
71 });
@@ -115,7 +115,7 @@ export function drawMessageUser(messageContainer, id, type, heading, content, te
115 if (hasAttachments) {
116 const attachmentsContainer = document.createElement('div');
117 attachmentsContainer.classList.add('attachments-container');
118 -
118 +
119 // Add "Attachments" heading if there's text content
120 if (hasContent) {
121 const attachmentsHeading = document.createElement('h4');
@@ -204,10 +204,10 @@ function drawKvps(container, kvps) {
204 if (window.renderMathInElement) {
205 renderMathInElement(span, {
206 delimiters: [
207 - {left: "$$", right: "$$", display: true},
208 - {left: "\$$", right: "\$$", display: true},
209 - {left: "$", right: "$", display: false},
210 - {left: "\$$", right: "\$$", display: false}
207 + { left: "$$", right: "$$", display: true },
208 + { left: "\$$", right: "\$$", display: true },
209 + { left: "$", right: "$", display: false },
210 + { left: "\$$", right: "\$$", display: false }
211 ],
212 throwOnError: false // Prevent KaTeX from throwing errors
213 });
webui/speech copy.js deleted
-337
@@ -1,337 +0,0 @@
1 -import { pipeline, read_audio } from './transformers@3.0.2.js';
2 -import { updateChatInput, sendMessage } from './index.js';
3 -
4 -const microphoneButton = document.getElementById('microphone-button');
5 -let microphoneInput = null;
6 -let isProcessingClick = false;
7 -
8 -class MicrophoneInput {
9 - /**
10 - * Voice Input Handler with Whisper Transcription
11 - *
12 - * Whisper Model Size Configuration:
13 - * - 'tiny': Smallest model, fastest, lowest accuracy (~32MB)
14 - * - Best for: Quick prototyping, low-resource environments
15 - * - Pros: Very fast, low memory usage
16 - * - Cons: Lowest transcription accuracy
17 - *
18 - * - 'base': Small model, good balance of speed and accuracy (~74MB)
19 - * - Best for: General-purpose voice input
20 - * - Pros: Reasonable accuracy, moderate resource usage
21 - * - Cons: Less accurate than larger models
22 - *
23 - * - 'small': Medium-sized model, better accuracy (~244MB)
24 - * - Best for: More precise transcription needs
25 - * - Pros: Improved accuracy over base model
26 - * - Cons: Slower, more memory-intensive
27 - *
28 - * - 'medium': Large model with high accuracy (~769MB)
29 - * - Best for: Professional transcription, multi-language support
30 - * - Pros: Very high accuracy
31 - * - Cons: Significant computational resources required
32 - *
33 - * - 'large': Largest model, highest accuracy (~1.5GB)
34 - * - Best for: Professional, multi-language transcription
35 - * - Pros: Highest possible accuracy
36 - * - Cons: Slowest, most resource-intensive
37 - *
38 - * Recommended Default: 'base' for most web applications
39 - */
40 - constructor(updateCallback, options = {}) {
41 - this.mediaRecorder = null;
42 - this.audioChunks = [];
43 - this.isRecording = false;
44 - this.updateCallback = updateCallback;
45 - this.isFinalizing = false;
46 - this.messageSent = false; // move messageSent into class
47 -
48 - // New properties for silence detection
49 - this.audioContext = null;
50 - this.mediaStreamSource = null;
51 - this.analyserNode = null;
52 - this.silenceTimer = null;
53 - this.silenceThreshold = options.silenceThreshold || 0.01; // Adjust as needed
54 - this.silenceDuration = options.silenceDuration || 2000; // Duration in milliseconds
55 -
56 - this.options = {
57 - modelSize: 'tiny',
58 - language: 'en',
59 - chunkDuration: 3000,
60 - ...options
61 - };
62 - }
63 -
64 - async initialize() {
65 - try {
66 -
67 - this.transcriber = await pipeline(`automatic-speech-recognition`, `Xenova/whisper-${this.options.modelSize}.${this.options.language}`);
68 -
69 - const stream = await navigator.mediaDevices.getUserMedia({
70 - audio: {
71 - echoCancellation: true,
72 - noiseSuppression: true,
73 - channelCount: 1
74 - }
75 - });
76 -
77 - // Configure MediaRecorder
78 - this.mediaRecorder = new MediaRecorder(stream);
79 -
80 - // Handle audio data availability
81 - this.mediaRecorder.ondataavailable = async (event) => {
82 - if (event.data.size > 0) {
83 - this.audioChunks.push(event.data);
84 - // await this.processAudioChunk(event.data);
85 - }
86 - };
87 -
88 - // Handle recording stop
89 - this.mediaRecorder.onstop = async () => {
90 - await this.finalizeRecording();
91 - };
92 -
93 - // Set up AudioContext and AnalyserNode for silence detection
94 - this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
95 - this.mediaStreamSource = this.audioContext.createMediaStreamSource(stream);
96 - this.analyserNode = this.audioContext.createAnalyser();
97 - this.analyserNode.minDecibels = -90;
98 - this.analyserNode.maxDecibels = -10;
99 - this.analyserNode.smoothingTimeConstant = 0.85;
100 -
101 - this.mediaStreamSource.connect(this.analyserNode);
102 - } catch (error) {
103 - console.error('Microphone initialization error:', error);
104 - toast('Failed to access microphone. Please check permissions.', 'error');
105 - }
106 - }
107 -
108 - startSilenceDetection() {
109 - const dataArray = new Uint8Array(this.analyserNode.fftSize);
110 - const checkSilence = () => {
111 - this.analyserNode.getByteTimeDomainData(dataArray);
112 -
113 - let sum = 0;
114 - for (let i = 0; i < dataArray.length; i++) {
115 - const amplitude = (dataArray[i] - 128) / 128;
116 - sum += amplitude * amplitude;
117 - }
118 - const rms = Math.sqrt(sum / dataArray.length);
119 -
120 - if (rms < this.silenceThreshold) {
121 - if (!this.silenceTimer) {
122 - this.silenceTimer = setTimeout(() => {
123 - if (this.isRecording) {
124 - console.log('Silence detected. Stopping recording.');
125 - this.stopRecording();
126 - microphoneButton.classList.remove('recording');
127 - microphoneButton.classList.remove('mic-pulse');
128 - }
129 - }, this.silenceDuration);
130 - }
131 - } else {
132 - if (this.silenceTimer) {
133 - clearTimeout(this.silenceTimer);
134 - this.silenceTimer = null;
135 - }
136 - }
137 -
138 - if (this.isRecording) {
139 - requestAnimationFrame(checkSilence);
140 - }
141 - };
142 -
143 - if (this.isRecording) {
144 - requestAnimationFrame(checkSilence);
145 - }
146 - }
147 -
148 - startRecording() {
149 - if (this.mediaRecorder && this.audioContext) {
150 - this.isRecording = true;
151 - this.audioChunks = [];
152 - this.messageSent = false;
153 - this.mediaRecorder.start(this.options.chunkDuration);
154 - this.audioContext.resume();
155 - this.startSilenceDetection();
156 - }
157 - }
158 -
159 - stopRecording() {
160 - if (this.mediaRecorder && this.isRecording) {
161 - this.isRecording = false;
162 - if (!this.isFinalizing) {
163 - this.isFinalizing = true;
164 - this.mediaRecorder.stop();
165 - this.audioContext.suspend();
166 - if (this.silenceTimer) {
167 - clearTimeout(this.silenceTimer);
168 - this.silenceTimer = null;
169 - }
170 - }
171 - }
172 - }
173 -
174 -
175 - async finalizeRecording() {
176 - if (this.isFinalizing) {
177 - this.isFinalizing = false;
178 -
179 - if (this.audioChunks.length > 0) {
180 -
181 - const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
182 - const audioUrl = URL.createObjectURL(audioBlob);
183 - const samplingRate = 16000; // Adjust as needed for the model
184 - const audioData = await read_audio(audioUrl, samplingRate);
185 - URL.revokeObjectURL(audioUrl);
186 -
187 - // Transcribe the audio
188 - const result = await this.transcriber(audioData);
189 -
190 - if (result.text) {
191 - console.log('Final transcription received:', result.text);
192 - await this.updateCallback(result.text, true);
193 - } else {
194 - console.warn('Final transcription returned empty text.');
195 - }
196 -
197 -
198 - // Release the object URL after use
199 -
200 - // const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' });
201 - // this.audioChunks = []; // Clear for next recording
202 -
203 - // const reader = new FileReader();
204 - // reader.onloadend = async () => {
205 - // const base64Data = reader.result.split(',')[1];
206 -
207 - // try {
208 - // const response = await fetch('/transcribe', {
209 - // method: 'POST',
210 - // headers: {
211 - // 'Content-Type': 'application/json'
212 - // },
213 - // body: JSON.stringify({
214 - // audio_data: base64Data,
215 - // model_size: this.options.modelSize,
216 - // language: this.options.language,
217 - // is_final: true
218 - // })
219 - // });
220 -
221 - // const result = await response.json();
222 -
223 - // if (result.text) {
224 - // console.log('Final transcription received:', result.text);
225 - // await this.updateCallback(result.text, true);
226 - // } else {
227 - // console.warn('Final transcription returned empty text.');
228 - // }
229 - // } catch (transcribeError) {
230 - // console.error('Final transcription error:', transcribeError);
231 - // toast('Final transcription failed.', 'error');
232 - // } finally {
233 - // // Reset the microphone button state
234 - // microphoneButton.classList.remove('recording');
235 - // microphoneButton.classList.remove('mic-pulse');
236 - // microphoneButton.style.backgroundColor = '';
237 - // }
238 - // };
239 - // reader.readAsDataURL(audioBlob);
240 - }
241 - }
242 - }
243 -}
244 -
245 -export default MicrophoneInput;
246 -
247 -async function initializeMicrophoneInput() {
248 - console.log('Initializing microphone input');
249 -
250 - microphoneInput = new MicrophoneInput(
251 - async (text, isFinal) => {
252 - if (isFinal) {
253 - console.log('Final transcription callback received:', text);
254 - updateChatInput(text)
255 - // chatInput.value = text;
256 - // adjustTextareaHeight();
257 -
258 - if (!microphoneInput.messageSent) {
259 - microphoneInput.messageSent = true;
260 - console.log('Sending message');
261 - await sendMessage();
262 - }
263 - }
264 - },
265 - {
266 - modelSize: 'tiny',
267 - language: 'en',
268 - silenceThreshold: 0.07, // Adjust as needed
269 - silenceDuration: 2000, // Adjust as needed
270 - onError: (error) => {
271 - console.error('Microphone input error:', error);
272 - toast('Microphone error: ' + error.message, 'error');
273 - // Reset recording state
274 - if (microphoneButton.classList.contains('recording')) {
275 - microphoneButton.classList.remove('recording');
276 - }
277 - }
278 - }
279 - );
280 -
281 - await microphoneInput.initialize();
282 -}
283 -
284 -
285 -function toggleRecording() {
286 - console.log('toggleRecording called, isRecording:', microphoneInput.isRecording);
287 -
288 - if (microphoneInput.isRecording) {
289 - microphoneInput.stopRecording();
290 - microphoneButton.classList.remove('recording');
291 - // Add pulsing animation class
292 - microphoneButton.classList.remove('mic-pulse');
293 - } else {
294 - microphoneInput.startRecording();
295 - microphoneButton.classList.add('recording');
296 - // Add pulsing animation class
297 - microphoneButton.classList.add('mic-pulse');
298 - }
299 -
300 - // Add visual feedback
301 - microphoneButton.style.backgroundColor = microphoneInput.isRecording ? '#ff4444' : '';
302 - console.log('New recording state:', microphoneInput.isRecording);
303 -}
304 -
305 -// Some error handling for microphone input
306 -async function requestMicrophonePermission() {
307 - try {
308 - await navigator.mediaDevices.getUserMedia({ audio: true });
309 - return true;
310 - } catch (err) {
311 - console.error('Error accessing microphone:', err);
312 - toast('Microphone access denied. Please enable microphone access in your browser settings.', 'error');
313 - return false;
314 - }
315 -}
316 -// microphoneButton click event listener modifier
317 -microphoneButton.addEventListener('click', async () => {
318 - console.log('Microphone button clicked');
319 - if (isProcessingClick) {
320 - console.log('Click already being processed, ignoring');
321 - return;
322 - }
323 - isProcessingClick = true;
324 -
325 - const hasPermission = await requestMicrophonePermission();
326 - if (!hasPermission) return;
327 -
328 - if (!microphoneInput) {
329 - await initializeMicrophoneInput();
330 - }
331 -
332 - await toggleRecording();
333 -
334 - setTimeout(() => {
335 - isProcessingClick = false;
336 - }, 300); // Add a 300ms delay before allowing another click
337 -});
webui/speech.js
+43 -1
@@ -217,7 +217,8 @@ class MicrophoneInput {
217 this.silenceStartTime = null;
218
219 if (this.status === Status.LISTENING || this.status === Status.WAITING) {
220 - this.status = Status.RECORDING;
220 + if (!speech.isSpeaking()) // TODO? a better way to ignore agent's voice?
221 + this.status = Status.RECORDING;
222 }
223 } else if (this.status === Status.RECORDING) {
224 if (!this.silenceStartTime) {
@@ -347,3 +348,44 @@ async function requestMicrophonePermission() {
348 return false;
349 }
350 }
351 +
352 +
353 +class Speech {
354 + constructor() {
355 + this.synth = window.speechSynthesis;
356 + this.utterance = null;
357 + }
358 +
359 + stripEmojis(str) {
360 + return str
361 + .replace(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, '')
362 + .replace(/\s+/g, ' ')
363 + .trim();
364 + }
365 +
366 + speak(text) {
367 + console.log('Speaking:', text);
368 + // Stop any current utterance
369 + this.stop();
370 +
371 + // Remove emojis and create a new utterance
372 + text = this.stripEmojis(text);
373 + this.utterance = new SpeechSynthesisUtterance(text);
374 +
375 + // Speak the new utterance
376 + this.synth.speak(this.utterance);
377 + }
378 +
379 + stop() {
380 + if (this.isSpeaking()) {
381 + this.synth.cancel();
382 + }
383 + }
384 +
385 + isSpeaking() {
386 + return this.synth?.speaking || false;
387 + }
388 +}
389 +
390 +export const speech = new Speech();
391 +window.speech = speech
\ No newline at end of file
webui/test.html deleted
-82
@@ -1,82 +0,0 @@
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, maximum-scale=1">
6 - <title>Agent Zero</title>
7 -
8 - <script type="module">
9 - import { pipeline, read_audio } from './transformers@3.0.2.js';
10 -
11 - let transcriber;
12 - let mediaRecorder;
13 - let audioChunks = [];
14 - let isRecording = false;
15 -
16 - // Initialize the transcriber
17 - async function initTranscriber() {
18 - transcriber = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny.en');
19 - }
20 -
21 - // Toggle recording state
22 - async function toggleRecording() {
23 - if (isRecording) {
24 - stopRecording();
25 - } else {
26 - startRecording();
27 - }
28 - }
29 -
30 - // Start recording from the microphone
31 - async function startRecording() {
32 - isRecording = true;
33 - audioChunks = [];
34 - document.getElementById("micButton").innerText = "Stop Recording";
35 -
36 - const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
37 - mediaRecorder = new MediaRecorder(stream);
38 -
39 - mediaRecorder.ondataavailable = event => {
40 - audioChunks.push(event.data);
41 - };
42 -
43 - mediaRecorder.onstop = async () => {
44 - const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
45 - const audioUrl = URL.createObjectURL(audioBlob);
46 -
47 - // Use read_audio to fetch and process the audio from the URL
48 - const samplingRate = 16000; // Adjust as needed for the model
49 - const audioData = await read_audio(audioUrl, samplingRate);
50 -
51 - // Transcribe the audio
52 - const output = await transcriber(audioData);
53 - document.getElementById("transcript").innerText = output.text;
54 -
55 - // Release the object URL after use
56 - URL.revokeObjectURL(audioUrl);
57 - };
58 -
59 - mediaRecorder.start();
60 - }
61 -
62 - // Stop recording
63 - function stopRecording() {
64 - isRecording = false;
65 - document.getElementById("micButton").innerText = "Start Recording";
66 - mediaRecorder.stop();
67 - }
68 -
69 - // Expose functions to the global scope
70 - window.toggleRecording = toggleRecording;
71 -
72 - // Initialize the transcriber on page load
73 - window.onload = initTranscriber;
74 - </script>
75 -</head>
76 -
77 -<body>
78 - <h1>Agent Zero Speech Transcription</h1>
79 - <button id="micButton" onclick="toggleRecording()">Start Recording</button>
80 - <p id="transcript">Transcript will appear here...</p>
81 -</body>
82 -</html>
webui/test2.html deleted
-122
@@ -1,122 +0,0 @@
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, maximum-scale=1">
6 - <title>Agent Zero</title>
7 -
8 - <script type="module">
9 - import { pipeline, read_audio } from './transformers@3.0.2.js';
10 -
11 - let transcriber;
12 - let mediaRecorder;
13 - let isRecording = false;
14 - let audioChunks = [];
15 - let chunks_to_process = [{ tokens: [], finalized: false }];
16 -
17 - async function initTranscriber() {
18 - try {
19 - transcriber = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny.en');
20 - } catch (error) {
21 - console.error("Failed to initialize transcriber:", error);
22 - }
23 - }
24 -
25 - async function toggleRecording() {
26 - if (isRecording) {
27 - stopRecording();
28 - } else {
29 - startRecording();
30 - }
31 - }
32 -
33 - async function startRecording() {
34 - isRecording = true;
35 - document.getElementById("micButton").innerText = "Stop Recording";
36 -
37 - const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
38 - mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' });
39 -
40 - // Handle data as it’s available
41 - mediaRecorder.ondataavailable = (event) => {
42 - audioChunks.push(event.data);
43 -
44 - // Process chunks every 3 seconds
45 - if (audioChunks.length >= 3) {
46 - processAudioChunks();
47 - }
48 - };
49 -
50 - // Capture small chunks every second
51 - mediaRecorder.start(1000);
52 - }
53 -
54 - async function processAudioChunks() {
55 - const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
56 - const audioUrl = URL.createObjectURL(audioBlob);
57 -
58 - try {
59 - const audioData = await read_audio(audioUrl, 16000);
60 -
61 - if (transcriber) {
62 - await transcriber(audioData, {
63 - callback_function: mergeChunksCallback,
64 - chunk_callback: processChunkCallback,
65 - return_timestamps: true,
66 - force_full_sequences: false,
67 - });
68 - } else {
69 - console.warn("Transcriber is not ready yet.");
70 - }
71 - } catch (error) {
72 - console.error("Error during transcription:", error);
73 - }
74 -
75 - // Cleanup and reset buffer
76 - URL.revokeObjectURL(audioUrl);
77 - audioChunks = [];
78 - }
79 -
80 - function processChunkCallback(chunk) {
81 - let lastChunk = chunks_to_process[chunks_to_process.length - 1];
82 - Object.assign(lastChunk, chunk);
83 - lastChunk.finalized = true;
84 -
85 - if (!chunk.is_last) {
86 - chunks_to_process.push({ tokens: [], finalized: false });
87 - }
88 - }
89 -
90 - function mergeChunksCallback(item) {
91 - let lastChunk = chunks_to_process[chunks_to_process.length - 1];
92 - lastChunk.tokens = [...item[0].output_token_ids];
93 -
94 - // Merge text chunks and update transcript
95 - const mergedText = transcriber.tokenizer._decode_asr(chunks_to_process, {
96 - time_precision: 1.0 / transcriber.processor.feature_extractor.config.chunk_length,
97 - return_timestamps: true,
98 - force_full_sequences: false,
99 - });
100 -
101 - document.getElementById("transcript").innerText += mergedText + ' ';
102 - }
103 -
104 - function stopRecording() {
105 - isRecording = false;
106 - document.getElementById("micButton").innerText = "Start Recording";
107 - if (mediaRecorder && mediaRecorder.state !== 'inactive') {
108 - mediaRecorder.stop();
109 - }
110 - }
111 -
112 - window.toggleRecording = toggleRecording;
113 - window.onload = initTranscriber;
114 - </script>
115 -</head>
116 -
117 -<body>
118 - <h1>Agent Zero Speech Transcription</h1>
119 - <button id="micButton" onclick="toggleRecording()">Start Recording</button>
120 - <p id="transcript">Transcript will appear here...</p>
121 -</body>
122 -</html>