@estuary-ai/sdk 0.4.1 → 0.5.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/README.md +35 -0
- package/dist/index.d.mts +56 -1
- package/dist/index.d.ts +56 -1
- package/dist/index.js +283 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +283 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -46,6 +46,37 @@ client.sendText('What do you remember about me?');
|
|
|
46
46
|
client.sendText('Just respond in text', true); // textOnly mode
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
### Scripted Lines
|
|
50
|
+
|
|
51
|
+
Make the character speak exact, prewritten lines (straight to TTS, skipping the LLM):
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
client.sayLine('Welcome back, traveler.'); // TTS audio
|
|
55
|
+
client.sayLine('System: connection restored.', true); // text only, no audio
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For a sequence, use `playScript()` — it paces the lines so each finishes before the next is
|
|
59
|
+
sent. (`say_line` interrupts any in-progress speech, so firing several at once would make each
|
|
60
|
+
line cut off the previous one.)
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const script = client.playScript(
|
|
64
|
+
['Welcome to my shop, traveler!', 'I have wares, if you have coin.', 'Come back anytime.'],
|
|
65
|
+
{ textOnly: false, lineGapMs: 250 },
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
client.on('scriptLineStarted', ({ index, text }) => console.log(`line ${index}: ${text}`));
|
|
69
|
+
await script.done; // resolves when all lines have been spoken
|
|
70
|
+
|
|
71
|
+
script.pause();
|
|
72
|
+
script.resume();
|
|
73
|
+
script.next(); // skip ahead
|
|
74
|
+
script.stop(); // halt + interrupt
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`sayLines(lines, opts)` is a convenience alias of `playScript(lines, opts)`. Lines may be plain
|
|
78
|
+
strings or per-line overrides: `{ text: '(a silent stage direction)', textOnly: true }`.
|
|
79
|
+
|
|
49
80
|
### Voice (WebSocket)
|
|
50
81
|
|
|
51
82
|
```typescript
|
|
@@ -182,6 +213,10 @@ client.on('interrupt', (data) => { /* response interrupted */ });
|
|
|
182
213
|
client.on('characterAction', (action) => { /* parsed action from bot response */ });
|
|
183
214
|
client.on('cameraCaptureRequest', (request) => { /* server requests a camera image */ });
|
|
184
215
|
|
|
216
|
+
// Scripted lines (playScript / sayLines)
|
|
217
|
+
client.on('scriptLineStarted', ({ index, text, messageId }) => { /* a scripted line began */ });
|
|
218
|
+
client.on('scriptComplete', ({ reason }) => { /* 'finished' | 'stopped' | 'disconnected' | 'interrupted' */ });
|
|
219
|
+
|
|
185
220
|
// Voice
|
|
186
221
|
client.on('voiceStarted', () => { /* voice session began */ });
|
|
187
222
|
client.on('voiceStopped', () => { /* voice session ended */ });
|
package/dist/index.d.mts
CHANGED
|
@@ -156,6 +156,46 @@ interface CharacterAction {
|
|
|
156
156
|
/** Message ID of the bot response that contained this action */
|
|
157
157
|
messageId: string;
|
|
158
158
|
}
|
|
159
|
+
/** A scripted line: plain text (uses the script's default textOnly) or an explicit override. */
|
|
160
|
+
type ScriptLine = string | {
|
|
161
|
+
text: string;
|
|
162
|
+
textOnly?: boolean;
|
|
163
|
+
};
|
|
164
|
+
interface ScriptOptions {
|
|
165
|
+
/** Default for plain-string lines: false = TTS audio (default), true = text-only. */
|
|
166
|
+
textOnly?: boolean;
|
|
167
|
+
/** Pause inserted after each line completes, in ms (default 0). */
|
|
168
|
+
lineGapMs?: number;
|
|
169
|
+
/** Begin speaking immediately on creation (default true). If false, call play(). */
|
|
170
|
+
autoStart?: boolean;
|
|
171
|
+
/** Repeat from the first line after the last (default false). */
|
|
172
|
+
loop?: boolean;
|
|
173
|
+
/** Force-advance a line if no completion signal arrives within this many ms (default 30000). */
|
|
174
|
+
lineTimeoutMs?: number;
|
|
175
|
+
}
|
|
176
|
+
type ScriptEndReason = 'finished' | 'stopped' | 'disconnected' | 'interrupted';
|
|
177
|
+
type ScriptState = 'idle' | 'playing' | 'paused' | 'done';
|
|
178
|
+
interface ScriptLineStartedInfo {
|
|
179
|
+
index: number;
|
|
180
|
+
text: string;
|
|
181
|
+
messageId: string;
|
|
182
|
+
}
|
|
183
|
+
/** Handle returned by EstuaryClient.playScript() / sayLines(). */
|
|
184
|
+
interface ScriptController {
|
|
185
|
+
readonly length: number;
|
|
186
|
+
/** Index of the current / most-recently-started line (-1 before the first line starts). */
|
|
187
|
+
readonly index: number;
|
|
188
|
+
readonly state: ScriptState;
|
|
189
|
+
/** Resolves (never rejects) when the script ends, with the reason. Awaitable. */
|
|
190
|
+
readonly done: Promise<{
|
|
191
|
+
reason: ScriptEndReason;
|
|
192
|
+
}>;
|
|
193
|
+
play(): void;
|
|
194
|
+
pause(): void;
|
|
195
|
+
resume(): void;
|
|
196
|
+
next(): void;
|
|
197
|
+
stop(): void;
|
|
198
|
+
}
|
|
159
199
|
type EstuaryEventMap = {
|
|
160
200
|
connected: (session: SessionInfo) => void;
|
|
161
201
|
disconnected: (reason: string) => void;
|
|
@@ -179,6 +219,10 @@ type EstuaryEventMap = {
|
|
|
179
219
|
/** Bot audio level 0.0–1.0, emitted during playback for both transports. */
|
|
180
220
|
botAudioLevel: (level: number) => void;
|
|
181
221
|
memoryUpdated: (event: MemoryUpdatedEvent) => void;
|
|
222
|
+
scriptLineStarted: (info: ScriptLineStartedInfo) => void;
|
|
223
|
+
scriptComplete: (info: {
|
|
224
|
+
reason: ScriptEndReason;
|
|
225
|
+
}) => void;
|
|
182
226
|
};
|
|
183
227
|
interface VoiceManager {
|
|
184
228
|
start(): Promise<void>;
|
|
@@ -360,6 +404,7 @@ declare class EstuaryClient extends TypedEventEmitter<EstuaryEventMap> {
|
|
|
360
404
|
private _hasAutoInterrupted;
|
|
361
405
|
private _autoInterruptGraceTimer;
|
|
362
406
|
private _isLiveKitSpeaking;
|
|
407
|
+
private _activeScript;
|
|
363
408
|
constructor(config: EstuaryConfig);
|
|
364
409
|
/** Memory API client for querying memories, graphs, and facts */
|
|
365
410
|
get memory(): MemoryClient;
|
|
@@ -385,6 +430,16 @@ declare class EstuaryClient extends TypedEventEmitter<EstuaryEventMap> {
|
|
|
385
430
|
sendText(text: string, textOnly?: boolean): void;
|
|
386
431
|
/** Script the character to say a specific prewritten line. Defaults to TTS enabled (textOnly=false). */
|
|
387
432
|
sayLine(text: string, textOnly?: boolean): void;
|
|
433
|
+
/**
|
|
434
|
+
* Script a sequence of prewritten lines. Lines are paced so each finishes before the next
|
|
435
|
+
* is sent — required because say_line interrupts any in-progress response server-side, so
|
|
436
|
+
* unpaced lines would stomp each other. Returns a controller (play/pause/resume/next/stop +
|
|
437
|
+
* an awaitable `done`). Starting a new script stops any currently-active one.
|
|
438
|
+
*/
|
|
439
|
+
playScript(lines: ScriptLine[], opts?: ScriptOptions): ScriptController;
|
|
440
|
+
/** Convenience alias of playScript() for fire-and-forget scripted sequences. */
|
|
441
|
+
sayLines(lines: ScriptLine[], opts?: ScriptOptions): ScriptController;
|
|
442
|
+
private createScriptHost;
|
|
388
443
|
/** Interrupt the current bot response */
|
|
389
444
|
interrupt(messageId?: string): void;
|
|
390
445
|
/** Send a camera image for vision processing */
|
|
@@ -470,4 +525,4 @@ declare class CharacterClient {
|
|
|
470
525
|
dispose(): void;
|
|
471
526
|
}
|
|
472
527
|
|
|
473
|
-
export { type BotResponse, type BotVoice, type CameraCaptureRequest, type CharacterAction, CharacterClient, type CharacterInfo, ConnectionState, type CoreFact, type CoreFactsResponse, ErrorCode, EstuaryClient, type EstuaryConfig, EstuaryError, type EstuaryEventMap, type InterruptData, type LiveKitTokenResponse, MemoryClient, type MemoryData, type MemoryGraphEdge, type MemoryGraphNode, type MemoryGraphOptions, type MemoryGraphResponse, type MemoryListOptions, type MemoryListResponse, type MemorySearchOptions, type MemorySearchResponse, type MemoryStatsResponse, type MemoryTimelineOptions, type MemoryTimelineResponse, type MemoryUpdatedEvent, type ParsedAction, type QuotaExceededData, type SessionCapabilities, type SessionInfo, type ShareOpenResponse, type SttResponse, type VoiceManager, type VoiceTransport, parseActions };
|
|
528
|
+
export { type BotResponse, type BotVoice, type CameraCaptureRequest, type CharacterAction, CharacterClient, type CharacterInfo, ConnectionState, type CoreFact, type CoreFactsResponse, ErrorCode, EstuaryClient, type EstuaryConfig, EstuaryError, type EstuaryEventMap, type InterruptData, type LiveKitTokenResponse, MemoryClient, type MemoryData, type MemoryGraphEdge, type MemoryGraphNode, type MemoryGraphOptions, type MemoryGraphResponse, type MemoryListOptions, type MemoryListResponse, type MemorySearchOptions, type MemorySearchResponse, type MemoryStatsResponse, type MemoryTimelineOptions, type MemoryTimelineResponse, type MemoryUpdatedEvent, type ParsedAction, type QuotaExceededData, type ScriptController, type ScriptEndReason, type ScriptLine, type ScriptLineStartedInfo, type ScriptOptions, type ScriptState, type SessionCapabilities, type SessionInfo, type ShareOpenResponse, type SttResponse, type VoiceManager, type VoiceTransport, parseActions };
|
package/dist/index.d.ts
CHANGED
|
@@ -156,6 +156,46 @@ interface CharacterAction {
|
|
|
156
156
|
/** Message ID of the bot response that contained this action */
|
|
157
157
|
messageId: string;
|
|
158
158
|
}
|
|
159
|
+
/** A scripted line: plain text (uses the script's default textOnly) or an explicit override. */
|
|
160
|
+
type ScriptLine = string | {
|
|
161
|
+
text: string;
|
|
162
|
+
textOnly?: boolean;
|
|
163
|
+
};
|
|
164
|
+
interface ScriptOptions {
|
|
165
|
+
/** Default for plain-string lines: false = TTS audio (default), true = text-only. */
|
|
166
|
+
textOnly?: boolean;
|
|
167
|
+
/** Pause inserted after each line completes, in ms (default 0). */
|
|
168
|
+
lineGapMs?: number;
|
|
169
|
+
/** Begin speaking immediately on creation (default true). If false, call play(). */
|
|
170
|
+
autoStart?: boolean;
|
|
171
|
+
/** Repeat from the first line after the last (default false). */
|
|
172
|
+
loop?: boolean;
|
|
173
|
+
/** Force-advance a line if no completion signal arrives within this many ms (default 30000). */
|
|
174
|
+
lineTimeoutMs?: number;
|
|
175
|
+
}
|
|
176
|
+
type ScriptEndReason = 'finished' | 'stopped' | 'disconnected' | 'interrupted';
|
|
177
|
+
type ScriptState = 'idle' | 'playing' | 'paused' | 'done';
|
|
178
|
+
interface ScriptLineStartedInfo {
|
|
179
|
+
index: number;
|
|
180
|
+
text: string;
|
|
181
|
+
messageId: string;
|
|
182
|
+
}
|
|
183
|
+
/** Handle returned by EstuaryClient.playScript() / sayLines(). */
|
|
184
|
+
interface ScriptController {
|
|
185
|
+
readonly length: number;
|
|
186
|
+
/** Index of the current / most-recently-started line (-1 before the first line starts). */
|
|
187
|
+
readonly index: number;
|
|
188
|
+
readonly state: ScriptState;
|
|
189
|
+
/** Resolves (never rejects) when the script ends, with the reason. Awaitable. */
|
|
190
|
+
readonly done: Promise<{
|
|
191
|
+
reason: ScriptEndReason;
|
|
192
|
+
}>;
|
|
193
|
+
play(): void;
|
|
194
|
+
pause(): void;
|
|
195
|
+
resume(): void;
|
|
196
|
+
next(): void;
|
|
197
|
+
stop(): void;
|
|
198
|
+
}
|
|
159
199
|
type EstuaryEventMap = {
|
|
160
200
|
connected: (session: SessionInfo) => void;
|
|
161
201
|
disconnected: (reason: string) => void;
|
|
@@ -179,6 +219,10 @@ type EstuaryEventMap = {
|
|
|
179
219
|
/** Bot audio level 0.0–1.0, emitted during playback for both transports. */
|
|
180
220
|
botAudioLevel: (level: number) => void;
|
|
181
221
|
memoryUpdated: (event: MemoryUpdatedEvent) => void;
|
|
222
|
+
scriptLineStarted: (info: ScriptLineStartedInfo) => void;
|
|
223
|
+
scriptComplete: (info: {
|
|
224
|
+
reason: ScriptEndReason;
|
|
225
|
+
}) => void;
|
|
182
226
|
};
|
|
183
227
|
interface VoiceManager {
|
|
184
228
|
start(): Promise<void>;
|
|
@@ -360,6 +404,7 @@ declare class EstuaryClient extends TypedEventEmitter<EstuaryEventMap> {
|
|
|
360
404
|
private _hasAutoInterrupted;
|
|
361
405
|
private _autoInterruptGraceTimer;
|
|
362
406
|
private _isLiveKitSpeaking;
|
|
407
|
+
private _activeScript;
|
|
363
408
|
constructor(config: EstuaryConfig);
|
|
364
409
|
/** Memory API client for querying memories, graphs, and facts */
|
|
365
410
|
get memory(): MemoryClient;
|
|
@@ -385,6 +430,16 @@ declare class EstuaryClient extends TypedEventEmitter<EstuaryEventMap> {
|
|
|
385
430
|
sendText(text: string, textOnly?: boolean): void;
|
|
386
431
|
/** Script the character to say a specific prewritten line. Defaults to TTS enabled (textOnly=false). */
|
|
387
432
|
sayLine(text: string, textOnly?: boolean): void;
|
|
433
|
+
/**
|
|
434
|
+
* Script a sequence of prewritten lines. Lines are paced so each finishes before the next
|
|
435
|
+
* is sent — required because say_line interrupts any in-progress response server-side, so
|
|
436
|
+
* unpaced lines would stomp each other. Returns a controller (play/pause/resume/next/stop +
|
|
437
|
+
* an awaitable `done`). Starting a new script stops any currently-active one.
|
|
438
|
+
*/
|
|
439
|
+
playScript(lines: ScriptLine[], opts?: ScriptOptions): ScriptController;
|
|
440
|
+
/** Convenience alias of playScript() for fire-and-forget scripted sequences. */
|
|
441
|
+
sayLines(lines: ScriptLine[], opts?: ScriptOptions): ScriptController;
|
|
442
|
+
private createScriptHost;
|
|
388
443
|
/** Interrupt the current bot response */
|
|
389
444
|
interrupt(messageId?: string): void;
|
|
390
445
|
/** Send a camera image for vision processing */
|
|
@@ -470,4 +525,4 @@ declare class CharacterClient {
|
|
|
470
525
|
dispose(): void;
|
|
471
526
|
}
|
|
472
527
|
|
|
473
|
-
export { type BotResponse, type BotVoice, type CameraCaptureRequest, type CharacterAction, CharacterClient, type CharacterInfo, ConnectionState, type CoreFact, type CoreFactsResponse, ErrorCode, EstuaryClient, type EstuaryConfig, EstuaryError, type EstuaryEventMap, type InterruptData, type LiveKitTokenResponse, MemoryClient, type MemoryData, type MemoryGraphEdge, type MemoryGraphNode, type MemoryGraphOptions, type MemoryGraphResponse, type MemoryListOptions, type MemoryListResponse, type MemorySearchOptions, type MemorySearchResponse, type MemoryStatsResponse, type MemoryTimelineOptions, type MemoryTimelineResponse, type MemoryUpdatedEvent, type ParsedAction, type QuotaExceededData, type SessionCapabilities, type SessionInfo, type ShareOpenResponse, type SttResponse, type VoiceManager, type VoiceTransport, parseActions };
|
|
528
|
+
export { type BotResponse, type BotVoice, type CameraCaptureRequest, type CharacterAction, CharacterClient, type CharacterInfo, ConnectionState, type CoreFact, type CoreFactsResponse, ErrorCode, EstuaryClient, type EstuaryConfig, EstuaryError, type EstuaryEventMap, type InterruptData, type LiveKitTokenResponse, MemoryClient, type MemoryData, type MemoryGraphEdge, type MemoryGraphNode, type MemoryGraphOptions, type MemoryGraphResponse, type MemoryListOptions, type MemoryListResponse, type MemorySearchOptions, type MemorySearchResponse, type MemoryStatsResponse, type MemoryTimelineOptions, type MemoryTimelineResponse, type MemoryUpdatedEvent, type ParsedAction, type QuotaExceededData, type ScriptController, type ScriptEndReason, type ScriptLine, type ScriptLineStartedInfo, type ScriptOptions, type ScriptState, type SessionCapabilities, type SessionInfo, type ShareOpenResponse, type SttResponse, type VoiceManager, type VoiceTransport, parseActions };
|
package/dist/index.js
CHANGED
|
@@ -9624,6 +9624,231 @@ function parseActions(text) {
|
|
|
9624
9624
|
return parser.parse(text);
|
|
9625
9625
|
}
|
|
9626
9626
|
|
|
9627
|
+
// src/scripting/script-player.ts
|
|
9628
|
+
var DEFAULT_LINE_TIMEOUT_MS = 3e4;
|
|
9629
|
+
var ScriptPlayer = class {
|
|
9630
|
+
host;
|
|
9631
|
+
lines;
|
|
9632
|
+
lineGapMs;
|
|
9633
|
+
loop;
|
|
9634
|
+
lineTimeoutMs;
|
|
9635
|
+
_index = -1;
|
|
9636
|
+
_state = "idle";
|
|
9637
|
+
currentMessageId = null;
|
|
9638
|
+
lineAcked = false;
|
|
9639
|
+
retiredIds = /* @__PURE__ */ new Set();
|
|
9640
|
+
selfInterruptPending = false;
|
|
9641
|
+
pauseRequested = false;
|
|
9642
|
+
// Number of stray bot_responses still expected from text-only lines that were superseded
|
|
9643
|
+
// (via next()/stop()) BEFORE they were acked. Their server messageId isn't known yet, so we
|
|
9644
|
+
// can't retire them by id — instead we swallow that many fresh responses by arrival order.
|
|
9645
|
+
pendingStaleResponses = 0;
|
|
9646
|
+
lineTimer = null;
|
|
9647
|
+
gapTimer = null;
|
|
9648
|
+
resolveDone;
|
|
9649
|
+
done;
|
|
9650
|
+
onBotResponse = (r) => this.handleBotResponse(r);
|
|
9651
|
+
onAudioComplete = (messageId) => this.handleAudioComplete(messageId);
|
|
9652
|
+
onInterrupt = (data) => this.handleExternalInterrupt(data);
|
|
9653
|
+
onDisconnected = () => this.finish("disconnected");
|
|
9654
|
+
constructor(host, lines, opts = {}) {
|
|
9655
|
+
this.host = host;
|
|
9656
|
+
const defaultTextOnly = opts.textOnly ?? false;
|
|
9657
|
+
this.lines = lines.map(
|
|
9658
|
+
(line) => typeof line === "string" ? { text: line, textOnly: defaultTextOnly } : { text: line.text, textOnly: line.textOnly ?? defaultTextOnly }
|
|
9659
|
+
);
|
|
9660
|
+
this.lineGapMs = Math.max(0, opts.lineGapMs ?? 0);
|
|
9661
|
+
this.loop = opts.loop ?? false;
|
|
9662
|
+
this.lineTimeoutMs = opts.lineTimeoutMs ?? DEFAULT_LINE_TIMEOUT_MS;
|
|
9663
|
+
this.done = new Promise((resolve) => {
|
|
9664
|
+
this.resolveDone = resolve;
|
|
9665
|
+
});
|
|
9666
|
+
this.host.on("botResponse", this.onBotResponse);
|
|
9667
|
+
this.host.on("audioPlaybackComplete", this.onAudioComplete);
|
|
9668
|
+
this.host.on("interrupt", this.onInterrupt);
|
|
9669
|
+
this.host.on("disconnected", this.onDisconnected);
|
|
9670
|
+
if (opts.autoStart ?? true) {
|
|
9671
|
+
queueMicrotask(() => this.play());
|
|
9672
|
+
}
|
|
9673
|
+
}
|
|
9674
|
+
get length() {
|
|
9675
|
+
return this.lines.length;
|
|
9676
|
+
}
|
|
9677
|
+
get index() {
|
|
9678
|
+
return this._index;
|
|
9679
|
+
}
|
|
9680
|
+
get state() {
|
|
9681
|
+
return this._state;
|
|
9682
|
+
}
|
|
9683
|
+
play() {
|
|
9684
|
+
if (this._state === "done" || this._state === "playing") return;
|
|
9685
|
+
this.pauseRequested = false;
|
|
9686
|
+
this._state = "playing";
|
|
9687
|
+
this.startNextLine();
|
|
9688
|
+
}
|
|
9689
|
+
resume() {
|
|
9690
|
+
this.play();
|
|
9691
|
+
}
|
|
9692
|
+
pause() {
|
|
9693
|
+
if (this._state === "done") return;
|
|
9694
|
+
this.pauseRequested = true;
|
|
9695
|
+
}
|
|
9696
|
+
next() {
|
|
9697
|
+
if (this._state === "done") return;
|
|
9698
|
+
if (this._state === "idle") {
|
|
9699
|
+
this.play();
|
|
9700
|
+
return;
|
|
9701
|
+
}
|
|
9702
|
+
const expectInterrupt = this._state === "playing";
|
|
9703
|
+
this.pauseRequested = false;
|
|
9704
|
+
this._state = "playing";
|
|
9705
|
+
this.clearTimers();
|
|
9706
|
+
this.supersedeCurrentLine(expectInterrupt);
|
|
9707
|
+
this.startNextLine();
|
|
9708
|
+
}
|
|
9709
|
+
stop() {
|
|
9710
|
+
this.finish("stopped", true);
|
|
9711
|
+
}
|
|
9712
|
+
// ─── internals ────────────────────────────────────────────────
|
|
9713
|
+
hasSpeakableLine() {
|
|
9714
|
+
return this.lines.some((l) => l.text.trim().length > 0);
|
|
9715
|
+
}
|
|
9716
|
+
startNextLine() {
|
|
9717
|
+
if (this._state !== "playing") return;
|
|
9718
|
+
if (!this.hasSpeakableLine()) {
|
|
9719
|
+
this.finish("finished");
|
|
9720
|
+
return;
|
|
9721
|
+
}
|
|
9722
|
+
let next = this._index + 1;
|
|
9723
|
+
for (; ; ) {
|
|
9724
|
+
if (next >= this.lines.length) {
|
|
9725
|
+
if (this.loop) {
|
|
9726
|
+
next = 0;
|
|
9727
|
+
} else {
|
|
9728
|
+
this.finish("finished");
|
|
9729
|
+
return;
|
|
9730
|
+
}
|
|
9731
|
+
}
|
|
9732
|
+
if (this.lines[next].text.trim()) break;
|
|
9733
|
+
next++;
|
|
9734
|
+
}
|
|
9735
|
+
this._index = next;
|
|
9736
|
+
this.currentMessageId = null;
|
|
9737
|
+
this.lineAcked = false;
|
|
9738
|
+
if (!this.host.isConnected) {
|
|
9739
|
+
this.finish("disconnected");
|
|
9740
|
+
return;
|
|
9741
|
+
}
|
|
9742
|
+
const line = this.lines[next];
|
|
9743
|
+
this.host.sayLine(line.text, line.textOnly);
|
|
9744
|
+
this.lineTimer = setTimeout(() => this.handleLineTimeout(), this.lineTimeoutMs);
|
|
9745
|
+
}
|
|
9746
|
+
handleBotResponse(r) {
|
|
9747
|
+
if (this._state === "done" || this._index < 0) return;
|
|
9748
|
+
if (r.messageId && this.retiredIds.has(r.messageId)) return;
|
|
9749
|
+
if (r.isInterjection) return;
|
|
9750
|
+
if (!this.lineAcked) {
|
|
9751
|
+
if (this.pendingStaleResponses > 0) {
|
|
9752
|
+
this.pendingStaleResponses--;
|
|
9753
|
+
if (r.messageId) this.retiredIds.add(r.messageId);
|
|
9754
|
+
return;
|
|
9755
|
+
}
|
|
9756
|
+
this.lineAcked = true;
|
|
9757
|
+
this.currentMessageId = r.messageId ?? null;
|
|
9758
|
+
this.selfInterruptPending = false;
|
|
9759
|
+
this.host.emitScriptLineStarted({
|
|
9760
|
+
index: this._index,
|
|
9761
|
+
text: this.lines[this._index].text,
|
|
9762
|
+
messageId: r.messageId
|
|
9763
|
+
});
|
|
9764
|
+
}
|
|
9765
|
+
if (this.currentMessageId && r.messageId && r.messageId !== this.currentMessageId) return;
|
|
9766
|
+
const line = this.lines[this._index];
|
|
9767
|
+
const completesOnResponse = line.textOnly || !this.host.willPlayScriptedAudio;
|
|
9768
|
+
if (completesOnResponse && r.isFinal) {
|
|
9769
|
+
this.onLineComplete();
|
|
9770
|
+
}
|
|
9771
|
+
}
|
|
9772
|
+
handleAudioComplete(messageId) {
|
|
9773
|
+
if (this._state === "done" || this._index < 0) return;
|
|
9774
|
+
if (messageId && this.retiredIds.has(messageId)) return;
|
|
9775
|
+
const line = this.lines[this._index];
|
|
9776
|
+
if (line.textOnly || !this.host.willPlayScriptedAudio) return;
|
|
9777
|
+
if (this.currentMessageId && messageId && messageId !== this.currentMessageId) return;
|
|
9778
|
+
this.onLineComplete();
|
|
9779
|
+
}
|
|
9780
|
+
handleLineTimeout() {
|
|
9781
|
+
if (this._state === "done") return;
|
|
9782
|
+
this.host.log(`script line ${this._index} timed out after ${this.lineTimeoutMs}ms; advancing`);
|
|
9783
|
+
this.onLineComplete();
|
|
9784
|
+
}
|
|
9785
|
+
onLineComplete() {
|
|
9786
|
+
this.clearTimers();
|
|
9787
|
+
if (this.lineGapMs > 0) {
|
|
9788
|
+
this.gapTimer = setTimeout(() => this.afterGap(), this.lineGapMs);
|
|
9789
|
+
} else {
|
|
9790
|
+
this.afterGap();
|
|
9791
|
+
}
|
|
9792
|
+
}
|
|
9793
|
+
afterGap() {
|
|
9794
|
+
this.gapTimer = null;
|
|
9795
|
+
if (this._state === "done") return;
|
|
9796
|
+
if (this.pauseRequested) {
|
|
9797
|
+
this._state = "paused";
|
|
9798
|
+
return;
|
|
9799
|
+
}
|
|
9800
|
+
this.startNextLine();
|
|
9801
|
+
}
|
|
9802
|
+
handleExternalInterrupt(data) {
|
|
9803
|
+
if (this._state === "done") return;
|
|
9804
|
+
if (data?.messageId && this.retiredIds.has(data.messageId)) return;
|
|
9805
|
+
if (this.selfInterruptPending) {
|
|
9806
|
+
this.selfInterruptPending = false;
|
|
9807
|
+
return;
|
|
9808
|
+
}
|
|
9809
|
+
this.finish("interrupted");
|
|
9810
|
+
}
|
|
9811
|
+
/**
|
|
9812
|
+
* Retire the current line because we're moving off it.
|
|
9813
|
+
* @param expectInterrupt true when the line is still in-flight (next()/stop() mid-line), so the
|
|
9814
|
+
* server will emit a self-induced `interrupt` we must not treat as external. false when the
|
|
9815
|
+
* line already completed (e.g. next() while paused), where no interrupt is coming.
|
|
9816
|
+
*/
|
|
9817
|
+
supersedeCurrentLine(expectInterrupt) {
|
|
9818
|
+
if (this.lineAcked && this.currentMessageId) {
|
|
9819
|
+
this.retiredIds.add(this.currentMessageId);
|
|
9820
|
+
} else if (expectInterrupt && this.lines[this._index]?.textOnly) {
|
|
9821
|
+
this.pendingStaleResponses++;
|
|
9822
|
+
}
|
|
9823
|
+
if (expectInterrupt) this.selfInterruptPending = true;
|
|
9824
|
+
}
|
|
9825
|
+
clearTimers() {
|
|
9826
|
+
if (this.lineTimer) {
|
|
9827
|
+
clearTimeout(this.lineTimer);
|
|
9828
|
+
this.lineTimer = null;
|
|
9829
|
+
}
|
|
9830
|
+
if (this.gapTimer) {
|
|
9831
|
+
clearTimeout(this.gapTimer);
|
|
9832
|
+
this.gapTimer = null;
|
|
9833
|
+
}
|
|
9834
|
+
}
|
|
9835
|
+
finish(reason, interrupt = false) {
|
|
9836
|
+
if (this._state === "done") return;
|
|
9837
|
+
this.clearTimers();
|
|
9838
|
+
if (interrupt) {
|
|
9839
|
+
this.selfInterruptPending = true;
|
|
9840
|
+
if (this.host.isConnected) this.host.interrupt();
|
|
9841
|
+
}
|
|
9842
|
+
this._state = "done";
|
|
9843
|
+
this.host.off("botResponse", this.onBotResponse);
|
|
9844
|
+
this.host.off("audioPlaybackComplete", this.onAudioComplete);
|
|
9845
|
+
this.host.off("interrupt", this.onInterrupt);
|
|
9846
|
+
this.host.off("disconnected", this.onDisconnected);
|
|
9847
|
+
this.host.emitScriptComplete({ reason });
|
|
9848
|
+
this.resolveDone({ reason });
|
|
9849
|
+
}
|
|
9850
|
+
};
|
|
9851
|
+
|
|
9627
9852
|
// src/client.ts
|
|
9628
9853
|
var DEFAULT_SAMPLE_RATE = 24e3;
|
|
9629
9854
|
var REST_UNAVAILABLE_MESSAGE = "REST API not available with session token auth. Use a server-side proxy for REST calls.";
|
|
@@ -9640,6 +9865,7 @@ var EstuaryClient = class extends TypedEventEmitter {
|
|
|
9640
9865
|
_hasAutoInterrupted = false;
|
|
9641
9866
|
_autoInterruptGraceTimer = null;
|
|
9642
9867
|
_isLiveKitSpeaking = false;
|
|
9868
|
+
_activeScript = null;
|
|
9643
9869
|
constructor(config) {
|
|
9644
9870
|
super();
|
|
9645
9871
|
if (!config.apiKey && !config.sessionToken) {
|
|
@@ -9714,6 +9940,10 @@ var EstuaryClient = class extends TypedEventEmitter {
|
|
|
9714
9940
|
/** Disconnect from the server */
|
|
9715
9941
|
async disconnect() {
|
|
9716
9942
|
this.logger.info("Disconnecting...");
|
|
9943
|
+
if (this._activeScript && this._activeScript.state !== "done") {
|
|
9944
|
+
this._activeScript.stop();
|
|
9945
|
+
}
|
|
9946
|
+
this._activeScript = null;
|
|
9717
9947
|
if (this._autoInterruptGraceTimer) {
|
|
9718
9948
|
clearTimeout(this._autoInterruptGraceTimer);
|
|
9719
9949
|
this._autoInterruptGraceTimer = null;
|
|
@@ -9735,6 +9965,59 @@ var EstuaryClient = class extends TypedEventEmitter {
|
|
|
9735
9965
|
this.ensureConnected();
|
|
9736
9966
|
this.socketManager.emitEvent("say_line", { text, text_only: textOnly });
|
|
9737
9967
|
}
|
|
9968
|
+
/**
|
|
9969
|
+
* Script a sequence of prewritten lines. Lines are paced so each finishes before the next
|
|
9970
|
+
* is sent — required because say_line interrupts any in-progress response server-side, so
|
|
9971
|
+
* unpaced lines would stomp each other. Returns a controller (play/pause/resume/next/stop +
|
|
9972
|
+
* an awaitable `done`). Starting a new script stops any currently-active one.
|
|
9973
|
+
*/
|
|
9974
|
+
playScript(lines, opts) {
|
|
9975
|
+
this.ensureConnected();
|
|
9976
|
+
if (this._activeScript && this._activeScript.state !== "done") {
|
|
9977
|
+
this._activeScript.stop();
|
|
9978
|
+
}
|
|
9979
|
+
const player = new ScriptPlayer(this.createScriptHost(), lines, opts);
|
|
9980
|
+
this._activeScript = player;
|
|
9981
|
+
return player;
|
|
9982
|
+
}
|
|
9983
|
+
/** Convenience alias of playScript() for fire-and-forget scripted sequences. */
|
|
9984
|
+
sayLines(lines, opts) {
|
|
9985
|
+
return this.playScript(lines, opts);
|
|
9986
|
+
}
|
|
9987
|
+
createScriptHost() {
|
|
9988
|
+
const self = this;
|
|
9989
|
+
return {
|
|
9990
|
+
sayLine(text, textOnly) {
|
|
9991
|
+
self.sayLine(text, textOnly);
|
|
9992
|
+
},
|
|
9993
|
+
interrupt() {
|
|
9994
|
+
self.interrupt();
|
|
9995
|
+
},
|
|
9996
|
+
on(event, listener) {
|
|
9997
|
+
self.on(event, listener);
|
|
9998
|
+
return self;
|
|
9999
|
+
},
|
|
10000
|
+
off(event, listener) {
|
|
10001
|
+
self.off(event, listener);
|
|
10002
|
+
return self;
|
|
10003
|
+
},
|
|
10004
|
+
get isConnected() {
|
|
10005
|
+
return self.isConnected;
|
|
10006
|
+
},
|
|
10007
|
+
get willPlayScriptedAudio() {
|
|
10008
|
+
return self.audioPlayer != null;
|
|
10009
|
+
},
|
|
10010
|
+
emitScriptLineStarted(info) {
|
|
10011
|
+
self.emit("scriptLineStarted", info);
|
|
10012
|
+
},
|
|
10013
|
+
emitScriptComplete(info) {
|
|
10014
|
+
self.emit("scriptComplete", info);
|
|
10015
|
+
},
|
|
10016
|
+
log(msg) {
|
|
10017
|
+
self.logger.debug(msg);
|
|
10018
|
+
}
|
|
10019
|
+
};
|
|
10020
|
+
}
|
|
9738
10021
|
/** Interrupt the current bot response */
|
|
9739
10022
|
interrupt(messageId) {
|
|
9740
10023
|
this.ensureConnected();
|