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">✔ Connected</h4>
63
<h4 class="disconnected" x-show="!connected">✘ 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">×</button>
220
+ <button @click="attachments.splice(index, 1); hasAttachments = attachments.length > 0"
221
+ class="remove-image">×</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>