ugly-app 0.1.985 → 0.1.986
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.
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/client/audio/useSTT.d.ts.map +1 -1
- package/dist/client/audio/useSTT.js +62 -1
- package/dist/client/audio/useSTT.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/version.ts +1 -1
- package/src/client/audio/useSTT.ts +66 -0
package/dist/cli/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.1.
|
|
1
|
+
export declare const CLI_VERSION = "0.1.986";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSTT.d.ts","sourceRoot":"","sources":["../../../src/client/audio/useSTT.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,UAAU,UAAU;IAClB,IAAI,CAAC,EAAE,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;wEAEoE;IACpE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;
|
|
1
|
+
{"version":3,"file":"useSTT.d.ts","sourceRoot":"","sources":["../../../src/client/audio/useSTT.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,UAAU,UAAU;IAClB,IAAI,CAAC,EAAE,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;wEAEoE;IACpE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAUD,wBAAgB,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,GAAE,UAAe;;;;;;;EA0MrE"}
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { AudioRecorder } from './AudioRecorder.js';
|
|
4
4
|
let sttCounter = 0;
|
|
5
|
+
// After stt:start the server owes us a frame (stt:ready at minimum). If none
|
|
6
|
+
// arrives within this window the stream is dead — the socket dropped silently
|
|
7
|
+
// or the server hung. Fail loudly instead of leaving `listening` true forever
|
|
8
|
+
// with the mic hot and the socket held. Mirrors useTTS's watchdog.
|
|
9
|
+
const WATCHDOG_MS = 30_000;
|
|
5
10
|
export function useSTT(socket, options = {}) {
|
|
6
11
|
const [transcript, setTranscript] = useState('');
|
|
7
12
|
const [isFinal, setIsFinal] = useState(false);
|
|
@@ -12,11 +17,18 @@ export function useSTT(socket, options = {}) {
|
|
|
12
17
|
const readyRef = useRef(false);
|
|
13
18
|
const cancelledRef = useRef(false);
|
|
14
19
|
const unsubsRef = useRef([]);
|
|
20
|
+
const watchdogRef = useRef(null);
|
|
15
21
|
function cleanupListeners() {
|
|
16
22
|
for (const unsub of unsubsRef.current)
|
|
17
23
|
unsub();
|
|
18
24
|
unsubsRef.current = [];
|
|
19
25
|
}
|
|
26
|
+
function clearWatchdog() {
|
|
27
|
+
if (watchdogRef.current) {
|
|
28
|
+
clearTimeout(watchdogRef.current);
|
|
29
|
+
watchdogRef.current = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
20
32
|
const start = useCallback(async () => {
|
|
21
33
|
if (listening)
|
|
22
34
|
return;
|
|
@@ -44,21 +56,60 @@ export function useSTT(socket, options = {}) {
|
|
|
44
56
|
});
|
|
45
57
|
await socket.acquire();
|
|
46
58
|
console.log('[STT] socket acquired');
|
|
59
|
+
// Fail the session like an stt:error: stop the mic (the red indicator
|
|
60
|
+
// must not outlive the session), tear down listeners, release the socket.
|
|
61
|
+
const fail = (message) => {
|
|
62
|
+
if (streamIdRef.current !== streamId)
|
|
63
|
+
return;
|
|
64
|
+
streamIdRef.current = null;
|
|
65
|
+
cancelledRef.current = true;
|
|
66
|
+
clearWatchdog();
|
|
67
|
+
recorderRef.current?.stop();
|
|
68
|
+
setError(message);
|
|
69
|
+
setListening(false);
|
|
70
|
+
cleanupListeners();
|
|
71
|
+
socket.release();
|
|
72
|
+
};
|
|
73
|
+
// Arms the start-window watchdog — see the arm site below for why only
|
|
74
|
+
// this window is watched.
|
|
75
|
+
const armWatchdog = () => {
|
|
76
|
+
clearWatchdog();
|
|
77
|
+
watchdogRef.current = setTimeout(() => {
|
|
78
|
+
watchdogRef.current = null;
|
|
79
|
+
console.error(`[STT] watchdog — no server frame within ${WATCHDOG_MS}ms of stt:start streamId=${streamId}`);
|
|
80
|
+
fail('STT stream timed out — no response from server');
|
|
81
|
+
}, WATCHDOG_MS);
|
|
82
|
+
};
|
|
47
83
|
cleanupListeners();
|
|
84
|
+
const unsubClose = socket.onClose?.(() => {
|
|
85
|
+
// The transport dropped mid-session: no more frames will arrive (the
|
|
86
|
+
// stream is dead server-side and is not replayed after the socket's
|
|
87
|
+
// auto-reconnect). Fail instead of hanging in `listening` forever.
|
|
88
|
+
if (streamIdRef.current !== streamId)
|
|
89
|
+
return;
|
|
90
|
+
console.error(`[STT] connection lost streamId=${streamId}`);
|
|
91
|
+
fail('connection lost');
|
|
92
|
+
});
|
|
48
93
|
unsubsRef.current.push(socket.on('stt:ready', (data) => {
|
|
49
94
|
if (data.streamId !== streamId)
|
|
50
95
|
return;
|
|
51
96
|
console.log(`[STT] ready streamId=${streamId}`);
|
|
97
|
+
// First proof the server owns the stream — the start-window watchdog
|
|
98
|
+
// has done its job; idle silence from here on is legitimate.
|
|
99
|
+
clearWatchdog();
|
|
52
100
|
readyRef.current = true;
|
|
53
101
|
}), socket.on('stt:transcript', (data) => {
|
|
54
102
|
if (data.streamId !== streamId)
|
|
55
103
|
return;
|
|
104
|
+
// A transcript before stt:ready still proves the stream is alive.
|
|
105
|
+
clearWatchdog();
|
|
56
106
|
console.log(`[STT] transcript isFinal=${data.isFinal} text="${data.text}" (after ${chunkCount} chunks)`);
|
|
57
107
|
setTranscript(data.text);
|
|
58
108
|
setIsFinal(data.isFinal);
|
|
59
109
|
}), socket.on('stt:error', (data) => {
|
|
60
110
|
if (data.streamId !== streamId)
|
|
61
111
|
return;
|
|
112
|
+
clearWatchdog();
|
|
62
113
|
console.error(`[STT] error streamId=${streamId}: ${data.message}`);
|
|
63
114
|
setError(data.message);
|
|
64
115
|
setListening(false);
|
|
@@ -67,17 +118,25 @@ export function useSTT(socket, options = {}) {
|
|
|
67
118
|
}), socket.on('stt:stopped', (data) => {
|
|
68
119
|
if (data.streamId !== streamId)
|
|
69
120
|
return;
|
|
121
|
+
clearWatchdog();
|
|
70
122
|
console.log(`[STT] stopped streamId=${streamId} (sent ${chunkCount} audio chunks)`);
|
|
71
123
|
setListening(false);
|
|
72
124
|
cleanupListeners();
|
|
73
125
|
socket.release();
|
|
74
|
-
}));
|
|
126
|
+
}), ...(unsubClose ? [unsubClose] : []));
|
|
75
127
|
socket.send('stt:start', {
|
|
76
128
|
streamId,
|
|
77
129
|
mode,
|
|
78
130
|
enableVAD,
|
|
79
131
|
...(options.lang !== undefined ? { lang: options.lang } : {}),
|
|
80
132
|
});
|
|
133
|
+
// Watchdog ONLY the start window: after stt:start a response (stt:ready)
|
|
134
|
+
// is unconditionally outstanding, so silence here means a dead stream.
|
|
135
|
+
// Once any frame arrives the watchdog is cleared for the rest of the
|
|
136
|
+
// session — realtime+VAD skips silence, so long gaps with no server
|
|
137
|
+
// frames are EXPECTED while the user isn't speaking, and a mid-session
|
|
138
|
+
// socket drop is surfaced via onClose instead.
|
|
139
|
+
armWatchdog();
|
|
81
140
|
// Wait for stt:ready (timeout after 5s)
|
|
82
141
|
const readyStart = Date.now();
|
|
83
142
|
await new Promise((resolve) => {
|
|
@@ -104,6 +163,7 @@ export function useSTT(socket, options = {}) {
|
|
|
104
163
|
const stop = useCallback(() => {
|
|
105
164
|
console.log(`[STT] stop streamId=${streamIdRef.current}`);
|
|
106
165
|
cancelledRef.current = true;
|
|
166
|
+
clearWatchdog();
|
|
107
167
|
recorderRef.current?.stop();
|
|
108
168
|
if (streamIdRef.current) {
|
|
109
169
|
socket.send('stt:stop', { streamId: streamIdRef.current });
|
|
@@ -113,6 +173,7 @@ export function useSTT(socket, options = {}) {
|
|
|
113
173
|
}, [socket]);
|
|
114
174
|
useEffect(() => {
|
|
115
175
|
return () => {
|
|
176
|
+
clearWatchdog();
|
|
116
177
|
recorderRef.current?.stop();
|
|
117
178
|
if (streamIdRef.current) {
|
|
118
179
|
socket.send('stt:stop', { streamId: streamIdRef.current });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSTT.js","sourceRoot":"","sources":["../../../src/client/audio/useSTT.ts"],"names":[],"mappings":"AAAA,6BAA6B;AAC7B,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEjE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAWnD,IAAI,UAAU,GAAG,CAAC,CAAC;AAEnB,MAAM,UAAU,MAAM,CAAC,MAAqB,EAAE,UAAsB,EAAE;IACpE,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IACjD,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IAExD,MAAM,WAAW,GAAG,MAAM,CAAuB,IAAI,CAAC,CAAC;IACvD,MAAM,WAAW,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC/B,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,SAAS,GAAG,MAAM,CAAiB,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"useSTT.js","sourceRoot":"","sources":["../../../src/client/audio/useSTT.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ugly-app",
|
|
3
3
|
"packageManager": "pnpm@10.34.5",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.986",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"comment:files": "Allowlist what ships to npm. dist = runtime; src = sourcemap targets (dist/*.js.map reference ../../src/); templates = CLI scaffold. Everything else at repo root (.pgdata local Postgres, coverage, assets/icons sources, test/, test-results/) is excluded by omission. The !negations strip the scaffold's installed deps + cruft (templates/node_modules is 200MB+ and must never ship). package.json/README/LICENSE ship automatically.",
|
|
7
7
|
"files": [
|
package/src/cli/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by prebuild — do not edit manually
|
|
2
|
-
export const CLI_VERSION = "0.1.
|
|
2
|
+
export const CLI_VERSION = "0.1.986";
|
|
@@ -14,6 +14,12 @@ interface STTOptions {
|
|
|
14
14
|
|
|
15
15
|
let sttCounter = 0;
|
|
16
16
|
|
|
17
|
+
// After stt:start the server owes us a frame (stt:ready at minimum). If none
|
|
18
|
+
// arrives within this window the stream is dead — the socket dropped silently
|
|
19
|
+
// or the server hung. Fail loudly instead of leaving `listening` true forever
|
|
20
|
+
// with the mic hot and the socket held. Mirrors useTTS's watchdog.
|
|
21
|
+
const WATCHDOG_MS = 30_000;
|
|
22
|
+
|
|
17
23
|
export function useSTT(socket: UglyBotSocket, options: STTOptions = {}) {
|
|
18
24
|
const [transcript, setTranscript] = useState('');
|
|
19
25
|
const [isFinal, setIsFinal] = useState(false);
|
|
@@ -25,12 +31,20 @@ export function useSTT(socket: UglyBotSocket, options: STTOptions = {}) {
|
|
|
25
31
|
const readyRef = useRef(false);
|
|
26
32
|
const cancelledRef = useRef(false);
|
|
27
33
|
const unsubsRef = useRef<(() => void)[]>([]);
|
|
34
|
+
const watchdogRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
28
35
|
|
|
29
36
|
function cleanupListeners() {
|
|
30
37
|
for (const unsub of unsubsRef.current) unsub();
|
|
31
38
|
unsubsRef.current = [];
|
|
32
39
|
}
|
|
33
40
|
|
|
41
|
+
function clearWatchdog() {
|
|
42
|
+
if (watchdogRef.current) {
|
|
43
|
+
clearTimeout(watchdogRef.current);
|
|
44
|
+
watchdogRef.current = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
const start = useCallback(async () => {
|
|
35
49
|
if (listening) return;
|
|
36
50
|
const streamId = `stt-${++sttCounter}`;
|
|
@@ -64,15 +78,54 @@ export function useSTT(socket: UglyBotSocket, options: STTOptions = {}) {
|
|
|
64
78
|
await socket.acquire();
|
|
65
79
|
console.log('[STT] socket acquired');
|
|
66
80
|
|
|
81
|
+
// Fail the session like an stt:error: stop the mic (the red indicator
|
|
82
|
+
// must not outlive the session), tear down listeners, release the socket.
|
|
83
|
+
const fail = (message: string) => {
|
|
84
|
+
if (streamIdRef.current !== streamId) return;
|
|
85
|
+
streamIdRef.current = null;
|
|
86
|
+
cancelledRef.current = true;
|
|
87
|
+
clearWatchdog();
|
|
88
|
+
recorderRef.current?.stop();
|
|
89
|
+
setError(message);
|
|
90
|
+
setListening(false);
|
|
91
|
+
cleanupListeners();
|
|
92
|
+
socket.release();
|
|
93
|
+
};
|
|
94
|
+
// Arms the start-window watchdog — see the arm site below for why only
|
|
95
|
+
// this window is watched.
|
|
96
|
+
const armWatchdog = () => {
|
|
97
|
+
clearWatchdog();
|
|
98
|
+
watchdogRef.current = setTimeout(() => {
|
|
99
|
+
watchdogRef.current = null;
|
|
100
|
+
console.error(
|
|
101
|
+
`[STT] watchdog — no server frame within ${WATCHDOG_MS}ms of stt:start streamId=${streamId}`,
|
|
102
|
+
);
|
|
103
|
+
fail('STT stream timed out — no response from server');
|
|
104
|
+
}, WATCHDOG_MS);
|
|
105
|
+
};
|
|
106
|
+
|
|
67
107
|
cleanupListeners();
|
|
108
|
+
const unsubClose = socket.onClose?.(() => {
|
|
109
|
+
// The transport dropped mid-session: no more frames will arrive (the
|
|
110
|
+
// stream is dead server-side and is not replayed after the socket's
|
|
111
|
+
// auto-reconnect). Fail instead of hanging in `listening` forever.
|
|
112
|
+
if (streamIdRef.current !== streamId) return;
|
|
113
|
+
console.error(`[STT] connection lost streamId=${streamId}`);
|
|
114
|
+
fail('connection lost');
|
|
115
|
+
});
|
|
68
116
|
unsubsRef.current.push(
|
|
69
117
|
socket.on('stt:ready', (data) => {
|
|
70
118
|
if (data.streamId !== streamId) return;
|
|
71
119
|
console.log(`[STT] ready streamId=${streamId}`);
|
|
120
|
+
// First proof the server owns the stream — the start-window watchdog
|
|
121
|
+
// has done its job; idle silence from here on is legitimate.
|
|
122
|
+
clearWatchdog();
|
|
72
123
|
readyRef.current = true;
|
|
73
124
|
}),
|
|
74
125
|
socket.on('stt:transcript', (data) => {
|
|
75
126
|
if (data.streamId !== streamId) return;
|
|
127
|
+
// A transcript before stt:ready still proves the stream is alive.
|
|
128
|
+
clearWatchdog();
|
|
76
129
|
console.log(
|
|
77
130
|
`[STT] transcript isFinal=${data.isFinal} text="${data.text}" (after ${chunkCount} chunks)`,
|
|
78
131
|
);
|
|
@@ -81,6 +134,7 @@ export function useSTT(socket: UglyBotSocket, options: STTOptions = {}) {
|
|
|
81
134
|
}),
|
|
82
135
|
socket.on('stt:error', (data) => {
|
|
83
136
|
if (data.streamId !== streamId) return;
|
|
137
|
+
clearWatchdog();
|
|
84
138
|
console.error(`[STT] error streamId=${streamId}: ${data.message}`);
|
|
85
139
|
setError(data.message);
|
|
86
140
|
setListening(false);
|
|
@@ -89,6 +143,7 @@ export function useSTT(socket: UglyBotSocket, options: STTOptions = {}) {
|
|
|
89
143
|
}),
|
|
90
144
|
socket.on('stt:stopped', (data) => {
|
|
91
145
|
if (data.streamId !== streamId) return;
|
|
146
|
+
clearWatchdog();
|
|
92
147
|
console.log(
|
|
93
148
|
`[STT] stopped streamId=${streamId} (sent ${chunkCount} audio chunks)`,
|
|
94
149
|
);
|
|
@@ -96,6 +151,7 @@ export function useSTT(socket: UglyBotSocket, options: STTOptions = {}) {
|
|
|
96
151
|
cleanupListeners();
|
|
97
152
|
socket.release();
|
|
98
153
|
}),
|
|
154
|
+
...(unsubClose ? [unsubClose] : []),
|
|
99
155
|
);
|
|
100
156
|
|
|
101
157
|
socket.send('stt:start', {
|
|
@@ -105,6 +161,14 @@ export function useSTT(socket: UglyBotSocket, options: STTOptions = {}) {
|
|
|
105
161
|
...(options.lang !== undefined ? { lang: options.lang } : {}),
|
|
106
162
|
});
|
|
107
163
|
|
|
164
|
+
// Watchdog ONLY the start window: after stt:start a response (stt:ready)
|
|
165
|
+
// is unconditionally outstanding, so silence here means a dead stream.
|
|
166
|
+
// Once any frame arrives the watchdog is cleared for the rest of the
|
|
167
|
+
// session — realtime+VAD skips silence, so long gaps with no server
|
|
168
|
+
// frames are EXPECTED while the user isn't speaking, and a mid-session
|
|
169
|
+
// socket drop is surfaced via onClose instead.
|
|
170
|
+
armWatchdog();
|
|
171
|
+
|
|
108
172
|
// Wait for stt:ready (timeout after 5s)
|
|
109
173
|
const readyStart = Date.now();
|
|
110
174
|
await new Promise<void>((resolve) => {
|
|
@@ -136,6 +200,7 @@ export function useSTT(socket: UglyBotSocket, options: STTOptions = {}) {
|
|
|
136
200
|
const stop = useCallback(() => {
|
|
137
201
|
console.log(`[STT] stop streamId=${streamIdRef.current}`);
|
|
138
202
|
cancelledRef.current = true;
|
|
203
|
+
clearWatchdog();
|
|
139
204
|
recorderRef.current?.stop();
|
|
140
205
|
if (streamIdRef.current) {
|
|
141
206
|
socket.send('stt:stop', { streamId: streamIdRef.current });
|
|
@@ -146,6 +211,7 @@ export function useSTT(socket: UglyBotSocket, options: STTOptions = {}) {
|
|
|
146
211
|
|
|
147
212
|
useEffect(() => {
|
|
148
213
|
return () => {
|
|
214
|
+
clearWatchdog();
|
|
149
215
|
recorderRef.current?.stop();
|
|
150
216
|
if (streamIdRef.current) {
|
|
151
217
|
socket.send('stt:stop', { streamId: streamIdRef.current });
|