openai 7.13.0 → 7.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/client.d.mts +3 -0
- package/client.d.mts.map +1 -1
- package/client.d.ts +3 -0
- package/client.d.ts.map +1 -1
- package/client.js +3 -0
- package/client.js.map +1 -1
- package/client.mjs +3 -0
- package/client.mjs.map +1 -1
- package/helpers/live.d.mts +2 -0
- package/helpers/live.d.mts.map +1 -0
- package/helpers/live.d.ts +2 -0
- package/helpers/live.d.ts.map +1 -0
- package/helpers/live.js +6 -0
- package/helpers/live.js.map +1 -0
- package/helpers/live.mjs +2 -0
- package/helpers/live.mjs.map +1 -0
- package/lib/live/transcript-grouper.d.mts +96 -0
- package/lib/live/transcript-grouper.d.mts.map +1 -0
- package/lib/live/transcript-grouper.d.ts +96 -0
- package/lib/live/transcript-grouper.d.ts.map +1 -0
- package/lib/live/transcript-grouper.js +223 -0
- package/lib/live/transcript-grouper.js.map +1 -0
- package/lib/live/transcript-grouper.mjs +219 -0
- package/lib/live/transcript-grouper.mjs.map +1 -0
- package/lib/live/transcript-grouping.d.mts +49 -0
- package/lib/live/transcript-grouping.d.mts.map +1 -0
- package/lib/live/transcript-grouping.d.ts +49 -0
- package/lib/live/transcript-grouping.d.ts.map +1 -0
- package/lib/live/transcript-grouping.js +287 -0
- package/lib/live/transcript-grouping.js.map +1 -0
- package/lib/live/transcript-grouping.mjs +283 -0
- package/lib/live/transcript-grouping.mjs.map +1 -0
- package/lib/live/webrtc.d.mts +13 -0
- package/lib/live/webrtc.d.mts.map +1 -0
- package/lib/live/webrtc.d.ts +13 -0
- package/lib/live/webrtc.d.ts.map +1 -0
- package/lib/live/webrtc.js +16 -0
- package/lib/live/webrtc.js.map +1 -0
- package/lib/live/webrtc.mjs +10 -0
- package/lib/live/webrtc.mjs.map +1 -0
- package/lib/realtime/webrtc.d.mts +13 -0
- package/lib/realtime/webrtc.d.mts.map +1 -0
- package/lib/realtime/webrtc.d.ts +13 -0
- package/lib/realtime/webrtc.d.ts.map +1 -0
- package/lib/realtime/webrtc.js +16 -0
- package/lib/realtime/webrtc.js.map +1 -0
- package/lib/realtime/webrtc.mjs +10 -0
- package/lib/realtime/webrtc.mjs.map +1 -0
- package/lib/webrtc/connection.d.mts +40 -0
- package/lib/webrtc/connection.d.mts.map +1 -0
- package/lib/webrtc/connection.d.ts +40 -0
- package/lib/webrtc/connection.d.ts.map +1 -0
- package/lib/webrtc/connection.js +228 -0
- package/lib/webrtc/connection.js.map +1 -0
- package/lib/webrtc/connection.mjs +224 -0
- package/lib/webrtc/connection.mjs.map +1 -0
- package/lib/webrtc/data-channel.d.mts +32 -0
- package/lib/webrtc/data-channel.d.mts.map +1 -0
- package/lib/webrtc/data-channel.d.ts +32 -0
- package/lib/webrtc/data-channel.d.ts.map +1 -0
- package/lib/webrtc/data-channel.js +125 -0
- package/lib/webrtc/data-channel.js.map +1 -0
- package/lib/webrtc/data-channel.mjs +121 -0
- package/lib/webrtc/data-channel.mjs.map +1 -0
- package/lib/webrtc/errors.d.mts +19 -0
- package/lib/webrtc/errors.d.mts.map +1 -0
- package/lib/webrtc/errors.d.ts +19 -0
- package/lib/webrtc/errors.d.ts.map +1 -0
- package/lib/webrtc/errors.js +18 -0
- package/lib/webrtc/errors.js.map +1 -0
- package/lib/webrtc/errors.mjs +14 -0
- package/lib/webrtc/errors.mjs.map +1 -0
- package/lib/webrtc/subscriptions.d.mts +13 -0
- package/lib/webrtc/subscriptions.d.mts.map +1 -0
- package/lib/webrtc/subscriptions.d.ts +13 -0
- package/lib/webrtc/subscriptions.d.ts.map +1 -0
- package/lib/webrtc/subscriptions.js +62 -0
- package/lib/webrtc/subscriptions.js.map +1 -0
- package/lib/webrtc/subscriptions.mjs +58 -0
- package/lib/webrtc/subscriptions.mjs.map +1 -0
- package/lib/webrtc/types.d.mts +99 -0
- package/lib/webrtc/types.d.mts.map +1 -0
- package/lib/webrtc/types.d.ts +99 -0
- package/lib/webrtc/types.d.ts.map +1 -0
- package/lib/webrtc/types.js +3 -0
- package/lib/webrtc/types.js.map +1 -0
- package/lib/webrtc/types.mjs +2 -0
- package/lib/webrtc/types.mjs.map +1 -0
- package/live/webrtc.d.mts +3 -0
- package/live/webrtc.d.mts.map +1 -0
- package/live/webrtc.d.ts +3 -0
- package/live/webrtc.d.ts.map +1 -0
- package/live/webrtc.js +8 -0
- package/live/webrtc.js.map +1 -0
- package/live/webrtc.mjs +2 -0
- package/live/webrtc.mjs.map +1 -0
- package/package.json +11 -1
- package/realtime/webrtc.d.mts +3 -0
- package/realtime/webrtc.d.mts.map +1 -0
- package/realtime/webrtc.d.ts +3 -0
- package/realtime/webrtc.d.ts.map +1 -0
- package/realtime/webrtc.js +8 -0
- package/realtime/webrtc.js.map +1 -0
- package/realtime/webrtc.mjs +2 -0
- package/realtime/webrtc.mjs.map +1 -0
- package/resources/beta/responses/responses.d.mts +48 -0
- package/resources/beta/responses/responses.d.mts.map +1 -1
- package/resources/beta/responses/responses.d.ts +48 -0
- package/resources/beta/responses/responses.d.ts.map +1 -1
- package/resources/beta/responses/responses.js.map +1 -1
- package/resources/beta/responses/responses.mjs.map +1 -1
- package/resources/conversations/items.d.mts +16 -0
- package/resources/conversations/items.d.mts.map +1 -1
- package/resources/conversations/items.d.ts +16 -0
- package/resources/conversations/items.d.ts.map +1 -1
- package/resources/index.d.mts +1 -0
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +1 -0
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js +3 -1
- package/resources/index.js.map +1 -1
- package/resources/index.mjs +1 -0
- package/resources/index.mjs.map +1 -1
- package/resources/live/forks/forks.d.mts +110 -0
- package/resources/live/forks/forks.d.mts.map +1 -0
- package/resources/live/forks/forks.d.ts +110 -0
- package/resources/live/forks/forks.d.ts.map +1 -0
- package/resources/live/forks/forks.js +9 -0
- package/resources/live/forks/forks.js.map +1 -0
- package/resources/live/forks/forks.mjs +5 -0
- package/resources/live/forks/forks.mjs.map +1 -0
- package/resources/live/forks/index.d.mts +4 -0
- package/resources/live/forks/index.d.mts.map +1 -0
- package/resources/live/forks/index.d.ts +4 -0
- package/resources/live/forks/index.d.ts.map +1 -0
- package/resources/live/forks/index.js +7 -0
- package/resources/live/forks/index.js.map +1 -0
- package/resources/live/forks/index.mjs +3 -0
- package/resources/live/forks/index.mjs.map +1 -0
- package/resources/live/forks/internal-base.d.mts +73 -0
- package/resources/live/forks/internal-base.d.mts.map +1 -0
- package/resources/live/forks/internal-base.d.ts +73 -0
- package/resources/live/forks/internal-base.d.ts.map +1 -0
- package/resources/live/forks/internal-base.js +49 -0
- package/resources/live/forks/internal-base.js.map +1 -0
- package/resources/live/forks/internal-base.mjs +43 -0
- package/resources/live/forks/internal-base.mjs.map +1 -0
- package/resources/live/forks/ws-base.d.mts +109 -0
- package/resources/live/forks/ws-base.d.mts.map +1 -0
- package/resources/live/forks/ws-base.d.ts +109 -0
- package/resources/live/forks/ws-base.d.ts.map +1 -0
- package/resources/live/forks/ws-base.js +476 -0
- package/resources/live/forks/ws-base.js.map +1 -0
- package/resources/live/forks/ws-base.mjs +472 -0
- package/resources/live/forks/ws-base.mjs.map +1 -0
- package/resources/live/forks/ws.d.mts +13 -0
- package/resources/live/forks/ws.d.mts.map +1 -0
- package/resources/live/forks/ws.d.ts +13 -0
- package/resources/live/forks/ws.d.ts.map +1 -0
- package/resources/live/forks/ws.js +34 -0
- package/resources/live/forks/ws.js.map +1 -0
- package/resources/live/forks/ws.mjs +29 -0
- package/resources/live/forks/ws.mjs.map +1 -0
- package/resources/live/forks.d.mts +2 -0
- package/resources/live/forks.d.mts.map +1 -0
- package/resources/live/forks.d.ts +2 -0
- package/resources/live/forks.d.ts.map +1 -0
- package/resources/live/forks.js +6 -0
- package/resources/live/forks.js.map +1 -0
- package/resources/live/forks.mjs +3 -0
- package/resources/live/forks.mjs.map +1 -0
- package/resources/live/index.d.mts +7 -0
- package/resources/live/index.d.mts.map +1 -0
- package/resources/live/index.d.ts +7 -0
- package/resources/live/index.d.ts.map +1 -0
- package/resources/live/index.js +13 -0
- package/resources/live/index.js.map +1 -0
- package/resources/live/index.mjs +6 -0
- package/resources/live/index.mjs.map +1 -0
- package/resources/live/internal-base.d.mts +71 -0
- package/resources/live/internal-base.d.mts.map +1 -0
- package/resources/live/internal-base.d.ts +71 -0
- package/resources/live/internal-base.d.ts.map +1 -0
- package/resources/live/internal-base.js +48 -0
- package/resources/live/internal-base.js.map +1 -0
- package/resources/live/internal-base.mjs +42 -0
- package/resources/live/internal-base.mjs.map +1 -0
- package/resources/live/live.d.mts +1864 -0
- package/resources/live/live.d.mts.map +1 -0
- package/resources/live/live.d.ts +1864 -0
- package/resources/live/live.d.ts.map +1 -0
- package/resources/live/live.js +40 -0
- package/resources/live/live.js.map +1 -0
- package/resources/live/live.mjs +35 -0
- package/resources/live/live.mjs.map +1 -0
- package/resources/live/sessions.d.mts +254 -0
- package/resources/live/sessions.d.mts.map +1 -0
- package/resources/live/sessions.d.ts +254 -0
- package/resources/live/sessions.d.ts.map +1 -0
- package/resources/live/sessions.js +118 -0
- package/resources/live/sessions.js.map +1 -0
- package/resources/live/sessions.mjs +114 -0
- package/resources/live/sessions.mjs.map +1 -0
- package/resources/live/sideband/index.d.mts +4 -0
- package/resources/live/sideband/index.d.mts.map +1 -0
- package/resources/live/sideband/index.d.ts +4 -0
- package/resources/live/sideband/index.d.ts.map +1 -0
- package/resources/live/sideband/index.js +7 -0
- package/resources/live/sideband/index.js.map +1 -0
- package/resources/live/sideband/index.mjs +3 -0
- package/resources/live/sideband/index.mjs.map +1 -0
- package/resources/live/sideband/internal-base.d.mts +73 -0
- package/resources/live/sideband/internal-base.d.mts.map +1 -0
- package/resources/live/sideband/internal-base.d.ts +73 -0
- package/resources/live/sideband/internal-base.d.ts.map +1 -0
- package/resources/live/sideband/internal-base.js +49 -0
- package/resources/live/sideband/internal-base.js.map +1 -0
- package/resources/live/sideband/internal-base.mjs +43 -0
- package/resources/live/sideband/internal-base.mjs.map +1 -0
- package/resources/live/sideband/sideband.d.mts +25 -0
- package/resources/live/sideband/sideband.d.mts.map +1 -0
- package/resources/live/sideband/sideband.d.ts +25 -0
- package/resources/live/sideband/sideband.d.ts.map +1 -0
- package/resources/live/sideband/sideband.js +9 -0
- package/resources/live/sideband/sideband.js.map +1 -0
- package/resources/live/sideband/sideband.mjs +5 -0
- package/resources/live/sideband/sideband.mjs.map +1 -0
- package/resources/live/sideband/ws-base.d.mts +114 -0
- package/resources/live/sideband/ws-base.d.mts.map +1 -0
- package/resources/live/sideband/ws-base.d.ts +114 -0
- package/resources/live/sideband/ws-base.d.ts.map +1 -0
- package/resources/live/sideband/ws-base.js +476 -0
- package/resources/live/sideband/ws-base.js.map +1 -0
- package/resources/live/sideband/ws-base.mjs +472 -0
- package/resources/live/sideband/ws-base.mjs.map +1 -0
- package/resources/live/sideband/ws.d.mts +13 -0
- package/resources/live/sideband/ws.d.mts.map +1 -0
- package/resources/live/sideband/ws.d.ts +13 -0
- package/resources/live/sideband/ws.d.ts.map +1 -0
- package/resources/live/sideband/ws.js +34 -0
- package/resources/live/sideband/ws.js.map +1 -0
- package/resources/live/sideband/ws.mjs +29 -0
- package/resources/live/sideband/ws.mjs.map +1 -0
- package/resources/live/sideband.d.mts +2 -0
- package/resources/live/sideband.d.mts.map +1 -0
- package/resources/live/sideband.d.ts +2 -0
- package/resources/live/sideband.d.ts.map +1 -0
- package/resources/live/sideband.js +6 -0
- package/resources/live/sideband.js.map +1 -0
- package/resources/live/sideband.mjs +3 -0
- package/resources/live/sideband.mjs.map +1 -0
- package/resources/live/ws-base.d.mts +106 -0
- package/resources/live/ws-base.d.mts.map +1 -0
- package/resources/live/ws-base.d.ts +106 -0
- package/resources/live/ws-base.d.ts.map +1 -0
- package/resources/live/ws-base.js +476 -0
- package/resources/live/ws-base.js.map +1 -0
- package/resources/live/ws-base.mjs +472 -0
- package/resources/live/ws-base.mjs.map +1 -0
- package/resources/live/ws.d.mts +13 -0
- package/resources/live/ws.d.mts.map +1 -0
- package/resources/live/ws.d.ts +13 -0
- package/resources/live/ws.d.ts.map +1 -0
- package/resources/live/ws.js +34 -0
- package/resources/live/ws.js.map +1 -0
- package/resources/live/ws.mjs +29 -0
- package/resources/live/ws.mjs.map +1 -0
- package/resources/live.d.mts +2 -0
- package/resources/live.d.mts.map +1 -0
- package/resources/live.d.ts +2 -0
- package/resources/live.d.ts.map +1 -0
- package/resources/live.js +6 -0
- package/resources/live.js.map +1 -0
- package/resources/live.mjs +3 -0
- package/resources/live.mjs.map +1 -0
- package/resources/responses/responses.d.mts +48 -0
- package/resources/responses/responses.d.mts.map +1 -1
- package/resources/responses/responses.d.ts +48 -0
- package/resources/responses/responses.d.ts.map +1 -1
- package/resources/responses/responses.js.map +1 -1
- package/resources/responses/responses.mjs.map +1 -1
- package/resources/webhooks/webhooks.d.mts +72 -5
- package/resources/webhooks/webhooks.d.mts.map +1 -1
- package/resources/webhooks/webhooks.d.ts +72 -5
- package/resources/webhooks/webhooks.d.ts.map +1 -1
- package/src/client.ts +5 -0
- package/src/helpers/live.ts +8 -0
- package/src/lib/live/transcript-grouper.ts +303 -0
- package/src/lib/live/transcript-grouping.ts +362 -0
- package/src/lib/live/webrtc.ts +23 -0
- package/src/lib/realtime/webrtc.ts +20 -0
- package/src/lib/webrtc/connection.ts +309 -0
- package/src/lib/webrtc/data-channel.ts +144 -0
- package/src/lib/webrtc/errors.ts +38 -0
- package/src/lib/webrtc/subscriptions.ts +62 -0
- package/src/lib/webrtc/types.ts +109 -0
- package/src/live/webrtc.ts +10 -0
- package/src/realtime/webrtc.ts +10 -0
- package/src/resources/beta/responses/responses.ts +60 -0
- package/src/resources/conversations/items.ts +20 -0
- package/src/resources/index.ts +1 -0
- package/src/resources/live/api.md +87 -0
- package/src/resources/live/forks/forks.ts +169 -0
- package/src/resources/live/forks/index.ts +5 -0
- package/src/resources/live/forks/internal-base.ts +107 -0
- package/src/resources/live/forks/ws-base.ts +611 -0
- package/src/resources/live/forks/ws.ts +46 -0
- package/src/resources/live/forks.ts +3 -0
- package/src/resources/live/index.ts +20 -0
- package/src/resources/live/internal-base.ts +104 -0
- package/src/resources/live/live.ts +2296 -0
- package/src/resources/live/sessions.ts +337 -0
- package/src/resources/live/sideband/index.ts +10 -0
- package/src/resources/live/sideband/internal-base.ts +107 -0
- package/src/resources/live/sideband/sideband.ts +58 -0
- package/src/resources/live/sideband/ws-base.ts +621 -0
- package/src/resources/live/sideband/ws.ts +46 -0
- package/src/resources/live/sideband.ts +3 -0
- package/src/resources/live/ws-base.ts +607 -0
- package/src/resources/live/ws.ts +42 -0
- package/src/resources/live.ts +3 -0
- package/src/resources/responses/responses.ts +60 -0
- package/src/resources/webhooks/api.md +1 -0
- package/src/resources/webhooks/webhooks.ts +82 -3
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { EventEmitter } from '../../core/EventEmitter';
|
|
2
|
+
import { OpenAIError } from '../../core/error';
|
|
3
|
+
import type { ServerEvent } from '../../resources/live/live';
|
|
4
|
+
import { TranscriptGrouping } from './transcript-grouping';
|
|
5
|
+
import type { GroupingUpdate, TranscriptFragment } from './transcript-grouping';
|
|
6
|
+
|
|
7
|
+
/** Options for the speaker-based transcript grouping policy. */
|
|
8
|
+
export interface TranscriptGrouperOptions {
|
|
9
|
+
/** Minimum source-time gap before promoting buffered assistant text. Default: 500 ms. */
|
|
10
|
+
minTurnSeparationMs?: number;
|
|
11
|
+
/** Assistant transcript inactivity before closing a segment. Default: 2000 ms; not an audio/VAD signal. */
|
|
12
|
+
assistantSilenceMs?: number;
|
|
13
|
+
/** Acknowledgments shorter than this may be suppressed as backchannels. Default: 1000 ms; zero disables suppression. */
|
|
14
|
+
backchannelMaxDurationMs?: number;
|
|
15
|
+
/** Isolation window used to distinguish backchannels from assistant replies. Default: 2000 ms. */
|
|
16
|
+
backchannelIsolationMs?: number;
|
|
17
|
+
/** Extra phrases eligible for backchannel suppression, in addition to the built-in acknowledgments.
|
|
18
|
+
* Normalized like transcript text (case, hyphens, whitespace, and surrounding punctuation).
|
|
19
|
+
* Copied when the grouper is created; the same timing thresholds apply.
|
|
20
|
+
*/
|
|
21
|
+
additionalAcknowledgments?: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** An immutable SDK projection of spoken text, not a server conversation item. */
|
|
25
|
+
export interface TranscriptSegment {
|
|
26
|
+
/** Stable ID local to this SDK projection; not a server turn or item ID. */
|
|
27
|
+
readonly id: string;
|
|
28
|
+
/** ID of the preceding emitted segment, or null for the first segment. */
|
|
29
|
+
readonly previousId: string | null;
|
|
30
|
+
/** Speaker whose transcript text was grouped. */
|
|
31
|
+
readonly speaker: 'user' | 'assistant';
|
|
32
|
+
/** Complete append-only text; replace the displayed text on each update. */
|
|
33
|
+
readonly text: string;
|
|
34
|
+
/** Start of the first contributing public transcript interval, in session milliseconds. */
|
|
35
|
+
readonly startMs: number;
|
|
36
|
+
/** Latest end of the contributing public transcript intervals; never a playback completion time. */
|
|
37
|
+
readonly endMs: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Why a display segment was finalized; none of these proves audible speech completed. */
|
|
41
|
+
export type TranscriptSegmentCloseReason =
|
|
42
|
+
| 'speaker_change'
|
|
43
|
+
| 'inactivity'
|
|
44
|
+
| 'timestamp_reset'
|
|
45
|
+
| 'session_closed'
|
|
46
|
+
| 'manual';
|
|
47
|
+
|
|
48
|
+
/** A final segment snapshot and the reason it will receive no further updates. */
|
|
49
|
+
export interface TranscriptSegmentClosedEvent {
|
|
50
|
+
/** The final immutable segment, including all previously emitted text. */
|
|
51
|
+
readonly segment: TranscriptSegment;
|
|
52
|
+
/** The local grouping decision that finalized the segment. */
|
|
53
|
+
readonly reason: TranscriptSegmentCloseReason;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Events emitted by a TranscriptGrouper. */
|
|
57
|
+
// oxlint-disable-next-line typescript/consistent-type-definitions -- A closed event-map type satisfies the SDK emitter's Record constraint without permitting arbitrary event names.
|
|
58
|
+
export type TranscriptGrouperEvents = {
|
|
59
|
+
/** First and subsequent complete snapshots of an emitted segment. */
|
|
60
|
+
'segment.updated': (segment: TranscriptSegment) => void;
|
|
61
|
+
/** Exactly one final snapshot per emitted segment; closed segments are never reopened. */
|
|
62
|
+
'segment.closed': (event: TranscriptSegmentClosedEvent) => void;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type ReceivedFragment = TranscriptFragment & { receivedAt: number };
|
|
66
|
+
const SETTLE_MS = 50;
|
|
67
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
68
|
+
let nextGrouperId = 0;
|
|
69
|
+
|
|
70
|
+
function normalizeTranscript(
|
|
71
|
+
event: Extract<ServerEvent, { type: 'session.input_transcript.delta' | 'session.output_transcript.delta' }>,
|
|
72
|
+
): ReceivedFragment & { id: string } {
|
|
73
|
+
const { event_id: id, delta: text, start_ms: startMs, end_ms: endMs } = event;
|
|
74
|
+
const speaker = event.type === 'session.input_transcript.delta' ? 'user' : 'assistant';
|
|
75
|
+
if (
|
|
76
|
+
typeof id !== 'string' ||
|
|
77
|
+
!id ||
|
|
78
|
+
typeof text !== 'string' ||
|
|
79
|
+
!Number.isSafeInteger(startMs) ||
|
|
80
|
+
startMs < 0 ||
|
|
81
|
+
!Number.isSafeInteger(endMs) ||
|
|
82
|
+
endMs < startMs
|
|
83
|
+
) {
|
|
84
|
+
throw new OpenAIError(
|
|
85
|
+
'Invalid Live transcript delta: expected an event ID, text, and a nonnegative timed interval',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return { id, speaker, text, startMs, endMs, receivedAt: performance.now() };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function optionsWithDefaults(options: TranscriptGrouperOptions): Required<TranscriptGrouperOptions> {
|
|
92
|
+
const resolved = {
|
|
93
|
+
minTurnSeparationMs: options.minTurnSeparationMs ?? 500,
|
|
94
|
+
assistantSilenceMs: options.assistantSilenceMs ?? 2000,
|
|
95
|
+
backchannelMaxDurationMs: options.backchannelMaxDurationMs ?? 1000,
|
|
96
|
+
backchannelIsolationMs: options.backchannelIsolationMs ?? 2000,
|
|
97
|
+
};
|
|
98
|
+
for (const [name, value] of Object.entries(resolved)) {
|
|
99
|
+
if (!Number.isFinite(value) || value < 0 || value > MAX_TIMEOUT_MS) {
|
|
100
|
+
throw new OpenAIError(`${name} must be a finite number between 0 and ${MAX_TIMEOUT_MS}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { ...resolved, additionalAcknowledgments: options.additionalAcknowledgments ?? [] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Groups public Live transcript events using speaker and
|
|
108
|
+
* backchannel heuristics. Raw events remain available on your transport.
|
|
109
|
+
*
|
|
110
|
+
* Only session.input_transcript.delta, session.output_transcript.delta and session.closed are
|
|
111
|
+
* consumed. No audio, engine frames, server turn events or internal end markers
|
|
112
|
+
* are required. Ambiguous speaker changes settle for at most 50 ms. When source
|
|
113
|
+
* time stops arriving, a monotonic local clock supplies a best-effort inactivity
|
|
114
|
+
* fallback. Delayed delivery can therefore change grouping; this is not VAD,
|
|
115
|
+
* playback tracking, or a lossless transcript (some backchannels are suppressed).
|
|
116
|
+
*
|
|
117
|
+
* Create one instance per session. Call close() on transport loss or teardown;
|
|
118
|
+
* the grouper never owns or closes your transport and does not reconnect it.
|
|
119
|
+
*/
|
|
120
|
+
// oxlint-disable-next-line unicorn/prefer-event-target -- This is the SDK's browser-compatible typed emitter, not node:events.
|
|
121
|
+
export class TranscriptGrouper extends EventEmitter<TranscriptGrouperEvents> {
|
|
122
|
+
private readonly grouping: TranscriptGrouping;
|
|
123
|
+
private readonly seenIds = new Set<string>();
|
|
124
|
+
private pending: ReceivedFragment[] = [];
|
|
125
|
+
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
126
|
+
private anchor: { sourceMs: number; receivedAt: number } | undefined;
|
|
127
|
+
private lastStartMs: number | undefined;
|
|
128
|
+
private closed = false;
|
|
129
|
+
private dispatching = false;
|
|
130
|
+
private readonly updates: GroupingUpdate[] = [];
|
|
131
|
+
|
|
132
|
+
/** Create a grouper with speaker-based defaults. Invalid timing options throw OpenAIError. */
|
|
133
|
+
constructor(options: TranscriptGrouperOptions = {}) {
|
|
134
|
+
super();
|
|
135
|
+
this.grouping = new TranscriptGrouping(optionsWithDefaults(options), `segment_${nextGrouperId}`);
|
|
136
|
+
nextGrouperId += 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Consume one public Live event. Duplicate transcript event IDs and unrelated
|
|
141
|
+
* event types are ignored. Empty text does not count as speech activity.
|
|
142
|
+
* Malformed transcript fields or use after close() throw OpenAIError.
|
|
143
|
+
*/
|
|
144
|
+
push(event: ServerEvent): void {
|
|
145
|
+
if (this.closed) {
|
|
146
|
+
throw new OpenAIError('Cannot push events after closing the transcript grouper');
|
|
147
|
+
}
|
|
148
|
+
if (event.type === 'session.closed') {
|
|
149
|
+
this.finish('session_closed');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (event.type !== 'session.input_transcript.delta' && event.type !== 'session.output_transcript.delta') {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const fragment = normalizeTranscript(event);
|
|
156
|
+
const { id, startMs, endMs, speaker } = fragment;
|
|
157
|
+
if (this.seenIds.has(id)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.seenIds.add(id);
|
|
161
|
+
if (!fragment.text) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const updates: GroupingUpdate[] = [];
|
|
165
|
+
const [pending] = this.pending;
|
|
166
|
+
if (pending && pending.startMs === startMs && pending.endMs === endMs) {
|
|
167
|
+
this.pending.push(fragment);
|
|
168
|
+
if (this.pending.some((part) => part.speaker !== speaker)) {
|
|
169
|
+
updates.push(...this.flushPending());
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
updates.push(...this.flushPending());
|
|
173
|
+
if (this.grouping.speaker === speaker) {
|
|
174
|
+
updates.push(...this.commit([fragment]));
|
|
175
|
+
} else {
|
|
176
|
+
this.pending.push(fragment);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
this.schedule();
|
|
180
|
+
this.dispatch(updates);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Flush buffered text according to the grouping policy, finalize all emitted
|
|
185
|
+
* segments, and cancel timers. Idempotent; further push() calls fail. Does not
|
|
186
|
+
* close the transport. Invoke before discarding the grouper at session teardown.
|
|
187
|
+
*/
|
|
188
|
+
close(): void {
|
|
189
|
+
this.finish('manual');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private finish(reason: 'manual' | 'session_closed'): void {
|
|
193
|
+
if (this.closed) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.closed = true;
|
|
197
|
+
this.clearTimer();
|
|
198
|
+
const updates = this.flushPending();
|
|
199
|
+
updates.push(...this.grouping.close(this.sourceNow(), reason));
|
|
200
|
+
this.seenIds.clear();
|
|
201
|
+
this.dispatch(updates);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private commit(fragments: readonly ReceivedFragment[]): GroupingUpdate[] {
|
|
205
|
+
const [first] = fragments;
|
|
206
|
+
if (!first) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
const updates: GroupingUpdate[] = [];
|
|
210
|
+
if (this.lastStartMs !== undefined && first.startMs < this.lastStartMs) {
|
|
211
|
+
updates.push(...this.grouping.close(this.sourceNow(), 'timestamp_reset'));
|
|
212
|
+
this.anchor = undefined;
|
|
213
|
+
}
|
|
214
|
+
this.lastStartMs = first.startMs;
|
|
215
|
+
// Overlapping intervals can end before earlier ones. Preserve the source
|
|
216
|
+
// high-water mark while restarting local inactivity on newly received text.
|
|
217
|
+
this.anchor = {
|
|
218
|
+
sourceMs: Math.max(this.anchor?.sourceMs ?? 0, first.endMs),
|
|
219
|
+
receivedAt: first.receivedAt,
|
|
220
|
+
};
|
|
221
|
+
for (const part of fragments) {
|
|
222
|
+
this.anchor.sourceMs = Math.max(this.anchor.sourceMs, part.endMs);
|
|
223
|
+
this.anchor.receivedAt = Math.max(this.anchor.receivedAt, part.receivedAt);
|
|
224
|
+
}
|
|
225
|
+
updates.push(...this.grouping.process(fragments));
|
|
226
|
+
return updates;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private flushPending(): GroupingUpdate[] {
|
|
230
|
+
const { pending } = this;
|
|
231
|
+
this.pending = [];
|
|
232
|
+
return this.commit(pending);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private sourceNow(): number {
|
|
236
|
+
return this.anchor ? this.anchor.sourceMs + Math.max(0, performance.now() - this.anchor.receivedAt) : 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private clearTimer(): void {
|
|
240
|
+
if (this.timer !== undefined) {
|
|
241
|
+
clearTimeout(this.timer);
|
|
242
|
+
}
|
|
243
|
+
this.timer = undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private schedule(): void {
|
|
247
|
+
this.clearTimer();
|
|
248
|
+
if (this.closed) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const [pending] = this.pending;
|
|
252
|
+
const deadline = this.grouping.deadline();
|
|
253
|
+
// Settle newly arrived text before applying an older inactivity deadline.
|
|
254
|
+
let delay: number | undefined;
|
|
255
|
+
if (pending) {
|
|
256
|
+
delay = pending.receivedAt + SETTLE_MS - performance.now();
|
|
257
|
+
} else if (deadline !== undefined) {
|
|
258
|
+
delay = deadline - this.sourceNow();
|
|
259
|
+
}
|
|
260
|
+
if (delay === undefined) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
this.timer = setTimeout(
|
|
264
|
+
() => {
|
|
265
|
+
this.timer = undefined;
|
|
266
|
+
const updates = this.flushPending();
|
|
267
|
+
updates.push(...this.grouping.advance(this.sourceNow()));
|
|
268
|
+
this.schedule();
|
|
269
|
+
this.dispatch(updates);
|
|
270
|
+
},
|
|
271
|
+
Math.min(MAX_TIMEOUT_MS, Math.max(0, Math.ceil(delay))),
|
|
272
|
+
);
|
|
273
|
+
const timer: unknown = this.timer;
|
|
274
|
+
if (
|
|
275
|
+
typeof timer === 'object' &&
|
|
276
|
+
timer !== null &&
|
|
277
|
+
'unref' in timer &&
|
|
278
|
+
typeof timer.unref === 'function'
|
|
279
|
+
) {
|
|
280
|
+
timer.unref();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private dispatch(updates: GroupingUpdate[]): void {
|
|
285
|
+
this.updates.push(...updates);
|
|
286
|
+
if (this.dispatching) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.dispatching = true;
|
|
290
|
+
try {
|
|
291
|
+
while (this.updates.length) {
|
|
292
|
+
const update = this.updates.shift();
|
|
293
|
+
if (update?.type === 'updated') {
|
|
294
|
+
this._emit('segment.updated', update.segment);
|
|
295
|
+
} else if (update) {
|
|
296
|
+
this._emit('segment.closed', { segment: update.segment, reason: update.reason });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} finally {
|
|
300
|
+
this.dispatching = false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TranscriptGrouperOptions,
|
|
3
|
+
TranscriptSegment,
|
|
4
|
+
TranscriptSegmentCloseReason,
|
|
5
|
+
} from './transcript-grouper';
|
|
6
|
+
|
|
7
|
+
export interface TranscriptFragment {
|
|
8
|
+
speaker: 'user' | 'assistant';
|
|
9
|
+
text: string;
|
|
10
|
+
startMs: number;
|
|
11
|
+
endMs: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type GroupingUpdate =
|
|
15
|
+
| { type: 'updated'; segment: TranscriptSegment }
|
|
16
|
+
| { type: 'closed'; segment: TranscriptSegment; reason: TranscriptSegmentCloseReason };
|
|
17
|
+
|
|
18
|
+
type Turn = TranscriptFragment & {
|
|
19
|
+
id: string;
|
|
20
|
+
previousId: string | null;
|
|
21
|
+
emitted: boolean;
|
|
22
|
+
canDropAsBackchannel: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ACKNOWLEDGMENTS = [
|
|
26
|
+
'aha',
|
|
27
|
+
'alright',
|
|
28
|
+
'gotcha',
|
|
29
|
+
'hm',
|
|
30
|
+
'hmm',
|
|
31
|
+
'mhm',
|
|
32
|
+
'mm',
|
|
33
|
+
'mm hmm',
|
|
34
|
+
'okay',
|
|
35
|
+
'ok',
|
|
36
|
+
'right',
|
|
37
|
+
'sure',
|
|
38
|
+
'uh huh',
|
|
39
|
+
'yeah',
|
|
40
|
+
'yep',
|
|
41
|
+
'yes',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function normalizeAcknowledgment(text: string): string {
|
|
45
|
+
return text
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.split('-')
|
|
48
|
+
.join(' ')
|
|
49
|
+
.replace(/^[\s.,!?;:"'()[\]{}]+/u, '')
|
|
50
|
+
.replace(/[\s.,!?;:"'()[\]{}]+$/u, '')
|
|
51
|
+
.split(/\s+/u)
|
|
52
|
+
.join(' ');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The v2 grouping policy, driven by timed public text rather than engine frames. */
|
|
56
|
+
export class TranscriptGrouping {
|
|
57
|
+
private current: Turn | undefined;
|
|
58
|
+
private buffered: Turn | undefined;
|
|
59
|
+
private lastId: string | null = null;
|
|
60
|
+
private nextId = 0;
|
|
61
|
+
private lastAssistantEnd: number | undefined;
|
|
62
|
+
|
|
63
|
+
private readonly options: Required<TranscriptGrouperOptions>;
|
|
64
|
+
private readonly idPrefix: string;
|
|
65
|
+
private readonly acknowledgments: readonly string[];
|
|
66
|
+
|
|
67
|
+
constructor(options: Required<TranscriptGrouperOptions>, idPrefix: string) {
|
|
68
|
+
this.options = options;
|
|
69
|
+
this.idPrefix = idPrefix;
|
|
70
|
+
this.acknowledgments = [
|
|
71
|
+
...ACKNOWLEDGMENTS,
|
|
72
|
+
...options.additionalAcknowledgments.map(normalizeAcknowledgment).filter(Boolean),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get speaker(): TranscriptFragment['speaker'] | undefined {
|
|
77
|
+
return this.current?.speaker;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
process(fragments: readonly TranscriptFragment[]): GroupingUpdate[] {
|
|
81
|
+
const events: GroupingUpdate[] = [];
|
|
82
|
+
const preferred = this.current?.speaker ?? 'user';
|
|
83
|
+
const ordered = [
|
|
84
|
+
...fragments.filter((fragment) => fragment.speaker === preferred),
|
|
85
|
+
...fragments.filter((fragment) => fragment.speaker !== preferred),
|
|
86
|
+
];
|
|
87
|
+
const [first] = ordered;
|
|
88
|
+
if (!first) {
|
|
89
|
+
return events;
|
|
90
|
+
}
|
|
91
|
+
// A later public interval supplies elapsed source time even when no silence
|
|
92
|
+
// events were sent. Resolve decisions strictly before the incoming interval.
|
|
93
|
+
let deadline = this.deadline();
|
|
94
|
+
while (deadline !== undefined && deadline < first.startMs) {
|
|
95
|
+
events.push(...this.advance(deadline));
|
|
96
|
+
deadline = this.deadline();
|
|
97
|
+
}
|
|
98
|
+
if (this.current && !ordered.some((fragment) => fragment.speaker === this.current?.speaker)) {
|
|
99
|
+
events.push(
|
|
100
|
+
...this.advance(
|
|
101
|
+
first.startMs,
|
|
102
|
+
ordered.some((fragment) => fragment.speaker === 'user'),
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
for (const fragment of ordered) {
|
|
107
|
+
events.push(...this.ingest(fragment));
|
|
108
|
+
}
|
|
109
|
+
return events;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
advance(timeMs: number, hasIncomingUser = false): GroupingUpdate[] {
|
|
113
|
+
if (
|
|
114
|
+
this.current &&
|
|
115
|
+
this.buffered &&
|
|
116
|
+
timeMs - this.current.endMs >= this.options.minTurnSeparationMs &&
|
|
117
|
+
!this.keepBackchannel(timeMs)
|
|
118
|
+
) {
|
|
119
|
+
this.buffered = this.maybeDropBackchannel(timeMs);
|
|
120
|
+
if (this.buffered) {
|
|
121
|
+
return this.promote();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (
|
|
125
|
+
this.current?.speaker === 'assistant' &&
|
|
126
|
+
!hasIncomingUser &&
|
|
127
|
+
timeMs - this.current.endMs >= this.options.assistantSilenceMs
|
|
128
|
+
) {
|
|
129
|
+
return this.finishCurrent('inactivity');
|
|
130
|
+
}
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
deadline(): number | undefined {
|
|
135
|
+
if (!this.current) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
if (this.buffered) {
|
|
139
|
+
const separation = this.current.endMs + this.options.minTurnSeparationMs;
|
|
140
|
+
if (
|
|
141
|
+
this.mightBeBackchannel() &&
|
|
142
|
+
this.buffered.canDropAsBackchannel &&
|
|
143
|
+
!this.userContinued() &&
|
|
144
|
+
!this.recentAssistant()
|
|
145
|
+
) {
|
|
146
|
+
return Math.max(separation, this.buffered.endMs + this.options.backchannelIsolationMs);
|
|
147
|
+
}
|
|
148
|
+
return separation;
|
|
149
|
+
}
|
|
150
|
+
return this.current.speaker === 'assistant'
|
|
151
|
+
? this.current.endMs + this.options.assistantSilenceMs
|
|
152
|
+
: undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
close(timeMs: number, reason: TranscriptSegmentCloseReason): GroupingUpdate[] {
|
|
156
|
+
const buffered = this.maybeDropBackchannel(timeMs);
|
|
157
|
+
const events = this.finishCurrent(reason);
|
|
158
|
+
this.buffered = undefined;
|
|
159
|
+
if (buffered?.text) {
|
|
160
|
+
events.push(...this.emit(buffered), ...this.finish(buffered, reason));
|
|
161
|
+
}
|
|
162
|
+
if (reason === 'timestamp_reset') {
|
|
163
|
+
this.lastAssistantEnd = undefined;
|
|
164
|
+
}
|
|
165
|
+
return events;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private ingest(fragment: TranscriptFragment): GroupingUpdate[] {
|
|
169
|
+
if (!this.current) {
|
|
170
|
+
this.current = this.newTurn(fragment);
|
|
171
|
+
return this.emit(this.current);
|
|
172
|
+
}
|
|
173
|
+
if (fragment.speaker === this.current.speaker) {
|
|
174
|
+
TranscriptGrouping.append(this.current, fragment, this.buffered !== undefined);
|
|
175
|
+
return this.emit(this.current);
|
|
176
|
+
}
|
|
177
|
+
if (this.current.speaker === 'user' && this.userContinued()) {
|
|
178
|
+
this.buffered = undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const separation = fragment.startMs - this.current.endMs;
|
|
182
|
+
if (this.current.speaker === 'assistant') {
|
|
183
|
+
this.buffer(fragment);
|
|
184
|
+
return this.promote();
|
|
185
|
+
}
|
|
186
|
+
if (separation < this.options.minTurnSeparationMs) {
|
|
187
|
+
this.buffer(fragment, this.possibleAcknowledgment(fragment));
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
if (this.buffered && this.standaloneAcknowledgment(fragment, this.buffered)) {
|
|
191
|
+
this.buffer(fragment, true);
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
if (!this.buffered && this.standaloneAcknowledgment(fragment)) {
|
|
195
|
+
this.buffer(fragment, false);
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
this.buffered = this.maybeDropBackchannel(undefined, fragment);
|
|
199
|
+
const events = this.finishCurrent('speaker_change');
|
|
200
|
+
if (this.buffered) {
|
|
201
|
+
this.current = this.buffered;
|
|
202
|
+
this.buffered = undefined;
|
|
203
|
+
TranscriptGrouping.append(this.current, fragment);
|
|
204
|
+
} else {
|
|
205
|
+
this.current = this.newTurn(fragment);
|
|
206
|
+
}
|
|
207
|
+
events.push(...this.emit(this.current));
|
|
208
|
+
return events;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private newTurn(fragment: TranscriptFragment): Turn {
|
|
212
|
+
const turn = {
|
|
213
|
+
...fragment,
|
|
214
|
+
id: `${this.idPrefix}_${this.nextId}`,
|
|
215
|
+
previousId: null,
|
|
216
|
+
emitted: false,
|
|
217
|
+
canDropAsBackchannel: true,
|
|
218
|
+
};
|
|
219
|
+
this.nextId += 1;
|
|
220
|
+
return turn;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private static append(turn: Turn, fragment: TranscriptFragment, separate = false): void {
|
|
224
|
+
const separator =
|
|
225
|
+
separate && /[\p{L}\p{N}]$/u.test(turn.text) && /^[\p{L}\p{N}]/u.test(fragment.text) ? ' ' : '';
|
|
226
|
+
turn.text += separator + fragment.text;
|
|
227
|
+
turn.endMs = Math.max(turn.endMs, fragment.endMs);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private buffer(fragment: TranscriptFragment, canDrop?: boolean): void {
|
|
231
|
+
if (this.buffered) {
|
|
232
|
+
TranscriptGrouping.append(this.buffered, fragment);
|
|
233
|
+
} else {
|
|
234
|
+
this.buffered = this.newTurn(fragment);
|
|
235
|
+
}
|
|
236
|
+
if (canDrop !== undefined) {
|
|
237
|
+
this.buffered.canDropAsBackchannel = canDrop;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private promote(): GroupingUpdate[] {
|
|
242
|
+
if (!this.buffered) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
const events = this.finishCurrent('speaker_change');
|
|
246
|
+
this.current = this.buffered;
|
|
247
|
+
this.buffered = undefined;
|
|
248
|
+
events.push(...this.emit(this.current));
|
|
249
|
+
return events;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private finishCurrent(reason: TranscriptSegmentCloseReason): GroupingUpdate[] {
|
|
253
|
+
const { current } = this;
|
|
254
|
+
this.current = undefined;
|
|
255
|
+
return current ? this.finish(current, reason) : [];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private finish(turn: Turn, reason: TranscriptSegmentCloseReason): GroupingUpdate[] {
|
|
259
|
+
if (turn.speaker === 'assistant') {
|
|
260
|
+
this.lastAssistantEnd = turn.endMs;
|
|
261
|
+
}
|
|
262
|
+
return turn.emitted ? [{ type: 'closed', segment: TranscriptGrouping.snapshot(turn), reason }] : [];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private emit(turn: Turn): GroupingUpdate[] {
|
|
266
|
+
if (!turn.text) {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
if (!turn.emitted) {
|
|
270
|
+
turn.previousId = this.lastId;
|
|
271
|
+
this.lastId = turn.id;
|
|
272
|
+
turn.emitted = true;
|
|
273
|
+
}
|
|
274
|
+
return [{ type: 'updated', segment: TranscriptGrouping.snapshot(turn) }];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private static snapshot(turn: Turn): TranscriptSegment {
|
|
278
|
+
return Object.freeze({
|
|
279
|
+
id: turn.id,
|
|
280
|
+
previousId: turn.previousId,
|
|
281
|
+
speaker: turn.speaker,
|
|
282
|
+
text: turn.text,
|
|
283
|
+
startMs: turn.startMs,
|
|
284
|
+
endMs: turn.endMs,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private mightBeBackchannel(): boolean {
|
|
289
|
+
return (
|
|
290
|
+
this.current?.speaker === 'user' &&
|
|
291
|
+
this.buffered?.speaker === 'assistant' &&
|
|
292
|
+
this.buffered.endMs - this.buffered.startMs < this.options.backchannelMaxDurationMs
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private userContinued(): boolean {
|
|
297
|
+
return (
|
|
298
|
+
this.mightBeBackchannel() &&
|
|
299
|
+
this.buffered?.canDropAsBackchannel === true &&
|
|
300
|
+
this.current !== undefined &&
|
|
301
|
+
this.current.endMs > this.buffered.endMs
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private recentAssistant(): boolean {
|
|
306
|
+
return (
|
|
307
|
+
this.current !== undefined &&
|
|
308
|
+
this.buffered !== undefined &&
|
|
309
|
+
this.lastAssistantEnd !== undefined &&
|
|
310
|
+
this.buffered.startMs - this.lastAssistantEnd < this.options.backchannelIsolationMs &&
|
|
311
|
+
this.buffered.startMs <= this.current.startMs
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private keepBackchannel(timeMs: number): boolean {
|
|
316
|
+
return (
|
|
317
|
+
this.mightBeBackchannel() &&
|
|
318
|
+
this.buffered?.canDropAsBackchannel === true &&
|
|
319
|
+
!this.userContinued() &&
|
|
320
|
+
!this.recentAssistant() &&
|
|
321
|
+
timeMs - this.buffered.endMs < this.options.backchannelIsolationMs
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private maybeDropBackchannel(timeMs?: number, next?: TranscriptFragment): Turn | undefined {
|
|
326
|
+
if (!this.mightBeBackchannel() || !this.buffered) {
|
|
327
|
+
return this.buffered;
|
|
328
|
+
}
|
|
329
|
+
if (this.userContinued()) {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
if (this.recentAssistant()) {
|
|
333
|
+
return this.buffered;
|
|
334
|
+
}
|
|
335
|
+
if (next && next.startMs - this.buffered.endMs < this.options.backchannelIsolationMs) {
|
|
336
|
+
return this.buffered;
|
|
337
|
+
}
|
|
338
|
+
if (
|
|
339
|
+
!next &&
|
|
340
|
+
(timeMs === undefined || timeMs - this.buffered.endMs < this.options.backchannelIsolationMs)
|
|
341
|
+
) {
|
|
342
|
+
return this.buffered;
|
|
343
|
+
}
|
|
344
|
+
return this.buffered.canDropAsBackchannel ? undefined : this.buffered;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private standaloneAcknowledgment(fragment: TranscriptFragment, previous?: Turn): boolean {
|
|
348
|
+
return (
|
|
349
|
+
fragment.endMs - (previous?.startMs ?? fragment.startMs) < this.options.backchannelMaxDurationMs &&
|
|
350
|
+
this.acknowledgments.includes(normalizeAcknowledgment((previous?.text ?? '') + fragment.text))
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private possibleAcknowledgment(fragment: TranscriptFragment): boolean {
|
|
355
|
+
const text = normalizeAcknowledgment((this.buffered?.text ?? '') + fragment.text);
|
|
356
|
+
return (
|
|
357
|
+
fragment.endMs - (this.buffered?.startMs ?? fragment.startMs) < this.options.backchannelMaxDurationMs &&
|
|
358
|
+
text.length > 0 &&
|
|
359
|
+
this.acknowledgments.some((acknowledgment) => acknowledgment.startsWith(text))
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* oxlint-disable max-classes-per-file -- Both thin facades belong to the same public Live WebRTC entrypoint. */
|
|
2
|
+
import type {
|
|
3
|
+
ConnectClientEvent as ClientEvent,
|
|
4
|
+
ConnectServerEvent as ServerEvent,
|
|
5
|
+
} from '../../resources/live/sideband/sideband';
|
|
6
|
+
import { WebRTCConnection } from '../webrtc/connection';
|
|
7
|
+
import { DataChannel } from '../webrtc/data-channel';
|
|
8
|
+
|
|
9
|
+
export { WebRTCError } from '../webrtc/errors';
|
|
10
|
+
export type { WebRTCErrorCode, WebRTCErrorPhase } from '../webrtc/errors';
|
|
11
|
+
export type {
|
|
12
|
+
ExchangeSdp,
|
|
13
|
+
WebRTCConnectOptions,
|
|
14
|
+
WebRTCConnectionEvent,
|
|
15
|
+
WebRTCConfiguration,
|
|
16
|
+
WebRTCState,
|
|
17
|
+
} from '../webrtc/types';
|
|
18
|
+
|
|
19
|
+
/** Browser Live connection with application-owned SDP exchange and typed server-event subscriptions. */
|
|
20
|
+
export class OpenAILiveWebRTC extends WebRTCConnection<ClientEvent, ServerEvent> {}
|
|
21
|
+
|
|
22
|
+
/** Typed Live events on a borrowed data channel; dispose() never closes application resources. */
|
|
23
|
+
export class LiveDataChannel extends DataChannel<ClientEvent, ServerEvent> {}
|