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.
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.1.985";
1
+ export declare const CLI_VERSION = "0.1.986";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.985";
2
+ export const CLI_VERSION = "0.1.986";
3
3
  //# sourceMappingURL=version.js.map
@@ -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;AAID,wBAAgB,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,GAAE,UAAe;;;;;;;EA8IrE"}
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":"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"}
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,6EAA6E;AAC7E,8EAA8E;AAC9E,8EAA8E;AAC9E,mEAAmE;AACnE,MAAM,WAAW,GAAG,MAAM,CAAC;AAE3B,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;IAC7C,MAAM,WAAW,GAAG,MAAM,CAAuC,IAAI,CAAC,CAAC;IAEvE,SAAS,gBAAgB;QACvB,KAAK,MAAM,KAAK,IAAI,SAAS,CAAC,OAAO;YAAE,KAAK,EAAE,CAAC;QAC/C,SAAS,CAAC,OAAO,GAAG,EAAE,CAAC;IACzB,CAAC;IAED,SAAS,aAAa;QACpB,IAAI,WAAW,CAAC,OAAO,EAAE,CAAC;YACxB,YAAY,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;YAClC,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACnC,IAAI,SAAS;YAAE,OAAO;QACtB,MAAM,QAAQ,GAAG,OAAO,EAAE,UAAU,EAAE,CAAC;QACvC,WAAW,CAAC,OAAO,GAAG,QAAQ,CAAC;QAC/B,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAC;QACzB,YAAY,CAAC,OAAO,GAAG,KAAK,CAAC;QAC7B,aAAa,CAAC,EAAE,CAAC,CAAC;QAClB,UAAU,CAAC,KAAK,CAAC,CAAC;QAClB,QAAQ,CAAC,IAAI,CAAC,CAAC;QACf,YAAY,CAAC,IAAI,CAAC,CAAC;QAEnB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,MAAM,CAAC;QACpC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,IAAI,KAAK,UAAU,CAAC;QAC3D,OAAO,CAAC,GAAG,CACT,wBAAwB,QAAQ,SAAS,IAAI,QAAQ,SAAS,SAAS,OAAO,CAAC,IAAI,IAAI,MAAM,EAAE,CAChG,CAAC;QAEF,8EAA8E;QAC9E,IAAI,CAAC,WAAW,CAAC,OAAO;YAAE,WAAW,CAAC,OAAO,GAAG,IAAI,aAAa,EAAE,CAAC;QACpE,IAAI,UAAU,GAAG,CAAC,CAAC;QACnB,MAAM,QAAQ,GAAG,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;YACnD,UAAU,EAAE,CAAC;YACb,IAAI,UAAU,IAAI,CAAC,IAAI,UAAU,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC;gBAC7C,OAAO,CAAC,GAAG,CACT,sBAAsB,UAAU,KAAK,KAAK,CAAC,MAAM,aAAa,CAC/D,CAAC;YACJ,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;QAErC,sEAAsE;QACtE,0EAA0E;QAC1E,MAAM,IAAI,GAAG,CAAC,OAAe,EAAE,EAAE;YAC/B,IAAI,WAAW,CAAC,OAAO,KAAK,QAAQ;gBAAE,OAAO;YAC7C,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;YAC3B,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;YAC5B,aAAa,EAAE,CAAC;YAChB,WAAW,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;YAC5B,QAAQ,CAAC,OAAO,CAAC,CAAC;YAClB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,gBAAgB,EAAE,CAAC;YACnB,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC,CAAC;QACF,uEAAuE;QACvE,0BAA0B;QAC1B,MAAM,WAAW,GAAG,GAAG,EAAE;YACvB,aAAa,EAAE,CAAC;YAChB,WAAW,CAAC,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;gBACpC,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;gBAC3B,OAAO,CAAC,KAAK,CACX,2CAA2C,WAAW,4BAA4B,QAAQ,EAAE,CAC7F,CAAC;gBACF,IAAI,CAAC,gDAAgD,CAAC,CAAC;YACzD,CAAC,EAAE,WAAW,CAAC,CAAC;QAClB,CAAC,CAAC;QAEF,gBAAgB,EAAE,CAAC;QACnB,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE;YACvC,qEAAqE;YACrE,oEAAoE;YACpE,mEAAmE;YACnE,IAAI,WAAW,CAAC,OAAO,KAAK,QAAQ;gBAAE,OAAO;YAC7C,OAAO,CAAC,KAAK,CAAC,kCAAkC,QAAQ,EAAE,CAAC,CAAC;YAC5D,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;QACH,SAAS,CAAC,OAAO,CAAC,IAAI,CACpB,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE;YAC9B,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ;gBAAE,OAAO;YACvC,OAAO,CAAC,GAAG,CAAC,wBAAwB,QAAQ,EAAE,CAAC,CAAC;YAChD,qEAAqE;YACrE,6DAA6D;YAC7D,aAAa,EAAE,CAAC;YAChB,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC;QAC1B,CAAC,CAAC,EACF,MAAM,CAAC,EAAE,CAAC,gBAAgB,EAAE,CAAC,IAAI,EAAE,EAAE;YACnC,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ;gBAAE,OAAO;YACvC,kEAAkE;YAClE,aAAa,EAAE,CAAC;YAChB,OAAO,CAAC,GAAG,CACT,4BAA4B,IAAI,CAAC,OAAO,UAAU,IAAI,CAAC,IAAI,YAAY,UAAU,UAAU,CAC5F,CAAC;YACF,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACzB,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3B,CAAC,CAAC,EACF,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE;YAC9B,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ;gBAAE,OAAO;YACvC,aAAa,EAAE,CAAC;YAChB,OAAO,CAAC,KAAK,CAAC,wBAAwB,QAAQ,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;YACnE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACvB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,gBAAgB,EAAE,CAAC;YACnB,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC,CAAC,EACF,MAAM,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC,IAAI,EAAE,EAAE;YAChC,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ;gBAAE,OAAO;YACvC,aAAa,EAAE,CAAC;YAChB,OAAO,CAAC,GAAG,CACT,0BAA0B,QAAQ,UAAU,UAAU,gBAAgB,CACvE,CAAC;YACF,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,gBAAgB,EAAE,CAAC;YACnB,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC,CAAC,EACF,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CACpC,CAAC;QAEF,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE;YACvB,QAAQ;YACR,IAAI;YACJ,SAAS;YACT,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9D,CAAC,CAAC;QAEH,yEAAyE;QACzE,uEAAuE;QACvE,qEAAqE;QACrE,oEAAoE;QACpE,uEAAuE;QACvE,+CAA+C;QAC/C,WAAW,EAAE,CAAC;QAEd,wCAAwC;QACxC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC9B,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAClC,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE;gBAC5B,IAAI,QAAQ,CAAC,OAAO,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,GAAG,IAAI,EAAE,CAAC;oBACvD,aAAa,CAAC,IAAI,CAAC,CAAC;oBACpB,OAAO,EAAE,CAAC;gBACZ,CAAC;YACH,CAAC,EAAE,EAAE,CAAC,CAAC;QACT,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;YACtB,OAAO,CAAC,IAAI,CACV,6BAA6B,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,wBAAwB,CAC7E,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,IAAI,CAAC,CAAC;QAC7D,CAAC;QAED,IAAI,YAAY,CAAC,OAAO,EAAE,CAAC;YACzB,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;YACxD,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,CAAC;QACf,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;IACzC,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;IAEvE,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE;QAC5B,OAAO,CAAC,GAAG,CAAC,uBAAuB,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QAC1D,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;QAC5B,aAAa,EAAE,CAAC;QAChB,WAAW,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QAC5B,IAAI,WAAW,CAAC,OAAO,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7D,CAAC;QACD,gBAAgB,EAAE,CAAC;QACnB,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,GAAG,EAAE;YACV,aAAa,EAAE,CAAC;YAChB,WAAW,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;YAC5B,IAAI,WAAW,CAAC,OAAO,EAAE,CAAC;gBACxB,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YAC7D,CAAC;YACD,gBAAgB,EAAE,CAAC;YACnB,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AAChE,CAAC"}
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.985",
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": [
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.985";
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 });