@openclaw/discord 2026.9.1 → 2026.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action-runtime-api.js +1 -1
- package/dist/{activities-api-E-segZn6.js → activities-api-CTcGq3Hf.js} +4 -4
- package/dist/activities-api.js +1 -1
- package/dist/{allow-list-C2JuH-R0.js → allow-list-DBTtR2dz.js} +3 -29
- package/dist/api.js +12 -12
- package/dist/{approval-handler.runtime-CW5k3mSa.js → approval-handler.runtime-DUSe5fyH.js} +5 -5
- package/dist/{audit-B9rydv5a.js → audit-DpPfwoJM.js} +4 -4
- package/dist/{channel-D93LnbTE.js → channel-BHajPgwR.js} +16 -16
- package/dist/{channel-actions.runtime-DMFv4doE.js → channel-actions.runtime-DS8pXo0S.js} +29 -32
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.setup-DTL7kPOM.js → channel.setup-BWmMe1tT.js} +3 -3
- package/dist/command-owners-D78sAoOz.js +10 -0
- package/dist/{components-DmXYIQ26.js → components-BimX47Mq.js} +1 -1
- package/dist/{conversation-binding-route-24afmvEC.js → conversation-binding-route-DuR5NGIx.js} +1 -1
- package/dist/{directory-config-BD4YL7wT.js → directory-config-CD2v2Szs.js} +1 -1
- package/dist/directory-contract-api.js +1 -1
- package/dist/{directory-live-BrFVc1Ds.js → directory-live-DaJzWJ2N.js} +2 -2
- package/dist/{doctor-BUB41_lr.js → doctor-D4HH1inA.js} +6 -3
- package/dist/doctor-contract-api.js +28 -26
- package/dist/{handle-action.guild-admin-B0egstYq.js → handle-action.guild-admin-4We6wWQU.js} +6 -3
- package/dist/index.js +1 -1
- package/dist/{ingress-identity-BCzfPPYM.js → ingress-identity-XNmW-mZS.js} +1 -1
- package/dist/{message-handler-Qdw9c0j4.js → message-handler-DhgBabyU.js} +52 -65
- package/dist/{message-handler.preflight-Dxi2yBfR.js → message-handler.preflight-oZ7dQCd0.js} +12 -12
- package/dist/{message-handler.process-BYDjtxZJ.js → message-handler.process-BzCYf1pa.js} +14 -14
- package/dist/{message-text-BRVqYm-t.js → message-text-DK4NnBg-.js} +1 -1
- package/dist/{outbound-adapter-DEhthWTk.js → outbound-adapter-CtggmGh9.js} +6 -6
- package/dist/{outbound-components-DehyxvA4.js → outbound-components-Dn8SJWMS.js} +3 -3
- package/dist/{outbound-session-route-BFGRF9fE.js → outbound-session-route-Q_HEusBQ.js} +5 -6
- package/dist/{owner-access-yc1sX7Gx.js → owner-access-Cp8r6Ipf.js} +3 -2
- package/dist/{pluralkit-XmmAHoV6.js → pluralkit-BY75YcK0.js} +1 -1
- package/dist/{provider-session.runtime-BA2BPyZ4.js → provider-session.runtime-BL2lHBH7.js} +3 -3
- package/dist/{provider-DuDtKbF8.js → provider-uKmmrDM4.js} +28 -28
- package/dist/provider.runtime-C-IkWF2w.js +2 -0
- package/dist/{realtime-session.runtime-DgSbX6ay.js → realtime-session.runtime-CGYm1t_0.js} +705 -295
- package/dist/{resolve-allowlist-common-BJ5UZNNT.js → resolve-allowlist-common-sKIKy-cF.js} +1 -1
- package/dist/{resolve-channels-CYseD5Uw.js → resolve-channels-CoiMlWy0.js} +3 -3
- package/dist/{resolve-users-DrmhDrqm.js → resolve-users-DJpiokfW.js} +2 -2
- package/dist/{retry-BNCxiyyR.js → retry-DQt1zDgH.js} +238 -184
- package/dist/{runtime-DB7Fb6AU.js → runtime-BHsdEdr5.js} +1 -1
- package/dist/{runtime-BbWNtfyq.js → runtime-Ghtfh5ZA.js} +32 -24
- package/dist/runtime-api.js +18 -18
- package/dist/{send-BnTEIeLE.js → send-CZeWKUiA.js} +3 -3
- package/dist/{send.components-BWVEcjRP.js → send.components-BG5IDABj.js} +4 -4
- package/dist/{send.outbound-b5xwHTlL.js → send.outbound-DHuFUL6W.js} +10 -44
- package/dist/{send.shared-5vxBTagO.js → send.shared-B0n_fWzu.js} +62 -141
- package/dist/{sender-identity-Dt0imZyY.js → sender-identity-B8Ncfi63.js} +1 -1
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{shared-BSJWHEMA.js → shared-C3dKun_X.js} +6 -3
- package/dist/{subagent-hooks-IzQKX4RQ.js → subagent-hooks-ZoQXEI1E.js} +2 -2
- package/dist/subagent-hooks-api.js +1 -1
- package/dist/{system-events-BOiDS8b3.js → system-events-DFF_awl7.js} +2 -2
- package/dist/{target-resolver-BVI8xSSC.js → target-resolver-DLtHDwMK.js} +2 -2
- package/dist/targets-BObRyaKX.js +3 -0
- package/dist/{thread-bindings-D7mcaXQC.js → thread-bindings-DKvsNjQR.js} +5 -5
- package/dist/{thread-bindings.manager-B1oySiVc.js → thread-bindings.manager-pbbRT5gb.js} +5 -5
- package/dist/{thread-bindings.session-updates-CC7HoQvt.js → thread-bindings.session-updates-t6jeFB1-.js} +1 -1
- package/dist/{thread-bindings.state-D8OMdk4O.js → thread-bindings.state-DtgOqsbX.js} +34 -1
- package/dist/{threading-OQnL-0Zx.js → threading-BCx49X-6.js} +3 -3
- package/dist/{transcripts-source-D8lH7AkN.js → transcripts-source-Dc6Nnli5.js} +48 -5
- package/dist/transcripts-source-api.js +1 -1
- package/dist/{typing-C9q0AQMb.js → typing-CuopV-Hi.js} +1 -1
- package/dist/{voice-runtime-Dfhjh1tV.js → voice-runtime-BQnILUbG.js} +82 -35
- package/node_modules/@discordjs/voice/node_modules/@types/node/README.md +1 -1
- package/node_modules/@discordjs/voice/node_modules/@types/node/buffer.buffer.d.ts +5 -5
- package/node_modules/@discordjs/voice/node_modules/@types/node/crypto.d.ts +2 -2
- package/node_modules/@discordjs/voice/node_modules/@types/node/http.d.ts +16 -0
- package/node_modules/@discordjs/voice/node_modules/@types/node/http2.d.ts +10 -2
- package/node_modules/@discordjs/voice/node_modules/@types/node/package.json +2 -2
- package/node_modules/@discordjs/voice/node_modules/@types/node/process.d.ts +52 -0
- package/node_modules/@discordjs/voice/node_modules/@types/node/quic.d.ts +210 -28
- package/node_modules/@discordjs/voice/node_modules/@types/node/sqlite.d.ts +6 -2
- package/node_modules/@discordjs/voice/node_modules/@types/node/test.d.ts +36 -0
- package/node_modules/typebox/build/format/json_pointer.d.mts +2 -2
- package/node_modules/typebox/build/format/json_pointer.mjs +2 -2
- package/node_modules/typebox/build/format/json_pointer_uri_fragment.d.mts +2 -2
- package/node_modules/typebox/build/format/json_pointer_uri_fragment.mjs +2 -2
- package/node_modules/typebox/build/format/relative_json_pointer.d.mts +2 -2
- package/node_modules/typebox/build/format/relative_json_pointer.mjs +2 -2
- package/node_modules/typebox/build/system/memory/assign.mjs +2 -1
- package/node_modules/typebox/build/system/memory/create.mjs +3 -4
- package/node_modules/typebox/build/system/memory/discard.mjs +2 -2
- package/node_modules/typebox/build/system/memory/freeze.d.mts +4 -0
- package/node_modules/typebox/build/system/memory/freeze.mjs +6 -0
- package/node_modules/typebox/build/system/memory/update.mjs +3 -2
- package/node_modules/typebox/package.json +1 -1
- package/node_modules/undici/docs/docs/api/Client.md +4 -0
- package/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +3 -2
- package/node_modules/undici/docs/docs/api/EventSource.md +4 -0
- package/node_modules/undici/docs/docs/api/Interceptors.md +7 -3
- package/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +7 -1
- package/node_modules/undici/lib/cache/memory-cache-store.js +2 -2
- package/node_modules/undici/lib/core/symbols.js +2 -0
- package/node_modules/undici/lib/dispatcher/agent.js +7 -2
- package/node_modules/undici/lib/dispatcher/balanced-pool.js +7 -4
- package/node_modules/undici/lib/dispatcher/client-h1.js +12 -4
- package/node_modules/undici/lib/dispatcher/client-h2.js +43 -3
- package/node_modules/undici/lib/dispatcher/client.js +3 -2
- package/node_modules/undici/lib/dispatcher/dispatcher-base.js +13 -1
- package/node_modules/undici/lib/dispatcher/dispatcher.js +15 -1
- package/node_modules/undici/lib/dispatcher/dispatcher1-wrapper.js +3 -0
- package/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +10 -3
- package/node_modules/undici/lib/dispatcher/proxy-agent.js +18 -1
- package/node_modules/undici/lib/dispatcher/retry-agent.js +3 -0
- package/node_modules/undici/lib/dispatcher/round-robin-pool.js +1 -1
- package/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +91 -21
- package/node_modules/undici/lib/handler/cache-handler.js +29 -5
- package/node_modules/undici/lib/handler/decorator-handler.js +7 -1
- package/node_modules/undici/lib/handler/deduplication-handler.js +12 -6
- package/node_modules/undici/lib/handler/redirect-handler.js +21 -4
- package/node_modules/undici/lib/handler/retry-handler.js +148 -30
- package/node_modules/undici/lib/interceptor/cache.js +65 -12
- package/node_modules/undici/lib/interceptor/decompress.js +115 -13
- package/node_modules/undici/lib/interceptor/deduplicate.js +5 -4
- package/node_modules/undici/lib/interceptor/dns.js +7 -0
- package/node_modules/undici/lib/interceptor/dump.js +10 -23
- package/node_modules/undici/lib/mock/mock-agent.js +13 -1
- package/node_modules/undici/lib/mock/mock-utils.js +11 -10
- package/node_modules/undici/lib/util/cache.js +45 -3
- package/node_modules/undici/lib/web/cookies/parse.js +150 -147
- package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +28 -1
- package/node_modules/undici/lib/web/eventsource/eventsource.js +13 -3
- package/node_modules/undici/lib/web/fetch/index.js +6 -0
- package/node_modules/undici/lib/web/fetch/request.js +2 -3
- package/node_modules/undici/lib/web/fetch/util.js +6 -1
- package/node_modules/undici/lib/web/webidl/index.js +3 -3
- package/node_modules/undici/lib/web/websocket/connection.js +1 -1
- package/node_modules/undici/lib/web/websocket/permessage-deflate.js +5 -0
- package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +8 -10
- package/node_modules/undici/package.json +3 -3
- package/node_modules/undici/types/client.d.ts +10 -0
- package/node_modules/undici/types/interceptors.d.ts +2 -0
- package/node_modules/undici/types/mock-agent.d.ts +11 -10
- package/node_modules/undici/types/socks5-proxy-agent.d.ts +2 -0
- package/openclaw.plugin.json +1 -2
- package/package.json +6 -6
- package/dist/provider.runtime-Db8YwZBv.js +0 -2
- package/dist/targets-DbxVbq8k.js +0 -3
|
@@ -1,25 +1,107 @@
|
|
|
1
1
|
import { n as discordRealtimeVoiceSecretOwnerId } from "./secret-config-contract-DFu9hfU8.js";
|
|
2
|
-
import { a as
|
|
2
|
+
import { a as createDiscordOpusEncodeStream, c as formatVoiceLogPreview, i as convertRealtimePcm24kMonoToDiscordPcm48kStereo, l as loadDiscordVoiceSdk, n as logVoiceVerbose, o as formatVoiceIngressPrompt, r as convertDiscordPcm48kStereoToRealtimePcm24kMono, s as maybeControlDiscordVoiceAgentRun } from "./voice-runtime-BQnILUbG.js";
|
|
3
3
|
import { asDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
4
4
|
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
5
5
|
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
|
6
6
|
import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
7
7
|
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
|
|
8
8
|
import { assertSecretOwnerAvailable, isSecretOwnerAvailable } from "openclaw/plugin-sdk/channel-secret-owner-runtime";
|
|
9
|
-
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AGENT_CONTROL_TOOL, REALTIME_VOICE_AGENT_CONTROL_TOOL_NAME, REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ, buildRealtimeVoiceAgentErrorProviderResult, buildRealtimeVoiceSessionInstructions, buildRealtimeVoiceSpeakExactMessage, canonicalizeRealtimeVoiceProviderId, classifyRealtimeVoiceConsultToolCall, classifySkippableRealtimeVoiceConsultTranscript, controlRealtimeVoiceAgentRun, createRealtimeVoiceAgentTalkbackQueue,
|
|
9
|
+
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AGENT_CONTROL_TOOL, REALTIME_VOICE_AGENT_CONTROL_TOOL_NAME, REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ, buildRealtimeVoiceAgentErrorProviderResult, buildRealtimeVoiceSessionInstructions, buildRealtimeVoiceSpeakExactMessage, canonicalizeRealtimeVoiceProviderId, classifyRealtimeVoiceConsultToolCall, classifySkippableRealtimeVoiceConsultTranscript, controlRealtimeVoiceAgentRun, createRealtimeVoiceAgentTalkbackQueue, createRealtimeVoiceOutputActivityTracker, createRealtimeVoiceSessionHarness, isRealtimeVoiceWakeNameRequired, matchRealtimeVoiceActivationName, matchRealtimeVoiceConsultQuestions, parseRealtimeVoiceAgentControlToolArgs, realtimeVoiceAudioDurationMs, resolveConfiguredRealtimeVoiceProvider, resolveRealtimeVoiceAgentConsultTools, resolveRealtimeVoiceBargeIn, resolveRealtimeVoiceInterruptResponseOnInputAudio, resolveRealtimeVoiceMinBargeInAudioEndMs, resolveRealtimeVoiceSessionPolicy } from "openclaw/plugin-sdk/realtime-voice";
|
|
10
10
|
import { PassThrough, pipeline } from "node:stream";
|
|
11
|
+
//#region extensions/discord/src/voice/realtime-player.ts
|
|
12
|
+
/** One physical player serves every speaker lane in the room. */
|
|
13
|
+
var DiscordRealtimePlayer = class {
|
|
14
|
+
constructor(player) {
|
|
15
|
+
this.player = player;
|
|
16
|
+
this.queue = [];
|
|
17
|
+
this.lanes = /* @__PURE__ */ new Set();
|
|
18
|
+
this.changing = false;
|
|
19
|
+
this.closed = false;
|
|
20
|
+
this.onIdle = () => {
|
|
21
|
+
const request = this.current;
|
|
22
|
+
this.current = void 0;
|
|
23
|
+
if (request) this.transition(() => request.onIdle());
|
|
24
|
+
};
|
|
25
|
+
player.on(loadDiscordVoiceSdk().AudioPlayerStatus.Idle, this.onIdle);
|
|
26
|
+
}
|
|
27
|
+
registerLane(lane) {
|
|
28
|
+
this.lanes.add(lane);
|
|
29
|
+
return () => this.lanes.delete(lane);
|
|
30
|
+
}
|
|
31
|
+
enqueue(request) {
|
|
32
|
+
if (this.closed || this.current === request || this.queue.includes(request)) return;
|
|
33
|
+
this.queue.push(request);
|
|
34
|
+
this.drain();
|
|
35
|
+
}
|
|
36
|
+
cancel(request) {
|
|
37
|
+
this.queue = this.queue.filter((queued) => queued !== request);
|
|
38
|
+
if (this.current !== request) return;
|
|
39
|
+
this.current = void 0;
|
|
40
|
+
this.transition(() => this.player.stop(true));
|
|
41
|
+
}
|
|
42
|
+
handleBargeIn(reason = "barge-in") {
|
|
43
|
+
if (this.current) {
|
|
44
|
+
this.current.onBargeIn(reason);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
for (const lane of this.lanes) if (lane.hasOutput()) lane.onBargeIn(reason);
|
|
48
|
+
}
|
|
49
|
+
isActive() {
|
|
50
|
+
return this.current !== void 0 || Array.from(this.lanes).some((lane) => lane.hasOutput());
|
|
51
|
+
}
|
|
52
|
+
cancelForControl() {
|
|
53
|
+
this.transition(() => {
|
|
54
|
+
this.queue = [];
|
|
55
|
+
for (const lane of this.lanes) lane.cancelForControl();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
close() {
|
|
59
|
+
this.closed = true;
|
|
60
|
+
this.queue = [];
|
|
61
|
+
this.lanes.clear();
|
|
62
|
+
this.current = void 0;
|
|
63
|
+
this.player.off(loadDiscordVoiceSdk().AudioPlayerStatus.Idle, this.onIdle);
|
|
64
|
+
this.player.stop(true);
|
|
65
|
+
}
|
|
66
|
+
transition(action) {
|
|
67
|
+
const wasChanging = this.changing;
|
|
68
|
+
this.changing = true;
|
|
69
|
+
try {
|
|
70
|
+
action();
|
|
71
|
+
} finally {
|
|
72
|
+
this.changing = wasChanging;
|
|
73
|
+
this.drain();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
drain() {
|
|
77
|
+
if (this.closed || this.changing || this.current) return;
|
|
78
|
+
const next = this.queue.shift();
|
|
79
|
+
if (!next) return;
|
|
80
|
+
this.current = next;
|
|
81
|
+
this.transition(() => {
|
|
82
|
+
try {
|
|
83
|
+
this.player.play(next.createResource());
|
|
84
|
+
next.onStart();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
this.current = void 0;
|
|
87
|
+
this.player.stop(true);
|
|
88
|
+
next.onError(error);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
//#endregion
|
|
11
94
|
//#region extensions/discord/src/voice/realtime-transcript.ts
|
|
95
|
+
const PARTIAL_TRANSCRIPT_MAX_CHARS = 240;
|
|
12
96
|
function mergeRealtimePartialTranscript(previous, next) {
|
|
13
97
|
const trimmed = next.trim();
|
|
14
98
|
if (!trimmed) return previous;
|
|
15
99
|
const merged = trimmed.startsWith(previous) ? trimmed : `${previous}${next}`;
|
|
16
|
-
return sliceUtf16Safe(merged,
|
|
100
|
+
return sliceUtf16Safe(merged, 0, PARTIAL_TRANSCRIPT_MAX_CHARS);
|
|
17
101
|
}
|
|
18
102
|
//#endregion
|
|
19
103
|
//#region extensions/discord/src/voice/realtime-turns.ts
|
|
20
|
-
const logger$
|
|
21
|
-
const DISCORD_REALTIME_PENDING_SPEAKER_CONTEXT_LIMIT = 32;
|
|
22
|
-
const DISCORD_REALTIME_IGNORED_WAKE_NAME_CONTEXT_TTL_MS = 1e4;
|
|
104
|
+
const logger$5 = createSubsystemLogger("discord/voice");
|
|
23
105
|
const DISCORD_REALTIME_WAKE_NAME_FOLLOWUP_TTL_MS = 1e4;
|
|
24
106
|
const REALTIME_PCM16_BYTES_PER_SAMPLE = 2;
|
|
25
107
|
const DISCORD_REALTIME_TRAILING_SILENCE_MIN_MS = 700;
|
|
@@ -27,49 +109,59 @@ const DISCORD_REALTIME_TRAILING_SILENCE_MAX_MS = 3e3;
|
|
|
27
109
|
var DiscordRealtimeTurns = class {
|
|
28
110
|
constructor(params) {
|
|
29
111
|
this.params = params;
|
|
30
|
-
this.
|
|
31
|
-
limit: DISCORD_REALTIME_PENDING_SPEAKER_CONTEXT_LIMIT,
|
|
32
|
-
ignoredContextTtlMs: DISCORD_REALTIME_IGNORED_WAKE_NAME_CONTEXT_TTL_MS,
|
|
33
|
-
deferUntilAudio: true
|
|
34
|
-
});
|
|
112
|
+
this.pendingAudio = false;
|
|
35
113
|
this.partialUserTranscript = "";
|
|
36
114
|
this.wakeNameAckedForTurn = false;
|
|
37
115
|
}
|
|
38
116
|
beginSpeakerTurn(context, userId) {
|
|
117
|
+
if (this.source && (this.source.userId !== userId || this.source.senderIsOwner !== context.senderIsOwner)) throw new Error("A Discord realtime connection cannot change speaker authority");
|
|
118
|
+
this.source ??= Object.freeze({
|
|
119
|
+
userId,
|
|
120
|
+
senderIsOwner: context.senderIsOwner
|
|
121
|
+
});
|
|
39
122
|
this.resetPartialWakeNameTracking();
|
|
40
|
-
const turn =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
123
|
+
const turn = {
|
|
124
|
+
context: {
|
|
125
|
+
...context,
|
|
126
|
+
...this.source
|
|
127
|
+
},
|
|
128
|
+
startedAt: Date.now(),
|
|
129
|
+
hasAudio: false,
|
|
130
|
+
closed: false,
|
|
44
131
|
transcripts: this.params.entry.transcripts,
|
|
45
132
|
inputDiscordBytes: 0,
|
|
46
133
|
inputRealtimeBytes: 0,
|
|
47
134
|
inputChunks: 0,
|
|
48
135
|
interruptedPlayback: false
|
|
49
|
-
}
|
|
136
|
+
};
|
|
50
137
|
return {
|
|
51
138
|
sendInputAudio: (discordPcm48kStereo) => this.sendInputAudioForTurn(turn, discordPcm48kStereo),
|
|
52
139
|
close: () => {
|
|
53
140
|
this.sendRealtimeTrailingSilenceForTurn(turn);
|
|
54
141
|
this.logSpeakerTurnClosed(turn);
|
|
55
|
-
|
|
142
|
+
turn.closed = true;
|
|
56
143
|
}
|
|
57
144
|
};
|
|
58
145
|
}
|
|
59
146
|
handlePartialUserTranscript(text) {
|
|
60
|
-
if (!this.isWakeNameRequired() || this.wakeNameAckedForTurn) return;
|
|
147
|
+
if (!this.lastSpeakerTurn || !this.isWakeNameRequired() || this.wakeNameAckedForTurn) return;
|
|
61
148
|
this.partialUserTranscript = mergeRealtimePartialTranscript(this.partialUserTranscript, text);
|
|
62
149
|
const wakeNameResult = matchRealtimeVoiceActivationName(this.partialUserTranscript, this.params.wakeNames());
|
|
63
|
-
if (
|
|
150
|
+
if (wakeNameResult?.edge !== "leading" || wakeNameResult.match !== "exact") return;
|
|
64
151
|
this.wakeNameAckedForTurn = true;
|
|
65
152
|
this.params.playback.sendWakeNameAck(wakeNameResult);
|
|
66
153
|
}
|
|
67
154
|
async handleFinalUserTranscript(text) {
|
|
155
|
+
if (!this.lastSpeakerTurn || this.params.stopped()) return;
|
|
68
156
|
const providerEpoch = this.params.providerEpoch();
|
|
69
157
|
const trimmed = text.trim();
|
|
70
|
-
if (!trimmed)
|
|
158
|
+
if (!trimmed) {
|
|
159
|
+
this.pendingAudio = false;
|
|
160
|
+
this.resetPartialWakeNameTracking();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
71
163
|
this.partialUserTranscript = "";
|
|
72
|
-
const transcriptsTurn = this.
|
|
164
|
+
const transcriptsTurn = this.lastSpeakerTurn;
|
|
73
165
|
let transcriptAttribution = this.transcriptAttributionFromTurn(transcriptsTurn);
|
|
74
166
|
const humanParticipantCount = this.params.getHumanParticipantCount();
|
|
75
167
|
const requireWakeName = this.isWakeNameRequired(humanParticipantCount);
|
|
@@ -80,12 +172,12 @@ var DiscordRealtimeTurns = class {
|
|
|
80
172
|
transcriptAttribution ??= pendingWakeNameFollowup;
|
|
81
173
|
if (!pendingWakeNameFollowup) {
|
|
82
174
|
this.recordTranscriptUtterance(trimmed, transcriptAttribution, providerEpoch);
|
|
83
|
-
this.
|
|
84
|
-
logger$
|
|
175
|
+
this.consumePendingSpeakerContext();
|
|
176
|
+
logger$5.info(`discord voice: realtime wake-name gate ignored transcript chars=${trimmed.length} humanParticipants=${humanParticipantCount} voiceSession=${this.params.entry.voiceSessionKey} agent=${this.params.entry.route.agentId} wakeNames=${this.params.wakeNames().join(",") || "none"}`);
|
|
85
177
|
return;
|
|
86
178
|
}
|
|
87
179
|
forcedSpeakerContext = pendingWakeNameFollowup.context;
|
|
88
|
-
logger$
|
|
180
|
+
logger$5.info(`discord voice: realtime wake-name follow-up accepted chars=${trimmed.length} speaker=${forcedSpeakerContext.speakerLabel} voiceSession=${this.params.entry.voiceSessionKey} agent=${this.params.entry.route.agentId}`);
|
|
89
181
|
}
|
|
90
182
|
this.recordTranscriptUtterance(trimmed, transcriptAttribution, providerEpoch);
|
|
91
183
|
const acceptedText = wakeNameResult.allowed ? wakeNameResult.text || trimmed : trimmed;
|
|
@@ -103,23 +195,24 @@ var DiscordRealtimeTurns = class {
|
|
|
103
195
|
resetProviderContinuity() {
|
|
104
196
|
this.partialUserTranscript = "";
|
|
105
197
|
this.pendingWakeNameFollowup = void 0;
|
|
198
|
+
this.pendingAudio = false;
|
|
199
|
+
this.lastSpeakerTurn = void 0;
|
|
106
200
|
}
|
|
107
201
|
clear() {
|
|
108
|
-
this.
|
|
202
|
+
this.pendingAudio = false;
|
|
203
|
+
this.lastSpeakerTurn = void 0;
|
|
109
204
|
this.resetPartialWakeNameTracking();
|
|
110
205
|
this.pendingWakeNameFollowup = void 0;
|
|
111
206
|
}
|
|
112
207
|
consumePendingSpeakerContext() {
|
|
113
|
-
|
|
208
|
+
this.pendingAudio = false;
|
|
209
|
+
return this.speakerContext();
|
|
114
210
|
}
|
|
115
|
-
|
|
116
|
-
return this.
|
|
117
|
-
}
|
|
118
|
-
peekPendingSpeakerTurn() {
|
|
119
|
-
return this.speakerTurns.peekAudioTurn();
|
|
211
|
+
speakerContext() {
|
|
212
|
+
return this.lastSpeakerTurn?.context;
|
|
120
213
|
}
|
|
121
214
|
hasPendingSpeakerAudioContext() {
|
|
122
|
-
return this.
|
|
215
|
+
return this.pendingAudio;
|
|
123
216
|
}
|
|
124
217
|
sendInputAudioForTurn(turn, discordPcm48kStereo) {
|
|
125
218
|
const bridge = this.params.bridge();
|
|
@@ -130,27 +223,28 @@ var DiscordRealtimeTurns = class {
|
|
|
130
223
|
turn.inputDiscordBytes += discordPcm48kStereo.length;
|
|
131
224
|
turn.inputRealtimeBytes += realtimePcm.length;
|
|
132
225
|
turn.inputChunks += 1;
|
|
133
|
-
if (turn.inputChunks === 1) logger$
|
|
134
|
-
|
|
135
|
-
if (!turn.interruptedPlayback && this.params.playback.isBargeInEnabled() && outputActive) {
|
|
226
|
+
if (turn.inputChunks === 1) logger$5.info(`discord voice: realtime input audio started guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} user=${turn.context.userId} speaker=${turn.context.speakerLabel} discordBytes=${discordPcm48kStereo.length} realtimeBytes=${realtimePcm.length} outputAudioMs=${this.params.playback.outputAudioMs()} outputActive=${this.params.playback.isOutputAudioActive()}`);
|
|
227
|
+
if (!turn.interruptedPlayback && this.params.interruptRoomPlayback()) {
|
|
136
228
|
turn.interruptedPlayback = true;
|
|
137
229
|
logVoiceVerbose(`realtime barge-in from active speaker audio: guild ${this.params.entry.guildId} channel ${this.params.entry.channelId} user ${turn.context.userId}`);
|
|
138
|
-
logger$
|
|
139
|
-
this.params.playback.handleBargeIn("active-speaker-audio");
|
|
230
|
+
logger$5.info(`discord voice: realtime barge-in detected source=active-speaker-audio guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} user=${turn.context.userId} speaker=${turn.context.speakerLabel} discordBytes=${discordPcm48kStereo.length} realtimeBytes=${realtimePcm.length}`);
|
|
140
231
|
}
|
|
141
232
|
if (this.params.recordInputAudio(realtimePcm)) bridge.sendAudio(realtimePcm);
|
|
142
233
|
}
|
|
143
234
|
}
|
|
144
235
|
registerSpeakerTurnAudioStarted(turn) {
|
|
236
|
+
this.pendingAudio = true;
|
|
237
|
+
this.lastSpeakerTurn = turn;
|
|
238
|
+
turn.lastAudioAt = Date.now();
|
|
145
239
|
if (turn.hasAudio) return;
|
|
146
|
-
|
|
147
|
-
logger$
|
|
240
|
+
turn.hasAudio = true;
|
|
241
|
+
logger$5.info(`discord voice: realtime speaker turn opened guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} user=${turn.context.userId} speaker=${turn.context.speakerLabel} owner=${turn.context.senderIsOwner}`);
|
|
148
242
|
}
|
|
149
243
|
logSpeakerTurnClosed(turn) {
|
|
150
244
|
if (turn.closed || !turn.hasAudio) return;
|
|
151
245
|
const elapsedMs = Date.now() - turn.startedAt;
|
|
152
246
|
const sinceLastAudioMs = turn.lastAudioAt ? Date.now() - turn.lastAudioAt : void 0;
|
|
153
|
-
logger$
|
|
247
|
+
logger$5.info(`discord voice: realtime speaker turn closed guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} user=${turn.context.userId} speaker=${turn.context.speakerLabel} owner=${turn.context.senderIsOwner} hasAudio=${turn.hasAudio} chunks=${turn.inputChunks} discordBytes=${turn.inputDiscordBytes} realtimeBytes=${turn.inputRealtimeBytes} elapsedMs=${elapsedMs}${sinceLastAudioMs === void 0 ? "" : ` sinceLastAudioMs=${sinceLastAudioMs}`} interruptedPlayback=${turn.interruptedPlayback}`);
|
|
154
248
|
}
|
|
155
249
|
sendRealtimeTrailingSilenceForTurn(turn) {
|
|
156
250
|
const bridge = this.params.bridge();
|
|
@@ -161,7 +255,7 @@ var DiscordRealtimeTurns = class {
|
|
|
161
255
|
const silenceBytes = Math.ceil(24e3 * silenceMs / 1e3) * REALTIME_PCM16_BYTES_PER_SAMPLE;
|
|
162
256
|
const silence = Buffer.alloc(silenceBytes);
|
|
163
257
|
bridge.sendAudio(silence);
|
|
164
|
-
logger$
|
|
258
|
+
logger$5.info(`discord voice: realtime trailing silence sent guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} user=${turn.context.userId} speaker=${turn.context.speakerLabel} silenceMs=${silenceMs} realtimeBytes=${silence.length}`);
|
|
165
259
|
}
|
|
166
260
|
resolveWakeNameTranscript(text, requireWakeName) {
|
|
167
261
|
if (!requireWakeName) return {
|
|
@@ -174,7 +268,7 @@ var DiscordRealtimeTurns = class {
|
|
|
174
268
|
};
|
|
175
269
|
const wakeNameResult = matchRealtimeVoiceActivationName(text, this.params.wakeNames());
|
|
176
270
|
if (wakeNameResult) {
|
|
177
|
-
logger$
|
|
271
|
+
logger$5.info(`discord voice: realtime wake-name gate matched canonical=${wakeNameResult.activationName} heard=${wakeNameResult.heardName} match=${wakeNameResult.match} voiceSession=${this.params.entry.voiceSessionKey} agent=${this.params.entry.route.agentId}`);
|
|
178
272
|
return wakeNameResult;
|
|
179
273
|
}
|
|
180
274
|
return {
|
|
@@ -216,14 +310,14 @@ var DiscordRealtimeTurns = class {
|
|
|
216
310
|
if (providerEpoch !== this.params.providerEpoch() || this.params.entry.transcripts !== transcripts) return;
|
|
217
311
|
return transcripts.onUtterance(utterance);
|
|
218
312
|
}).catch((error) => {
|
|
219
|
-
logger$
|
|
313
|
+
logger$5.warn(`discord voice: realtime transcripts utterance failed: ${formatErrorMessage(error)}`);
|
|
220
314
|
});
|
|
221
315
|
}
|
|
222
316
|
armWakeNameFollowup() {
|
|
223
|
-
const turn = this.
|
|
317
|
+
const turn = this.lastSpeakerTurn;
|
|
224
318
|
const context = this.consumePendingSpeakerContext();
|
|
225
319
|
if (!context) {
|
|
226
|
-
logger$
|
|
320
|
+
logger$5.warn(`discord voice: realtime wake-name follow-up has no speaker context voiceSession=${this.params.entry.voiceSessionKey} agent=${this.params.entry.route.agentId}`);
|
|
227
321
|
return;
|
|
228
322
|
}
|
|
229
323
|
const expiresAt = resolveExpiresAtMsFromDurationMs(DISCORD_REALTIME_WAKE_NAME_FOLLOWUP_TTL_MS);
|
|
@@ -234,7 +328,7 @@ var DiscordRealtimeTurns = class {
|
|
|
234
328
|
startedAt: turn?.startedAt ?? Date.now(),
|
|
235
329
|
expiresAt
|
|
236
330
|
};
|
|
237
|
-
logger$
|
|
331
|
+
logger$5.info(`discord voice: realtime wake-name follow-up armed speaker=${context.speakerLabel} voiceSession=${this.params.entry.voiceSessionKey} agent=${this.params.entry.route.agentId}`);
|
|
238
332
|
}
|
|
239
333
|
consumePendingWakeNameFollowup() {
|
|
240
334
|
const pending = this.pendingWakeNameFollowup;
|
|
@@ -242,25 +336,20 @@ var DiscordRealtimeTurns = class {
|
|
|
242
336
|
const now = asDateTimestampMs(Date.now());
|
|
243
337
|
const expiresAt = pending ? asDateTimestampMs(pending.expiresAt) : void 0;
|
|
244
338
|
if (!pending || now === void 0 || expiresAt === void 0 || now > expiresAt) return;
|
|
245
|
-
|
|
246
|
-
if (currentTurn && currentTurn.context.userId !== pending.context.userId) return;
|
|
247
|
-
if (currentTurn) this.consumePendingSpeakerContext();
|
|
339
|
+
this.consumePendingSpeakerContext();
|
|
248
340
|
return {
|
|
249
341
|
context: pending.context,
|
|
250
342
|
startedAt: pending.startedAt,
|
|
251
343
|
transcripts: pending.transcripts
|
|
252
344
|
};
|
|
253
345
|
}
|
|
254
|
-
rememberIgnoredWakeNameSpeakerContext(context) {
|
|
255
|
-
this.speakerTurns.rememberIgnoredContext(context);
|
|
256
|
-
}
|
|
257
346
|
};
|
|
258
347
|
function isDiscordRealtimeSpeakerContext(value) {
|
|
259
348
|
return Boolean(value) && typeof value === "object" && typeof value.userId === "string" && typeof value.senderIsOwner === "boolean" && typeof value.speakerLabel === "string";
|
|
260
349
|
}
|
|
261
350
|
//#endregion
|
|
262
351
|
//#region extensions/discord/src/voice/realtime-consults.ts
|
|
263
|
-
const logger$
|
|
352
|
+
const logger$4 = createSubsystemLogger("discord/voice");
|
|
264
353
|
const DISCORD_REALTIME_TALKBACK_DEBOUNCE_MS = 350;
|
|
265
354
|
const DISCORD_REALTIME_FALLBACK_TEXT = "I hit an error while checking that. Please try again.";
|
|
266
355
|
const DISCORD_REALTIME_FORCED_CONSULT_FALLBACK_DELAY_MS = 200;
|
|
@@ -279,6 +368,9 @@ var DiscordRealtimeConsults = class {
|
|
|
279
368
|
this.talkback.close();
|
|
280
369
|
this.clearProviderConsultState();
|
|
281
370
|
}
|
|
371
|
+
isIdle() {
|
|
372
|
+
return this.talkback.isIdle() && this.providerDeliveries.size === 0 && this.params.harness.forcedConsults.handles().every((handle) => handle.context?.result);
|
|
373
|
+
}
|
|
282
374
|
resetProviderContinuity() {
|
|
283
375
|
this.talkback.close();
|
|
284
376
|
this.talkback = this.createTalkbackQueue();
|
|
@@ -287,6 +379,10 @@ var DiscordRealtimeConsults = class {
|
|
|
287
379
|
async handleToolCall(event, session) {
|
|
288
380
|
const providerEpoch = this.params.providerEpoch();
|
|
289
381
|
const callId = event.callId || event.itemId || "unknown";
|
|
382
|
+
if (this.params.stopped() || !this.params.turns.speakerContext()) {
|
|
383
|
+
await session.submitToolResult(callId, { error: "No Discord speaker context available" });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
290
386
|
if (event.name === REALTIME_VOICE_AGENT_CONTROL_TOOL_NAME) {
|
|
291
387
|
await this.handleAgentControlToolCall(event, session, callId, providerEpoch);
|
|
292
388
|
return;
|
|
@@ -302,16 +398,16 @@ var DiscordRealtimeConsults = class {
|
|
|
302
398
|
const outcome = classifyRealtimeVoiceConsultToolCall(event.args, { retainedExactSpeechTexts: this.params.playback.retainedExactSpeechTexts() });
|
|
303
399
|
switch (outcome.kind) {
|
|
304
400
|
case "exact-speech-echo":
|
|
305
|
-
logger$
|
|
401
|
+
logger$4.info(`discord voice: realtime exact speech consult bypassed call=${callId || "unknown"} answerChars=${outcome.text.length}`);
|
|
306
402
|
await session.submitToolResult(callId, { text: outcome.text });
|
|
307
403
|
return;
|
|
308
404
|
case "malformed":
|
|
309
|
-
logger$
|
|
405
|
+
logger$4.warn(`discord voice: realtime consult rejected malformed args call=${callId || "unknown"}: ${outcome.error}`);
|
|
310
406
|
await session.submitToolResult(callId, { error: outcome.error });
|
|
311
407
|
return;
|
|
312
408
|
}
|
|
313
409
|
const consultMessage = outcome.message;
|
|
314
|
-
logger$
|
|
410
|
+
logger$4.info(`discord voice: realtime consult requested call=${callId || "unknown"} voiceSession=${this.params.entry.voiceSessionKey} supervisorSession=${this.params.entry.route.sessionKey} agent=${this.params.entry.route.agentId} question=${formatVoiceLogPreview(consultMessage)}`);
|
|
315
411
|
const nativeConsult = this.params.harness.forcedConsults.recordNativeConsult(event.args, callId);
|
|
316
412
|
if (nativeConsult.kind === "already_delivered" && this.params.harness.forcedConsults.isCancelled(nativeConsult.handle)) {
|
|
317
413
|
await this.submitTerminalRealtimeToolResult(callId, session, CANCELLED_CONSULT_RESULT);
|
|
@@ -326,7 +422,7 @@ var DiscordRealtimeConsults = class {
|
|
|
326
422
|
if (recentConsult) {
|
|
327
423
|
const recentSpeaker = recentConsult.context?.speaker;
|
|
328
424
|
if (this.params.turns.hasPendingSpeakerAudioContext()) {
|
|
329
|
-
logger$
|
|
425
|
+
logger$4.info(`discord voice: realtime consult matched recent agent result but newer speaker audio is pending call=${callId} speaker=${recentSpeaker?.speakerLabel ?? "unknown"} owner=${recentSpeaker?.senderIsOwner ?? false}`);
|
|
330
426
|
await session.submitToolResult(callId, { error: "Discord speaker context changed before this realtime consult completed" });
|
|
331
427
|
return;
|
|
332
428
|
}
|
|
@@ -341,7 +437,7 @@ var DiscordRealtimeConsults = class {
|
|
|
341
437
|
});
|
|
342
438
|
}
|
|
343
439
|
if (!context) {
|
|
344
|
-
logger$
|
|
440
|
+
logger$4.warn(`discord voice: realtime consult has no speaker context call=${callId || "unknown"}`);
|
|
345
441
|
await session.submitToolResult(callId, { error: "No Discord speaker context available" });
|
|
346
442
|
return;
|
|
347
443
|
}
|
|
@@ -350,8 +446,8 @@ var DiscordRealtimeConsults = class {
|
|
|
350
446
|
message: consultMessage
|
|
351
447
|
}));
|
|
352
448
|
if (providerEpoch !== this.params.providerEpoch()) return;
|
|
353
|
-
if ("text" in result) logger$
|
|
354
|
-
else if ("error" in result) logger$
|
|
449
|
+
if ("text" in result) logger$4.info(`discord voice: realtime consult answer (${result.text.length} chars) voiceSession=${this.params.entry.voiceSessionKey} supervisorSession=${this.params.entry.route.sessionKey} agent=${this.params.entry.route.agentId} speaker=${context.speakerLabel} owner=${context.senderIsOwner}: ${formatVoiceLogPreview(result.text)}`);
|
|
450
|
+
else if ("error" in result) logger$4.warn(`discord voice: realtime consult failed call=${callId}: ${result.error}`);
|
|
355
451
|
await this.submitAgentProxyConsultResult(callId, session, result);
|
|
356
452
|
}
|
|
357
453
|
async handleAcceptedTranscript(acceptedText, forcedSpeakerContext, providerEpoch) {
|
|
@@ -366,7 +462,7 @@ var DiscordRealtimeConsults = class {
|
|
|
366
462
|
});
|
|
367
463
|
} catch (error) {
|
|
368
464
|
if (providerEpoch !== this.params.providerEpoch()) return;
|
|
369
|
-
logger$
|
|
465
|
+
logger$4.warn(`discord voice: realtime active-run control failed; falling back to normal transcript handling: ${formatErrorMessage(error)}`);
|
|
370
466
|
control = void 0;
|
|
371
467
|
}
|
|
372
468
|
if (providerEpoch !== this.params.providerEpoch()) return;
|
|
@@ -388,7 +484,7 @@ var DiscordRealtimeConsults = class {
|
|
|
388
484
|
return createRealtimeVoiceAgentTalkbackQueue({
|
|
389
485
|
debounceMs: this.params.debounceMs() ?? DISCORD_REALTIME_TALKBACK_DEBOUNCE_MS,
|
|
390
486
|
isStopped: () => this.params.stopped() || providerEpoch !== this.params.providerEpoch(),
|
|
391
|
-
logger: logger$
|
|
487
|
+
logger: logger$4,
|
|
392
488
|
logPrefix: "[discord] realtime agent",
|
|
393
489
|
responseStyle: "Brief, natural spoken answer for a Discord voice channel.",
|
|
394
490
|
fallbackText: DISCORD_REALTIME_FALLBACK_TEXT,
|
|
@@ -431,7 +527,7 @@ var DiscordRealtimeConsults = class {
|
|
|
431
527
|
});
|
|
432
528
|
}
|
|
433
529
|
logAgentControlResult(result) {
|
|
434
|
-
logger$
|
|
530
|
+
logger$4.info(`discord voice: realtime active-run control handled mode=${result.mode} ok=${result.ok} active=${result.active} reason=${result.reason ?? "none"} voiceSession=${this.params.entry.voiceSessionKey} supervisorSession=${this.params.entry.route.sessionKey} agent=${this.params.entry.route.agentId}`);
|
|
435
531
|
}
|
|
436
532
|
prepareForcedAgentProxyConsult(transcript, speakerContext) {
|
|
437
533
|
if (this.params.consultPolicy() !== "always" && this.params.wakeNamePolicy() === "never") return;
|
|
@@ -440,18 +536,17 @@ var DiscordRealtimeConsults = class {
|
|
|
440
536
|
const skipReason = classifySkippableRealtimeVoiceConsultTranscript(question);
|
|
441
537
|
if (skipReason) {
|
|
442
538
|
const context = this.params.turns.consumePendingSpeakerContext();
|
|
443
|
-
logger$
|
|
539
|
+
logger$4.info(`discord voice: realtime forced agent consult skipped reason=${skipReason} chars=${question.length} speaker=${context?.speakerLabel ?? "unknown"} transcript=${formatVoiceLogPreview(question)}`);
|
|
444
540
|
return;
|
|
445
541
|
}
|
|
446
|
-
|
|
447
|
-
if (!context) context = this.params.turns.consumeRecentIgnoredWakeNameSpeakerContext();
|
|
542
|
+
const context = speakerContext ?? this.params.turns.consumePendingSpeakerContext();
|
|
448
543
|
if (!context) {
|
|
449
544
|
const recent = this.findRecentAgentProxyConsultContext(question);
|
|
450
545
|
if (recent) {
|
|
451
546
|
logVoiceVerbose(`realtime forced agent consult skipped (already delegated): guild ${this.params.entry.guildId} channel ${this.params.entry.channelId} speaker ${recent.context?.speaker.userId ?? "unknown"}`);
|
|
452
547
|
return;
|
|
453
548
|
}
|
|
454
|
-
logger$
|
|
549
|
+
logger$4.warn("discord voice: realtime forced agent consult has no speaker context");
|
|
455
550
|
return;
|
|
456
551
|
}
|
|
457
552
|
return this.params.harness.forcedConsults.prepare(question, { context: {
|
|
@@ -476,9 +571,9 @@ var DiscordRealtimeConsults = class {
|
|
|
476
571
|
return;
|
|
477
572
|
}
|
|
478
573
|
const startedAt = Date.now();
|
|
479
|
-
logger$
|
|
480
|
-
logger$
|
|
481
|
-
if (this.params.playback.hasInterruptibleOutputAudio()) logger$
|
|
574
|
+
logger$4.info(`discord voice: realtime forced agent consult starting chars=${question.length} voiceSession=${this.params.entry.voiceSessionKey} supervisorSession=${this.params.entry.route.sessionKey} agent=${this.params.entry.route.agentId} speaker=${context.speakerLabel} owner=${context.senderIsOwner}`);
|
|
575
|
+
logger$4.debug(`discord voice: realtime forced agent consult reason=${DISCORD_REALTIME_FORCED_CONSULT_REASON} consultPolicy=${this.params.consultPolicy()} wakeNamePolicy=${this.params.wakeNamePolicy()} requireWakeName=${this.params.isWakeNameRequired()} voiceSession=${this.params.entry.voiceSessionKey} supervisorSession=${this.params.entry.route.sessionKey} agent=${this.params.entry.route.agentId} speaker=${context.speakerLabel}`);
|
|
576
|
+
if (this.params.playback.hasInterruptibleOutputAudio()) logger$4.info(`discord voice: realtime forced agent consult preserving active playback guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} outputAudioMs=${this.params.playback.outputAudioMs()} outputActive=${this.params.playback.isOutputAudioActive()} playbackChunks=${this.params.harness.outputActivity.snapshot().chunks}`);
|
|
482
577
|
state.handledByForcedPlayback = true;
|
|
483
578
|
const result = await this.trackAgentProxyConsult(pending, this.runAgentTurn({
|
|
484
579
|
context,
|
|
@@ -487,8 +582,8 @@ var DiscordRealtimeConsults = class {
|
|
|
487
582
|
const deliveries = this.providerDeliveries.get(state);
|
|
488
583
|
await Promise.all(Array.from(deliveries ?? [], (delivery) => delivery.promise));
|
|
489
584
|
if (state.providerEpoch !== this.params.providerEpoch() || "status" in result) return;
|
|
490
|
-
if ("text" in result) logger$
|
|
491
|
-
else logger$
|
|
585
|
+
if ("text" in result) logger$4.info(`discord voice: realtime forced agent consult answer (${result.text.length} chars) elapsedMs=${Date.now() - startedAt} voiceSession=${this.params.entry.voiceSessionKey} supervisorSession=${this.params.entry.route.sessionKey} agent=${this.params.entry.route.agentId}: ${formatVoiceLogPreview(result.text)}`);
|
|
586
|
+
else logger$4.warn(`discord voice: realtime forced agent consult failed elapsedMs=${Date.now() - startedAt}: ${result.error}`);
|
|
492
587
|
const text = "text" in result ? result.text : DISCORD_REALTIME_FALLBACK_TEXT;
|
|
493
588
|
if (text.trim() && state.handledByForcedPlayback) this.params.playback.enqueueExactSpeechMessage(text);
|
|
494
589
|
}
|
|
@@ -540,7 +635,7 @@ var DiscordRealtimeConsults = class {
|
|
|
540
635
|
deliveries.add(providerDelivery);
|
|
541
636
|
this.providerDeliveries.set(state, deliveries);
|
|
542
637
|
}
|
|
543
|
-
logger$
|
|
638
|
+
logger$4.info(`discord voice: realtime consult ${state.result ? "reused recent" : "joined in-flight"} agent result call=${callId} speaker=${state.speaker.speakerLabel} owner=${state.speaker.senderIsOwner}`);
|
|
544
639
|
try {
|
|
545
640
|
const result = await pendingResult;
|
|
546
641
|
if (state.providerEpoch !== this.params.providerEpoch()) return true;
|
|
@@ -574,11 +669,223 @@ var DiscordRealtimeConsults = class {
|
|
|
574
669
|
}
|
|
575
670
|
};
|
|
576
671
|
//#endregion
|
|
672
|
+
//#region extensions/discord/src/voice/realtime-output.ts
|
|
673
|
+
const logger$3 = createSubsystemLogger("discord/voice");
|
|
674
|
+
const DISCORD_RAW_PCM_FRAME_BYTES = 3840;
|
|
675
|
+
const DISCORD_RAW_PCM_BYTES_PER_MS = 192;
|
|
676
|
+
const DISCORD_REALTIME_OUTPUT_PREROLL_FRAMES = 25;
|
|
677
|
+
const DISCORD_REALTIME_OUTPUT_PLAYBACK_WATCHDOG_MARGIN_MS = 3e3;
|
|
678
|
+
/** One output stream retains ownership through queued and audible playback. */
|
|
679
|
+
var DiscordRealtimeOutput = class {
|
|
680
|
+
constructor(params) {
|
|
681
|
+
this.params = params;
|
|
682
|
+
this.activity = createRealtimeVoiceOutputActivityTracker();
|
|
683
|
+
this.stream = new PassThrough({ highWaterMark: DISCORD_RAW_PCM_FRAME_BYTES * 128 });
|
|
684
|
+
this.playbackMarks = [];
|
|
685
|
+
this.playedPcmBytes = 0;
|
|
686
|
+
this.reportedPlaybackMs = 0;
|
|
687
|
+
this.audioSpans = [];
|
|
688
|
+
this.buffers = [];
|
|
689
|
+
this.bufferedBytes = 0;
|
|
690
|
+
this.closed = false;
|
|
691
|
+
this.failed = false;
|
|
692
|
+
this.stream.once("close", () => {
|
|
693
|
+
if (!this.activity.snapshot().playbackStarted) this.close("stream-close");
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
pendingBytes() {
|
|
697
|
+
return this.closed ? 0 : this.bufferedBytes + this.stream.writableLength + this.stream.readableLength;
|
|
698
|
+
}
|
|
699
|
+
playbackItems() {
|
|
700
|
+
const playedMs = Math.min(this.playedPcmBytes / DISCORD_RAW_PCM_BYTES_PER_MS, this.activity.snapshot().audioMs);
|
|
701
|
+
for (const span of this.audioSpans) span.item.audioEndMs += Math.max(0, Math.min(playedMs, span.endMs) - Math.max(this.reportedPlaybackMs, span.startMs));
|
|
702
|
+
this.reportedPlaybackMs = playedMs;
|
|
703
|
+
return this.audioSpans.filter(({ endMs }) => endMs > playedMs).map(({ item }) => item);
|
|
704
|
+
}
|
|
705
|
+
markPlayback(acknowledge) {
|
|
706
|
+
if (!this.closed) this.playbackMarks.push({
|
|
707
|
+
endBytes: this.activity.snapshot().sinkAudioBytes,
|
|
708
|
+
acknowledge
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
append(pcm, activity, item) {
|
|
712
|
+
if (this.closed) return;
|
|
713
|
+
const startMs = this.activity.snapshot().audioMs;
|
|
714
|
+
if (item) {
|
|
715
|
+
const previous = this.audioSpans.at(-1);
|
|
716
|
+
const endMs = startMs + activity.audioMs;
|
|
717
|
+
if (previous?.item === item && previous.endMs === startMs) previous.endMs = endMs;
|
|
718
|
+
else this.audioSpans.push({
|
|
719
|
+
item,
|
|
720
|
+
startMs,
|
|
721
|
+
endMs
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
this.activity.markAudio(activity);
|
|
725
|
+
if (this.activity.snapshot().playbackStarted && !this.drainHandler) {
|
|
726
|
+
if (!this.stream.write(pcm)) this.waitForDrain();
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
this.buffers.push(pcm);
|
|
730
|
+
this.bufferedBytes += pcm.length;
|
|
731
|
+
if (!this.drainHandler && this.bufferedBytes >= DISCORD_RAW_PCM_FRAME_BYTES * DISCORD_REALTIME_OUTPUT_PREROLL_FRAMES) this.startPlayback();
|
|
732
|
+
}
|
|
733
|
+
finish(reason, playBuffered) {
|
|
734
|
+
if (this.closed) return;
|
|
735
|
+
this.activity.markStreamEnding();
|
|
736
|
+
const activity = this.activity.snapshot();
|
|
737
|
+
logger$3.info(`discord voice: realtime audio playback finishing reason=${reason} ${this.params.logContext} audioMs=${Math.floor(activity.audioMs)} chunks=${activity.chunks}`);
|
|
738
|
+
if (!playBuffered) {
|
|
739
|
+
this.close(reason);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
this.startPlayback();
|
|
743
|
+
if (this.activity.snapshot().playbackStarted) {
|
|
744
|
+
this.scheduleWatchdog(reason);
|
|
745
|
+
if (!this.drainHandler) this.stream.end();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
close(reason) {
|
|
749
|
+
if (this.closed) return;
|
|
750
|
+
const playbackRetirement = reason === "player-idle" || reason === "output-pipeline-error" || reason === "playback-watchdog";
|
|
751
|
+
const heardMarks = playbackRetirement ? this.playbackMarks.filter((mark) => mark.endBytes <= this.playedPcmBytes) : [];
|
|
752
|
+
const lostPlaybackMarks = playbackRetirement && this.playbackMarks.some((mark) => mark.endBytes > this.playedPcmBytes);
|
|
753
|
+
this.closed = true;
|
|
754
|
+
this.playbackMarks = [];
|
|
755
|
+
this.playbackItems();
|
|
756
|
+
this.clearWatchdog();
|
|
757
|
+
const activity = this.activity.snapshot();
|
|
758
|
+
logger$3.info(`discord voice: realtime audio playback stopped reason=${reason} ${this.params.logContext} audioMs=${Math.floor(activity.audioMs)} elapsedMs=${this.activity.elapsedPlaybackMs()} chunks=${activity.chunks} discordBytes=${activity.sinkAudioBytes} realtimeBytes=${activity.sourceAudioBytes}`);
|
|
759
|
+
this.buffers = [];
|
|
760
|
+
this.bufferedBytes = 0;
|
|
761
|
+
if (this.drainHandler) {
|
|
762
|
+
this.stream.off("drain", this.drainHandler);
|
|
763
|
+
this.drainHandler = void 0;
|
|
764
|
+
}
|
|
765
|
+
this.stream.end();
|
|
766
|
+
this.stream.destroy();
|
|
767
|
+
if (lostPlaybackMarks) this.params.onError(/* @__PURE__ */ new Error(`Discord realtime audio stopped before playback completed: ${reason}`));
|
|
768
|
+
this.params.onClose(this, reason);
|
|
769
|
+
if (this.request) {
|
|
770
|
+
this.params.player.cancel(this.request);
|
|
771
|
+
this.request = void 0;
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
for (const mark of heardMarks) mark.acknowledge();
|
|
775
|
+
} catch (error) {
|
|
776
|
+
this.params.onError(error);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
startPlayback() {
|
|
780
|
+
if (this.closed || this.request) return;
|
|
781
|
+
this.request = {
|
|
782
|
+
createResource: () => this.createResource(),
|
|
783
|
+
onStart: () => {
|
|
784
|
+
this.activity.markPlaybackStarted();
|
|
785
|
+
this.params.onStart();
|
|
786
|
+
if (this.activity.snapshot().streamEnding) {
|
|
787
|
+
this.scheduleWatchdog("player-start");
|
|
788
|
+
if (!this.drainHandler) this.stream.end();
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
onIdle: () => this.close(this.failed ? "output-pipeline-error" : "player-idle"),
|
|
792
|
+
onBargeIn: this.params.onBargeIn,
|
|
793
|
+
onError: this.params.onError
|
|
794
|
+
};
|
|
795
|
+
this.params.player.enqueue(this.request);
|
|
796
|
+
}
|
|
797
|
+
createResource() {
|
|
798
|
+
const voiceSdk = loadDiscordVoiceSdk();
|
|
799
|
+
const opusStream = createDiscordOpusEncodeStream();
|
|
800
|
+
opusStream.once("error", () => {
|
|
801
|
+
this.failed = true;
|
|
802
|
+
});
|
|
803
|
+
pipeline(this.stream, opusStream, (error) => {
|
|
804
|
+
if (!error || this.closed) return;
|
|
805
|
+
logger$3.warn(`discord voice: realtime output pipeline failed ${this.params.logContext}: ${formatErrorMessage(error)}`);
|
|
806
|
+
this.close("output-pipeline-error");
|
|
807
|
+
});
|
|
808
|
+
const buffered = Buffer.concat(this.buffers, this.bufferedBytes);
|
|
809
|
+
this.buffers = [];
|
|
810
|
+
this.bufferedBytes = 0;
|
|
811
|
+
if (buffered.length > 0 && !this.stream.write(buffered)) this.waitForDrain();
|
|
812
|
+
const resource = voiceSdk.createAudioResource(opusStream, { inputType: voiceSdk.StreamType.Opus });
|
|
813
|
+
const read = resource.read.bind(resource);
|
|
814
|
+
resource.read = () => {
|
|
815
|
+
let packet;
|
|
816
|
+
try {
|
|
817
|
+
packet = read();
|
|
818
|
+
if (!packet && opusStream.flushPartialFrame()) packet = read();
|
|
819
|
+
if (packet) this.playedPcmBytes += opusStream.takePcmBytes(packet);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
opusStream.destroy(error instanceof Error ? error : new Error(formatErrorMessage(error)));
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
if (this.playbackMarks.length > 0) queueMicrotask(() => this.acknowledgePlayedMarks());
|
|
825
|
+
return packet;
|
|
826
|
+
};
|
|
827
|
+
return resource;
|
|
828
|
+
}
|
|
829
|
+
acknowledgePlayedMarks() {
|
|
830
|
+
try {
|
|
831
|
+
while (!this.closed) {
|
|
832
|
+
const mark = this.playbackMarks[0];
|
|
833
|
+
if (!mark || mark.endBytes > this.playedPcmBytes) return;
|
|
834
|
+
this.playbackMarks.shift();
|
|
835
|
+
mark.acknowledge();
|
|
836
|
+
}
|
|
837
|
+
} catch (error) {
|
|
838
|
+
this.params.onError(error);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
waitForDrain() {
|
|
842
|
+
if (this.drainHandler || this.closed) return;
|
|
843
|
+
logger$3.info(`discord voice: realtime audio playback buffering ${this.params.logContext} bufferedBytes=${this.stream.writableLength + this.stream.readableLength}`);
|
|
844
|
+
this.drainHandler = () => {
|
|
845
|
+
this.drainHandler = void 0;
|
|
846
|
+
if (this.closed) return;
|
|
847
|
+
let refreshWatchdog = this.activity.snapshot().streamEnding;
|
|
848
|
+
while (this.buffers.length > 0) {
|
|
849
|
+
const buffered = this.buffers.shift();
|
|
850
|
+
if (!buffered) break;
|
|
851
|
+
this.bufferedBytes -= buffered.length;
|
|
852
|
+
const writable = this.stream.write(buffered);
|
|
853
|
+
if (refreshWatchdog) {
|
|
854
|
+
this.scheduleWatchdog("output-drain");
|
|
855
|
+
refreshWatchdog = false;
|
|
856
|
+
}
|
|
857
|
+
if (!writable) {
|
|
858
|
+
this.waitForDrain();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (this.activity.snapshot().streamEnding) this.stream.end();
|
|
863
|
+
};
|
|
864
|
+
this.stream.once("drain", this.drainHandler);
|
|
865
|
+
}
|
|
866
|
+
scheduleWatchdog(reason) {
|
|
867
|
+
this.clearWatchdog();
|
|
868
|
+
const timeoutMs = this.activity.playbackWatchdogDelayMs({
|
|
869
|
+
marginMs: DISCORD_REALTIME_OUTPUT_PLAYBACK_WATCHDOG_MARGIN_MS,
|
|
870
|
+
minMs: DISCORD_REALTIME_OUTPUT_PLAYBACK_WATCHDOG_MARGIN_MS
|
|
871
|
+
});
|
|
872
|
+
if (timeoutMs === void 0) return;
|
|
873
|
+
this.watchdog = setTimeout(() => {
|
|
874
|
+
this.watchdog = void 0;
|
|
875
|
+
logger$3.warn(`discord voice: realtime audio playback watchdog fired reason=${reason} ${this.params.logContext} audioMs=${Math.floor(this.activity.snapshot().audioMs)} elapsedMs=${this.activity.elapsedPlaybackMs()}`);
|
|
876
|
+
this.close("playback-watchdog");
|
|
877
|
+
}, timeoutMs);
|
|
878
|
+
}
|
|
879
|
+
clearWatchdog() {
|
|
880
|
+
clearTimeout(this.watchdog);
|
|
881
|
+
this.watchdog = void 0;
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
//#endregion
|
|
577
885
|
//#region extensions/discord/src/voice/realtime-playback.ts
|
|
578
|
-
const logger$
|
|
886
|
+
const logger$2 = createSubsystemLogger("discord/voice");
|
|
579
887
|
const DISCORD_REALTIME_CONTROL_SPEECH_DEDUPE_MS = 5e3;
|
|
580
|
-
const
|
|
581
|
-
const DISCORD_REALTIME_MAX_RETAINED_EXACT_SPEECH_MESSAGES = 32;
|
|
888
|
+
const DISCORD_REALTIME_MAX_RETAINED_RESPONSES = 32;
|
|
582
889
|
const DISCORD_REALTIME_MAX_RETAINED_EXACT_SPEECH_BYTES = 32768;
|
|
583
890
|
const DISCORD_REALTIME_WAKE_ACKS = [
|
|
584
891
|
"Yeah.",
|
|
@@ -586,49 +893,45 @@ const DISCORD_REALTIME_WAKE_ACKS = [
|
|
|
586
893
|
"Got it.",
|
|
587
894
|
"One sec."
|
|
588
895
|
];
|
|
589
|
-
const
|
|
590
|
-
const DISCORD_REALTIME_OUTPUT_PREROLL_FRAMES = 25;
|
|
591
|
-
const DISCORD_REALTIME_MAX_PENDING_OUTPUT_BYTES = DISCORD_RAW_PCM_FRAME_BYTES * 6e3;
|
|
896
|
+
const DISCORD_REALTIME_MAX_PENDING_OUTPUT_BYTES = 2304e4;
|
|
592
897
|
function normalizeControlSpeechText(text) {
|
|
593
898
|
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
594
899
|
}
|
|
595
900
|
var DiscordRealtimePlayback = class {
|
|
596
901
|
constructor(params) {
|
|
597
902
|
this.params = params;
|
|
598
|
-
this.
|
|
599
|
-
this.
|
|
600
|
-
this.
|
|
903
|
+
this.outputs = /* @__PURE__ */ new Set();
|
|
904
|
+
this.generatingItems = /* @__PURE__ */ new Map();
|
|
905
|
+
this.responseAudio = "completed";
|
|
601
906
|
this.queuedExactSpeechMessages = [];
|
|
602
907
|
this.exactSpeechState = { status: "idle" };
|
|
603
908
|
this.wakeNameAckIndex = 0;
|
|
604
|
-
this.
|
|
605
|
-
|
|
606
|
-
this.
|
|
607
|
-
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
attachPlayer() {
|
|
611
|
-
const voiceSdk = loadDiscordVoiceSdk();
|
|
612
|
-
this.params.entry.player.on(voiceSdk.AudioPlayerStatus.Idle, this.playerIdleHandler);
|
|
909
|
+
this.unregisterPlayerLane = this.params.player.registerLane({
|
|
910
|
+
hasOutput: () => this.isOutputAudioActive(),
|
|
911
|
+
onBargeIn: (reason) => this.handleBargeIn(reason),
|
|
912
|
+
cancelForControl: () => this.cancelForControl()
|
|
913
|
+
});
|
|
613
914
|
}
|
|
614
915
|
close() {
|
|
916
|
+
this.unregisterPlayerLane();
|
|
615
917
|
this.queuedExactSpeechMessages = [];
|
|
616
918
|
this.exactSpeechState = { status: "idle" };
|
|
617
919
|
this.clearOutputAudio("session-close");
|
|
618
|
-
const voiceSdk = loadDiscordVoiceSdk();
|
|
619
|
-
this.params.entry.player.off(voiceSdk.AudioPlayerStatus.Idle, this.playerIdleHandler);
|
|
620
920
|
}
|
|
621
921
|
handleBargeIn(reason = "barge-in") {
|
|
622
922
|
if (!this.isBargeInEnabled()) {
|
|
623
|
-
logger$
|
|
923
|
+
logger$2.info(`discord voice: realtime barge-in ignored reason=${reason} bargeIn=false guild=${this.params.entry.guildId} channel=${this.params.entry.channelId}`);
|
|
624
924
|
return;
|
|
625
925
|
}
|
|
626
926
|
if (!this.hasInterruptibleOutputAudio()) {
|
|
627
|
-
logger$
|
|
927
|
+
logger$2.info(`discord voice: realtime barge-in ignored reason=${reason} outputActive=false guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} playbackChunks=${this.params.harness.outputActivity.snapshot().chunks}`);
|
|
628
928
|
return;
|
|
629
929
|
}
|
|
630
|
-
logger$
|
|
631
|
-
this.params.
|
|
930
|
+
logger$2.info(`discord voice: realtime barge-in requested reason=${reason} guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} outputAudioMs=${this.outputAudioMs()} outputActive=${this.isOutputAudioActive()} playbackChunks=${this.params.harness.outputActivity.snapshot().chunks}`);
|
|
931
|
+
const bridge = this.params.bridge();
|
|
932
|
+
this.params.harness.handleBargeIn({ audioPlaybackActive: true }, () => {
|
|
933
|
+
if (!bridge?.bridge.handleBargeIn) this.clearOutputAudio(reason);
|
|
934
|
+
});
|
|
632
935
|
}
|
|
633
936
|
isBargeInEnabled() {
|
|
634
937
|
if (this.params.wakeNameRequired()) return false;
|
|
@@ -641,67 +944,89 @@ var DiscordRealtimePlayback = class {
|
|
|
641
944
|
}
|
|
642
945
|
hasInterruptibleOutputAudio() {
|
|
643
946
|
this.params.bridge()?.setMediaTimestamp(this.outputAudioMs());
|
|
644
|
-
|
|
645
|
-
|
|
947
|
+
return this.isOutputAudioActive();
|
|
948
|
+
}
|
|
949
|
+
getPlaybackState() {
|
|
950
|
+
const items = /* @__PURE__ */ new Set();
|
|
951
|
+
for (const output of this.outputs) {
|
|
952
|
+
const outputItems = output.playbackItems();
|
|
953
|
+
if (outputItems.some((item) => this.generatingItems.get(item.itemId) === item)) for (const item of this.generatingItems.values()) items.add(item);
|
|
954
|
+
for (const item of outputItems) items.add(item);
|
|
955
|
+
}
|
|
956
|
+
for (const item of this.generatingItems.values()) items.add(item);
|
|
957
|
+
return Array.from(items, (item) => ({
|
|
958
|
+
...item,
|
|
959
|
+
audioEndMs: Math.floor(item.audioEndMs)
|
|
960
|
+
}));
|
|
646
961
|
}
|
|
647
|
-
|
|
962
|
+
beginResponse() {
|
|
963
|
+
if (this.responseAudio === "completed") this.responseAudio = "accepting";
|
|
964
|
+
}
|
|
965
|
+
sendOutputMark(acknowledge) {
|
|
966
|
+
if (!this.params.stopped() && this.responseAudio === "accepting") this.generatingOutput?.markPlayback(acknowledge);
|
|
967
|
+
}
|
|
968
|
+
sendOutputAudio(realtimePcm24kMono, metadata) {
|
|
648
969
|
this.params.markProviderGenerationObserved();
|
|
649
|
-
if (this.params.stopped()) return;
|
|
970
|
+
if (this.params.stopped() || this.responseAudio === "discarding") return;
|
|
650
971
|
const discordPcm = convertRealtimePcm24kMonoToDiscordPcm48kStereo(realtimePcm24kMono);
|
|
651
972
|
if (discordPcm.length === 0) return;
|
|
652
973
|
this.params.bridge()?.setMediaTimestamp(this.outputAudioMs());
|
|
653
|
-
|
|
654
|
-
|
|
974
|
+
const pendingBytes = Array.from(this.outputs).reduce((total, output) => total + output.pendingBytes(), 0);
|
|
975
|
+
if (discordPcm.length > DISCORD_REALTIME_MAX_PENDING_OUTPUT_BYTES - pendingBytes || !this.generatingOutput && this.outputs.size >= DISCORD_REALTIME_MAX_RETAINED_RESPONSES) {
|
|
976
|
+
this.stopAfterPlaybackFailure("output-audio-overflow", /* @__PURE__ */ new Error(`Discord realtime audio playback overflow: responses=${this.outputs.size} pendingBytes=${pendingBytes} incomingBytes=${discordPcm.length}`));
|
|
655
977
|
return;
|
|
656
978
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
audioStarted: true
|
|
661
|
-
};
|
|
662
|
-
this.params.harness.recordOutputAudio(realtimePcm24kMono, {
|
|
979
|
+
this.beginResponse();
|
|
980
|
+
const output = this.generatingOutput ?? this.createOutput();
|
|
981
|
+
const activity = {
|
|
663
982
|
audioMs: realtimeVoiceAudioDurationMs(REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ, realtimePcm24kMono.byteLength),
|
|
664
983
|
sourceAudioBytes: realtimePcm24kMono.length,
|
|
665
984
|
sinkAudioBytes: discordPcm.length
|
|
666
|
-
}
|
|
667
|
-
|
|
985
|
+
};
|
|
986
|
+
let item;
|
|
987
|
+
if (metadata) {
|
|
988
|
+
item = this.generatingItems.get(metadata.itemId);
|
|
989
|
+
if (!item) {
|
|
990
|
+
item = {
|
|
991
|
+
itemId: metadata.itemId,
|
|
992
|
+
audioEndMs: 0
|
|
993
|
+
};
|
|
994
|
+
this.generatingItems.set(metadata.itemId, item);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
this.params.harness.recordOutputAudio(realtimePcm24kMono, activity);
|
|
998
|
+
output.append(discordPcm, activity, item);
|
|
668
999
|
}
|
|
669
1000
|
clearOutputAudio(reason = "clear") {
|
|
670
|
-
this.
|
|
671
|
-
this.
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
this.params.harness.outputActivity.
|
|
677
|
-
|
|
678
|
-
if (playBuffered) {
|
|
679
|
-
this.startOutputPlayback(stream);
|
|
680
|
-
this.scheduleOutputPlaybackWatchdog(reason, stream);
|
|
681
|
-
} else {
|
|
682
|
-
this.resetOutputStream(reason);
|
|
683
|
-
this.params.entry.player.stop(true);
|
|
684
|
-
this.completeExactSpeechResponse(reason);
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
687
|
-
if (!this.outputDrainHandler) stream.end();
|
|
1001
|
+
if (this.responseAudio === "accepting") this.responseAudio = "discarding";
|
|
1002
|
+
this.generatingOutput = void 0;
|
|
1003
|
+
this.generatingItems.clear();
|
|
1004
|
+
const outputs = Array.from(this.outputs);
|
|
1005
|
+
this.outputs.clear();
|
|
1006
|
+
for (const output of outputs.toReversed()) output.close(reason);
|
|
1007
|
+
this.params.harness.outputActivity.reset();
|
|
1008
|
+
this.completeExactSpeechResponse(reason);
|
|
688
1009
|
}
|
|
689
1010
|
handleResponseDone(outcome) {
|
|
690
|
-
|
|
691
|
-
this.
|
|
1011
|
+
const output = this.generatingOutput;
|
|
1012
|
+
this.generatingOutput = void 0;
|
|
1013
|
+
this.generatingItems.clear();
|
|
1014
|
+
this.responseAudio = "completed";
|
|
1015
|
+
output?.finish(outcome.status, outcome.status === "completed");
|
|
1016
|
+
this.completeExactSpeechResponse(outcome.status);
|
|
692
1017
|
}
|
|
693
1018
|
enqueueExactSpeechMessage(text) {
|
|
694
1019
|
if (this.params.stopped() || !text.trim()) return;
|
|
695
1020
|
const retainedMessages = this.queuedExactSpeechMessages.length + (this.exactSpeechState.status === "active" ? 1 : 0);
|
|
696
1021
|
const retainedBytes = this.queuedExactSpeechMessages.reduce((total, message) => total + Buffer.byteLength(message, "utf8"), 0) + Buffer.byteLength(this.exactSpeechState.status === "active" ? this.exactSpeechState.message : "", "utf8");
|
|
697
1022
|
const incomingBytes = Buffer.byteLength(text, "utf8");
|
|
698
|
-
if (retainedMessages >=
|
|
699
|
-
this.
|
|
1023
|
+
if (retainedMessages >= DISCORD_REALTIME_MAX_RETAINED_RESPONSES || retainedBytes + incomingBytes > DISCORD_REALTIME_MAX_RETAINED_EXACT_SPEECH_BYTES) {
|
|
1024
|
+
this.stopAfterPlaybackFailure("exact-speech-overflow", /* @__PURE__ */ new Error(`Discord realtime exact speech overflow: retained=${retainedMessages} retainedBytes=${retainedBytes} incomingBytes=${incomingBytes}`));
|
|
700
1025
|
return;
|
|
701
1026
|
}
|
|
702
|
-
if (!this.params.bridgeReady() || this.exactSpeechState.status === "active" || this.hasInterruptibleOutputAudio()) {
|
|
1027
|
+
if (!this.params.bridgeReady() || this.responseAudio !== "completed" || this.exactSpeechState.status === "active" || this.hasInterruptibleOutputAudio()) {
|
|
703
1028
|
this.queuedExactSpeechMessages.push(text);
|
|
704
|
-
logger$
|
|
1029
|
+
logger$2.info(`discord voice: realtime exact speech queued guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} queued=${this.queuedExactSpeechMessages.length} outputAudioMs=${this.outputAudioMs()} outputActive=${this.isOutputAudioActive()}`);
|
|
705
1030
|
return;
|
|
706
1031
|
}
|
|
707
1032
|
this.sendExactSpeechMessage(text);
|
|
@@ -710,32 +1035,27 @@ var DiscordRealtimePlayback = class {
|
|
|
710
1035
|
return [...this.exactSpeechState.status === "active" ? [this.exactSpeechState.message] : [], ...this.queuedExactSpeechMessages];
|
|
711
1036
|
}
|
|
712
1037
|
drainQueuedExactSpeechMessages(reason) {
|
|
713
|
-
if (this.params.stopped() || !this.params.bridgeReady() || this.exactSpeechState.status === "active" || this.queuedExactSpeechMessages.length === 0 || this.hasInterruptibleOutputAudio()) return;
|
|
1038
|
+
if (this.params.stopped() || !this.params.bridgeReady() || this.responseAudio !== "completed" || this.exactSpeechState.status === "active" || this.queuedExactSpeechMessages.length === 0 || this.hasInterruptibleOutputAudio()) return;
|
|
714
1039
|
const next = this.queuedExactSpeechMessages.shift();
|
|
715
1040
|
if (!next) return;
|
|
716
|
-
logger$
|
|
1041
|
+
logger$2.info(`discord voice: realtime exact speech dequeued reason=${reason} guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} queued=${this.queuedExactSpeechMessages.length}`);
|
|
717
1042
|
this.sendExactSpeechMessage(next);
|
|
718
1043
|
}
|
|
719
1044
|
sendWakeNameAck(result) {
|
|
720
1045
|
if (!result.allowed || this.params.stopped() || this.exactSpeechState.status === "active") return;
|
|
721
|
-
if (this.hasInterruptibleOutputAudio()) {
|
|
722
|
-
logger$
|
|
1046
|
+
if (this.params.player.isActive() || this.hasInterruptibleOutputAudio()) {
|
|
1047
|
+
logger$2.info(`discord voice: realtime wake-name ack skipped outputActive=true voiceSession=${this.params.entry.voiceSessionKey} agent=${this.params.entry.route.agentId}`);
|
|
723
1048
|
return;
|
|
724
1049
|
}
|
|
725
1050
|
const ack = DISCORD_REALTIME_WAKE_ACKS[this.wakeNameAckIndex % DISCORD_REALTIME_WAKE_ACKS.length];
|
|
726
1051
|
this.wakeNameAckIndex += 1;
|
|
727
|
-
logger$
|
|
1052
|
+
logger$2.info(`discord voice: realtime wake-name ack canonical=${result.activationName} heard=${result.heardName} match=${result.match} voiceSession=${this.params.entry.voiceSessionKey} agent=${this.params.entry.route.agentId}`);
|
|
728
1053
|
this.enqueueExactSpeechMessage(ack ?? "Yeah.");
|
|
729
1054
|
}
|
|
730
1055
|
speakControlResult(text) {
|
|
731
1056
|
const trimmed = text.trim();
|
|
732
1057
|
if (this.params.stopped() || !trimmed) return;
|
|
733
|
-
this.
|
|
734
|
-
this.completeExactSpeechResponse("active-run-control", { drain: false });
|
|
735
|
-
this.params.harness.handleBargeIn({
|
|
736
|
-
audioPlaybackActive: true,
|
|
737
|
-
force: true
|
|
738
|
-
}, () => this.clearOutputAudio("active-run-control"));
|
|
1058
|
+
this.params.player.cancelForControl();
|
|
739
1059
|
this.lastControlSpeech = {
|
|
740
1060
|
normalizedText: normalizeControlSpeechText(trimmed),
|
|
741
1061
|
sentAt: Date.now(),
|
|
@@ -743,6 +1063,15 @@ var DiscordRealtimePlayback = class {
|
|
|
743
1063
|
};
|
|
744
1064
|
this.enqueueExactSpeechMessage(trimmed);
|
|
745
1065
|
}
|
|
1066
|
+
cancelForControl() {
|
|
1067
|
+
this.queuedExactSpeechMessages = [];
|
|
1068
|
+
this.exactSpeechState = { status: "idle" };
|
|
1069
|
+
if (this.responseAudio === "completed" && this.outputs.size === 0) return;
|
|
1070
|
+
this.params.harness.handleBargeIn({
|
|
1071
|
+
audioPlaybackActive: true,
|
|
1072
|
+
force: true
|
|
1073
|
+
}, () => this.clearOutputAudio("active-run-control"));
|
|
1074
|
+
}
|
|
746
1075
|
suppressDuplicateControlSpeech(text) {
|
|
747
1076
|
const recent = this.lastControlSpeech;
|
|
748
1077
|
if (!recent) return;
|
|
@@ -753,7 +1082,7 @@ var DiscordRealtimePlayback = class {
|
|
|
753
1082
|
if (normalizeControlSpeechText(text) !== recent.normalizedText) return;
|
|
754
1083
|
recent.assistantTranscriptCount += 1;
|
|
755
1084
|
if (recent.assistantTranscriptCount <= 1) return;
|
|
756
|
-
logger$
|
|
1085
|
+
logger$2.info(`discord voice: realtime duplicate active-run control speech suppressed guild=${this.params.entry.guildId} channel=${this.params.entry.channelId}`);
|
|
757
1086
|
this.params.harness.handleBargeIn({
|
|
758
1087
|
audioPlaybackActive: true,
|
|
759
1088
|
force: true
|
|
@@ -761,168 +1090,73 @@ var DiscordRealtimePlayback = class {
|
|
|
761
1090
|
}
|
|
762
1091
|
resetProviderContinuity(reason) {
|
|
763
1092
|
this.lastControlSpeech = void 0;
|
|
764
|
-
const replayExactSpeech = this.exactSpeechState.status === "active" && !this.
|
|
1093
|
+
const replayExactSpeech = this.exactSpeechState.status === "active" && !this.exactSpeechState.output?.activity.snapshot().playbackStarted ? this.exactSpeechState.message : void 0;
|
|
765
1094
|
this.exactSpeechState = { status: "idle" };
|
|
766
1095
|
if (replayExactSpeech) this.queuedExactSpeechMessages.unshift(replayExactSpeech);
|
|
1096
|
+
this.responseAudio = "discarding";
|
|
767
1097
|
this.params.harness.flushOutput(() => this.clearOutputAudio(reason));
|
|
1098
|
+
this.responseAudio = "completed";
|
|
768
1099
|
this.params.harness.finishOutputAudio(reason);
|
|
769
1100
|
}
|
|
770
1101
|
outputAudioMs() {
|
|
771
1102
|
return Math.floor(this.params.harness.outputActivity.snapshot().audioMs);
|
|
772
1103
|
}
|
|
773
1104
|
isOutputAudioActive() {
|
|
774
|
-
return this.
|
|
775
|
-
}
|
|
776
|
-
currentOutputStream() {
|
|
777
|
-
return this.outputStream;
|
|
1105
|
+
return this.outputs.size > 0 || this.generatingItems.size > 0;
|
|
778
1106
|
}
|
|
779
|
-
|
|
1107
|
+
stopAfterPlaybackFailure(reason, error) {
|
|
780
1108
|
this.params.stopTerminally();
|
|
781
1109
|
this.queuedExactSpeechMessages = [];
|
|
782
1110
|
this.exactSpeechState = { status: "idle" };
|
|
783
1111
|
this.clearOutputAudio(reason);
|
|
784
1112
|
this.params.onTerminalError(error);
|
|
785
1113
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
const pendingBytes = this.outputPacedBytes + stream.writableLength + stream.readableLength;
|
|
802
|
-
if (discordPcm.length > DISCORD_REALTIME_MAX_PENDING_OUTPUT_BYTES - pendingBytes) {
|
|
803
|
-
this.stopAfterOverflow("output-audio-overflow", /* @__PURE__ */ new Error(`Discord realtime audio playback overflow: pendingBytes=${pendingBytes} incomingBytes=${discordPcm.length}`));
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
if (this.params.harness.outputActivity.snapshot().playbackStarted && !this.outputDrainHandler) {
|
|
807
|
-
if (!stream.write(discordPcm)) this.waitForOutputDrain(stream);
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
this.outputPacedBuffers.push(discordPcm);
|
|
811
|
-
this.outputPacedBytes += discordPcm.length;
|
|
812
|
-
if (!this.outputDrainHandler && this.outputPacedBytes >= DISCORD_RAW_PCM_FRAME_BYTES * DISCORD_REALTIME_OUTPUT_PREROLL_FRAMES) this.startOutputPlayback(stream);
|
|
813
|
-
}
|
|
814
|
-
waitForOutputDrain(stream) {
|
|
815
|
-
if (this.outputDrainHandler || this.outputStream !== stream) return;
|
|
816
|
-
logger$1.info(`discord voice: realtime audio playback buffering guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} bufferedBytes=${stream.writableLength + stream.readableLength}`);
|
|
817
|
-
this.outputDrainHandler = () => {
|
|
818
|
-
if (this.params.stopped() || this.outputStream !== stream || stream.destroyed) return;
|
|
819
|
-
this.outputDrainHandler = void 0;
|
|
820
|
-
let refreshWatchdog = this.params.harness.outputActivity.snapshot().streamEnding;
|
|
821
|
-
while (this.outputPacedBuffers.length > 0) {
|
|
822
|
-
const buffered = this.outputPacedBuffers.shift();
|
|
823
|
-
if (!buffered) break;
|
|
824
|
-
this.outputPacedBytes -= buffered.length;
|
|
825
|
-
const writable = stream.write(buffered);
|
|
826
|
-
if (refreshWatchdog) {
|
|
827
|
-
this.scheduleOutputPlaybackWatchdog("output-drain", stream);
|
|
828
|
-
refreshWatchdog = false;
|
|
829
|
-
}
|
|
830
|
-
if (!writable) {
|
|
831
|
-
this.waitForOutputDrain(stream);
|
|
832
|
-
return;
|
|
1114
|
+
createOutput() {
|
|
1115
|
+
const logContext = `guild=${this.params.entry.guildId} channel=${this.params.entry.channelId}`;
|
|
1116
|
+
const output = new DiscordRealtimeOutput({
|
|
1117
|
+
player: this.params.player,
|
|
1118
|
+
logContext,
|
|
1119
|
+
onStart: () => {
|
|
1120
|
+
this.params.harness.outputActivity.markPlaybackStarted();
|
|
1121
|
+
const config = this.params.realtimeConfig();
|
|
1122
|
+
logger$2.info(`discord voice: realtime audio playback started ${logContext} mode=${this.params.mode} model=${config?.model ?? "provider-default"} voice=${config?.speakerVoice ?? config?.speakerVoiceId ?? "provider-default"}`);
|
|
1123
|
+
},
|
|
1124
|
+
onClose: (closed, reason) => {
|
|
1125
|
+
if (!this.outputs.delete(closed)) return;
|
|
1126
|
+
if (this.generatingOutput === closed) {
|
|
1127
|
+
this.generatingOutput = void 0;
|
|
1128
|
+
if (reason !== "player-idle") this.responseAudio = "discarding";
|
|
833
1129
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
startOutputPlayback(stream) {
|
|
840
|
-
if (this.params.harness.outputActivity.snapshot().playbackStarted || stream.destroyed) return;
|
|
841
|
-
const voiceSdk = loadDiscordVoiceSdk();
|
|
842
|
-
const opusStream = createDiscordOpusEncodeStream();
|
|
843
|
-
pipeline(stream, opusStream, (err) => {
|
|
844
|
-
if (!err || this.outputStream !== stream) return;
|
|
845
|
-
logger$1.warn(`discord voice: realtime output pipeline failed guild=${this.params.entry.guildId} channel=${this.params.entry.channelId}: ${formatErrorMessage(err)}`);
|
|
846
|
-
this.clearOutputAudio("output-pipeline-error");
|
|
847
|
-
this.completeExactSpeechResponse("output-pipeline-error");
|
|
848
|
-
});
|
|
849
|
-
const buffered = Buffer.concat(this.outputPacedBuffers, this.outputPacedBytes);
|
|
850
|
-
this.outputPacedBuffers = [];
|
|
851
|
-
this.outputPacedBytes = 0;
|
|
852
|
-
if (buffered.length > 0 && !stream.write(buffered)) this.waitForOutputDrain(stream);
|
|
853
|
-
const resource = voiceSdk.createAudioResource(opusStream, { inputType: voiceSdk.StreamType.Opus });
|
|
854
|
-
this.params.entry.player.play(resource);
|
|
855
|
-
this.params.harness.outputActivity.markPlaybackStarted();
|
|
856
|
-
const realtimeConfig = this.params.realtimeConfig();
|
|
857
|
-
logger$1.info(`discord voice: realtime audio playback started guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} mode=${this.params.mode} model=${realtimeConfig?.model ?? "provider-default"} voice=${realtimeConfig?.speakerVoice ?? realtimeConfig?.speakerVoiceId ?? "provider-default"}`);
|
|
858
|
-
}
|
|
859
|
-
resetOutputStream(reason = "reset") {
|
|
860
|
-
const stream = this.outputStream;
|
|
861
|
-
this.clearOutputPlaybackWatchdog();
|
|
862
|
-
this.logOutputAudioStopped(reason);
|
|
863
|
-
this.outputStream = null;
|
|
864
|
-
this.outputPacedBuffers = [];
|
|
865
|
-
this.outputPacedBytes = 0;
|
|
866
|
-
if (this.outputDrainHandler) {
|
|
867
|
-
stream?.off("drain", this.outputDrainHandler);
|
|
868
|
-
this.outputDrainHandler = void 0;
|
|
869
|
-
}
|
|
870
|
-
this.params.harness.outputActivity.reset();
|
|
871
|
-
stream?.end();
|
|
872
|
-
stream?.destroy();
|
|
873
|
-
}
|
|
874
|
-
scheduleOutputPlaybackWatchdog(reason, stream) {
|
|
875
|
-
this.clearOutputPlaybackWatchdog();
|
|
876
|
-
const timeoutMs = this.params.harness.outputActivity.playbackWatchdogDelayMs({
|
|
877
|
-
marginMs: DISCORD_REALTIME_OUTPUT_PLAYBACK_WATCHDOG_MARGIN_MS,
|
|
878
|
-
minMs: DISCORD_REALTIME_OUTPUT_PLAYBACK_WATCHDOG_MARGIN_MS
|
|
1130
|
+
if (this.outputs.size === 0) this.params.harness.outputActivity.reset();
|
|
1131
|
+
this.completeExactSpeechResponse(reason);
|
|
1132
|
+
},
|
|
1133
|
+
onBargeIn: (reason) => this.handleBargeIn(reason),
|
|
1134
|
+
onError: (error) => this.stopAfterPlaybackFailure("output-playback-error", error instanceof Error ? error : new Error(formatErrorMessage(error)))
|
|
879
1135
|
});
|
|
880
|
-
if (
|
|
881
|
-
this.
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
this.completeExactSpeechResponse("playback-watchdog");
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
logger$1.warn(`discord voice: realtime audio playback watchdog fired reason=${reason} guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} audioMs=${this.outputAudioMs()} elapsedMs=${this.params.harness.outputActivity.elapsedPlaybackMs()}`);
|
|
889
|
-
this.clearOutputAudio("playback-watchdog");
|
|
890
|
-
this.completeExactSpeechResponse("playback-watchdog");
|
|
891
|
-
}, timeoutMs);
|
|
892
|
-
}
|
|
893
|
-
clearOutputPlaybackWatchdog() {
|
|
894
|
-
if (!this.outputPlaybackWatchdog) return;
|
|
895
|
-
clearTimeout(this.outputPlaybackWatchdog);
|
|
896
|
-
this.outputPlaybackWatchdog = void 0;
|
|
1136
|
+
if (this.outputs.size === 0) this.params.harness.outputActivity.markStreamOpened();
|
|
1137
|
+
this.outputs.add(output);
|
|
1138
|
+
this.generatingOutput = output;
|
|
1139
|
+
if (this.exactSpeechState.status === "active") this.exactSpeechState.output ??= output;
|
|
1140
|
+
return output;
|
|
897
1141
|
}
|
|
898
1142
|
sendExactSpeechMessage(text) {
|
|
899
1143
|
if (this.params.stopped() || !text.trim()) return;
|
|
900
1144
|
this.exactSpeechState = {
|
|
901
1145
|
status: "active",
|
|
902
|
-
message: text
|
|
903
|
-
audioStarted: false
|
|
1146
|
+
message: text
|
|
904
1147
|
};
|
|
1148
|
+
this.beginResponse();
|
|
905
1149
|
this.params.bridge()?.sendUserMessage(this.params.buildSpeakExactMessage(text));
|
|
906
1150
|
}
|
|
907
|
-
completeExactSpeechResponse(reason
|
|
908
|
-
if (this.
|
|
1151
|
+
completeExactSpeechResponse(reason) {
|
|
1152
|
+
if (this.responseAudio !== "completed" || this.outputs.size > 0) return;
|
|
909
1153
|
this.exactSpeechState = { status: "idle" };
|
|
910
|
-
if (options?.drain === false) return;
|
|
911
1154
|
this.drainQueuedExactSpeechMessages(reason);
|
|
912
1155
|
}
|
|
913
|
-
logOutputAudioStopped(reason) {
|
|
914
|
-
const activity = this.params.harness.outputActivity.snapshot();
|
|
915
|
-
const audioMs = Math.floor(activity.audioMs);
|
|
916
|
-
const chunks = activity.chunks;
|
|
917
|
-
const discordBytes = activity.sinkAudioBytes;
|
|
918
|
-
const realtimeBytes = activity.sourceAudioBytes;
|
|
919
|
-
const elapsedMs = this.params.harness.outputActivity.elapsedPlaybackMs();
|
|
920
|
-
if (this.outputStream || chunks > 0 || audioMs > 0) logger$1.info(`discord voice: realtime audio playback stopped reason=${reason} guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} audioMs=${audioMs} elapsedMs=${elapsedMs} chunks=${chunks} discordBytes=${discordBytes} realtimeBytes=${realtimeBytes}`);
|
|
921
|
-
}
|
|
922
1156
|
};
|
|
923
1157
|
//#endregion
|
|
924
|
-
//#region extensions/discord/src/voice/realtime-session.
|
|
925
|
-
const logger = createSubsystemLogger("discord/voice");
|
|
1158
|
+
//#region extensions/discord/src/voice/realtime-speaker-session.ts
|
|
1159
|
+
const logger$1 = createSubsystemLogger("discord/voice");
|
|
926
1160
|
const DISCORD_REALTIME_DUPLICATE_ERROR_SUPPRESS_MS = 6e4;
|
|
927
1161
|
const discordRealtimeTalkPayload = () => ({});
|
|
928
1162
|
const DISCORD_REALTIME_VERBOSE_OMITTED_EVENTS = /* @__PURE__ */ new Set([
|
|
@@ -958,7 +1192,7 @@ function readProviderConfigString(config, key) {
|
|
|
958
1192
|
function isDiscordAgentProxyVoiceMode(mode) {
|
|
959
1193
|
return mode === "agent-proxy";
|
|
960
1194
|
}
|
|
961
|
-
var
|
|
1195
|
+
var DiscordRealtimeSpeakerSession = class {
|
|
962
1196
|
constructor(params) {
|
|
963
1197
|
this.params = params;
|
|
964
1198
|
this.bridge = null;
|
|
@@ -972,9 +1206,13 @@ var DiscordRealtimeVoiceSession = class {
|
|
|
972
1206
|
this.wakeNames = [];
|
|
973
1207
|
this.providerGenerationObserved = false;
|
|
974
1208
|
this.providerContinuityEpoch = 0;
|
|
1209
|
+
this.captures = /* @__PURE__ */ new Set();
|
|
1210
|
+
this.inputOpen = true;
|
|
1211
|
+
this.activeOperations = 0;
|
|
1212
|
+
this.lastActivityAt = Date.now();
|
|
975
1213
|
this.harness = createRealtimeVoiceSessionHarness({
|
|
976
1214
|
talk: {
|
|
977
|
-
sessionId:
|
|
1215
|
+
sessionId: this.params.sessionId,
|
|
978
1216
|
mode: "realtime",
|
|
979
1217
|
transport: "gateway-relay",
|
|
980
1218
|
brain: "agent-consult"
|
|
@@ -1001,6 +1239,7 @@ var DiscordRealtimeVoiceSession = class {
|
|
|
1001
1239
|
surfaceLabel: "the Discord voice channel"
|
|
1002
1240
|
}),
|
|
1003
1241
|
entry: this.params.entry,
|
|
1242
|
+
player: this.params.player,
|
|
1004
1243
|
harness: this.harness,
|
|
1005
1244
|
markProviderGenerationObserved: () => this.markProviderGenerationObserved(),
|
|
1006
1245
|
mode: this.params.mode,
|
|
@@ -1018,6 +1257,11 @@ var DiscordRealtimeVoiceSession = class {
|
|
|
1018
1257
|
bridge: () => this.bridge,
|
|
1019
1258
|
entry: this.params.entry,
|
|
1020
1259
|
getHumanParticipantCount: () => this.humanParticipantCount(),
|
|
1260
|
+
interruptRoomPlayback: () => {
|
|
1261
|
+
if (!this.playback.isBargeInEnabled() || !this.params.player.isActive()) return false;
|
|
1262
|
+
this.params.player.handleBargeIn("active-speaker-audio");
|
|
1263
|
+
return true;
|
|
1264
|
+
},
|
|
1021
1265
|
onAcceptedTranscript: (text, context, providerEpoch) => this.consults.handleAcceptedTranscript(text, context, providerEpoch),
|
|
1022
1266
|
playback: this.playback,
|
|
1023
1267
|
providerEpoch: () => this.providerContinuityEpoch,
|
|
@@ -1039,7 +1283,7 @@ var DiscordRealtimeVoiceSession = class {
|
|
|
1039
1283
|
isWakeNameRequired: () => this.isWakeNameRequired(),
|
|
1040
1284
|
playback: this.playback,
|
|
1041
1285
|
providerEpoch: () => this.providerContinuityEpoch,
|
|
1042
|
-
runAgentTurn: this.params.runAgentTurn,
|
|
1286
|
+
runAgentTurn: (turn) => this.trackOperation(() => this.params.runAgentTurn(turn)),
|
|
1043
1287
|
stopped: () => this.isStopped(),
|
|
1044
1288
|
turns: this.turns,
|
|
1045
1289
|
usesRealtimeAgentHandoff: () => this.params.mode === "bidi" || this.consultToolPolicy !== "none",
|
|
@@ -1116,40 +1360,49 @@ var DiscordRealtimeVoiceSession = class {
|
|
|
1116
1360
|
instructions,
|
|
1117
1361
|
autoRespondToAudio,
|
|
1118
1362
|
interruptResponseOnInputAudio,
|
|
1119
|
-
markStrategy: "
|
|
1363
|
+
markStrategy: "transport",
|
|
1120
1364
|
tools: usesRealtimeAgentHandoff ? resolveRealtimeVoiceAgentConsultTools(toolPolicy, toolPolicy !== "none" ? [REALTIME_VOICE_AGENT_CONTROL_TOOL] : []) : [],
|
|
1121
1365
|
audioSink: {
|
|
1122
1366
|
isOpen: () => !this.isStopped(),
|
|
1123
|
-
sendAudio: (audio) => this.playback.sendOutputAudio(audio),
|
|
1367
|
+
sendAudio: (audio, metadata) => this.playback.sendOutputAudio(audio, metadata),
|
|
1368
|
+
sendMark: (markName, acknowledge) => {
|
|
1369
|
+
if (acknowledge) this.playback.sendOutputMark(acknowledge);
|
|
1370
|
+
else this.bridge?.acknowledgeMark(markName);
|
|
1371
|
+
},
|
|
1372
|
+
getPlaybackState: () => this.playback.getPlaybackState(),
|
|
1124
1373
|
clearAudio: () => {
|
|
1125
1374
|
this.markProviderGenerationObserved();
|
|
1126
1375
|
this.harness.flushOutput(() => this.playback.clearOutputAudio("provider-clear-audio"));
|
|
1127
1376
|
}
|
|
1128
1377
|
},
|
|
1129
1378
|
onTranscript: (role, text, isFinal) => {
|
|
1379
|
+
if (this.isStopped()) return;
|
|
1130
1380
|
this.markProviderGenerationObserved();
|
|
1131
|
-
if (isFinal && text.trim()) logger.info(`discord voice: realtime ${role} transcript (${text.length} chars): ${formatVoiceLogPreview(text)}`);
|
|
1381
|
+
if (isFinal && text.trim()) logger$1.info(`discord voice: realtime ${role} transcript (${text.length} chars): ${formatVoiceLogPreview(text)}`);
|
|
1132
1382
|
if (isFinal && role === "assistant") this.playback.suppressDuplicateControlSpeech(text);
|
|
1133
1383
|
if (role !== "user") return;
|
|
1134
1384
|
if (!isFinal) {
|
|
1135
1385
|
this.turns.handlePartialUserTranscript(text);
|
|
1136
1386
|
return;
|
|
1137
1387
|
}
|
|
1138
|
-
this.turns.handleFinalUserTranscript(text);
|
|
1388
|
+
this.trackOperation(() => this.turns.handleFinalUserTranscript(text)).catch((error) => this.logRealtimeError(formatErrorMessage(error)));
|
|
1139
1389
|
},
|
|
1140
1390
|
onToolCall: (event, session) => {
|
|
1391
|
+
if (this.isStopped()) return;
|
|
1141
1392
|
this.markProviderGenerationObserved();
|
|
1142
|
-
return this.consults.handleToolCall(event, session);
|
|
1393
|
+
return this.trackOperation(() => this.consults.handleToolCall(event, session));
|
|
1143
1394
|
},
|
|
1144
1395
|
onReady,
|
|
1145
1396
|
onEvent: (event) => {
|
|
1397
|
+
if (this.isStopped()) return;
|
|
1146
1398
|
this.handleBridgeEvent(event);
|
|
1147
1399
|
if (event.direction === "client" && event.type === "session.reconnect.ready") onReady();
|
|
1148
1400
|
},
|
|
1149
1401
|
onResponseDone: (outcome) => {
|
|
1402
|
+
if (this.isStopped()) return;
|
|
1150
1403
|
this.markProviderGenerationObserved();
|
|
1151
1404
|
this.playback.handleResponseDone(outcome);
|
|
1152
|
-
if (outcome.status === "cancelled") logger.info(`discord voice: realtime model interrupt confirmed server:response.done status=cancelled${outcome.reason ? ` reason=${outcome.reason}` : ""}`);
|
|
1405
|
+
if (outcome.status === "cancelled") logger$1.info(`discord voice: realtime model interrupt confirmed server:response.done status=cancelled${outcome.reason ? ` reason=${outcome.reason}` : ""}`);
|
|
1153
1406
|
else if (outcome.status === "failed" || outcome.status === "incomplete") this.logRealtimeError(outcome.message);
|
|
1154
1407
|
},
|
|
1155
1408
|
onError: (error) => this.logRealtimeError(formatErrorMessage(error)),
|
|
@@ -1167,8 +1420,7 @@ var DiscordRealtimeVoiceSession = class {
|
|
|
1167
1420
|
const resolvedModel = readProviderConfigString(resolved.providerConfig, "model") ?? resolved.provider.defaultModel;
|
|
1168
1421
|
const resolvedVoice = readProviderConfigString(resolved.providerConfig, "voice");
|
|
1169
1422
|
const humanParticipantCount = this.humanParticipantCount();
|
|
1170
|
-
logger.info(`discord voice: realtime bridge starting mode=${this.params.mode} provider=${resolved.provider.id} model=${resolvedModel ?? "default"} voice=${resolvedVoice ?? "default"} consultPolicy=${consultPolicy} toolPolicy=${toolPolicy} autoRespond=${autoRespondToAudio} wakeNamePolicy=${this.wakeNamePolicy} requireWakeName=${this.isWakeNameRequired(humanParticipantCount)} humanParticipants=${humanParticipantCount} wakeNames=${this.wakeNames.join(",") || "none"} interruptResponse=${interruptResponseOnInputAudio} bargeIn=${bargeIn} minBargeInAudioEndMs=${minBargeInAudioEndMs}`);
|
|
1171
|
-
this.playback.attachPlayer();
|
|
1423
|
+
logger$1.info(`discord voice: realtime bridge starting mode=${this.params.mode} provider=${resolved.provider.id} model=${resolvedModel ?? "default"} voice=${resolvedVoice ?? "default"} consultPolicy=${consultPolicy} toolPolicy=${toolPolicy} autoRespond=${autoRespondToAudio} wakeNamePolicy=${this.wakeNamePolicy} requireWakeName=${this.isWakeNameRequired(humanParticipantCount)} humanParticipants=${humanParticipantCount} wakeNames=${this.wakeNames.join(",") || "none"} interruptResponse=${interruptResponseOnInputAudio} bargeIn=${bargeIn} minBargeInAudioEndMs=${minBargeInAudioEndMs}`);
|
|
1172
1424
|
await this.bridge.connect();
|
|
1173
1425
|
if (!this.markLifecycleReady(lifecycleGeneration)) {
|
|
1174
1426
|
this.bridge?.close();
|
|
@@ -1176,10 +1428,11 @@ var DiscordRealtimeVoiceSession = class {
|
|
|
1176
1428
|
}
|
|
1177
1429
|
this.markProviderGenerationObserved();
|
|
1178
1430
|
this.playback.drainQueuedExactSpeechMessages("provider-connected");
|
|
1179
|
-
logger.info(`discord voice: realtime bridge ready mode=${this.params.mode} provider=${resolved.provider.id} model=${resolvedModel ?? "default"} voice=${resolvedVoice ?? "default"}`);
|
|
1431
|
+
logger$1.info(`discord voice: realtime bridge ready mode=${this.params.mode} provider=${resolved.provider.id} model=${resolvedModel ?? "default"} voice=${resolvedVoice ?? "default"}`);
|
|
1180
1432
|
}
|
|
1181
1433
|
close() {
|
|
1182
1434
|
this.lifecycle.status = "stopped";
|
|
1435
|
+
this.drain();
|
|
1183
1436
|
this.providerContinuityEpoch += 1;
|
|
1184
1437
|
this.flushSuppressedRealtimeErrors();
|
|
1185
1438
|
this.consults.close();
|
|
@@ -1191,7 +1444,46 @@ var DiscordRealtimeVoiceSession = class {
|
|
|
1191
1444
|
this.realtimeProviderId = void 0;
|
|
1192
1445
|
}
|
|
1193
1446
|
beginSpeakerTurn(context, userId) {
|
|
1194
|
-
|
|
1447
|
+
if (!this.inputOpen || this.isStopped()) throw new Error("Discord realtime speaker input is closed");
|
|
1448
|
+
const turn = this.turns.beginSpeakerTurn(context, userId);
|
|
1449
|
+
let closed = false;
|
|
1450
|
+
const capture = {
|
|
1451
|
+
sendInputAudio: (audio) => {
|
|
1452
|
+
if (!closed) {
|
|
1453
|
+
this.lastActivityAt = Date.now();
|
|
1454
|
+
turn.sendInputAudio(audio);
|
|
1455
|
+
}
|
|
1456
|
+
},
|
|
1457
|
+
close: () => {
|
|
1458
|
+
if (closed) return;
|
|
1459
|
+
closed = true;
|
|
1460
|
+
this.captures.delete(capture);
|
|
1461
|
+
this.lastActivityAt = Date.now();
|
|
1462
|
+
turn.close();
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
this.captures.add(capture);
|
|
1466
|
+
return capture;
|
|
1467
|
+
}
|
|
1468
|
+
drain() {
|
|
1469
|
+
this.inputOpen = false;
|
|
1470
|
+
for (const capture of this.captures) capture.close();
|
|
1471
|
+
}
|
|
1472
|
+
releaseReasonBefore(cutoff) {
|
|
1473
|
+
if (!(this.lastActivityAt < cutoff && this.captures.size === 0 && this.activeOperations === 0 && this.consults.isIdle() && !this.playback.isOutputAudioActive() && this.playback.retainedExactSpeechTexts().length === 0)) return;
|
|
1474
|
+
return this.turns.hasPendingSpeakerAudioContext() ? "input-timeout" : "idle";
|
|
1475
|
+
}
|
|
1476
|
+
notify(text) {
|
|
1477
|
+
this.playback.enqueueExactSpeechMessage(text);
|
|
1478
|
+
}
|
|
1479
|
+
async trackOperation(operation) {
|
|
1480
|
+
this.activeOperations += 1;
|
|
1481
|
+
try {
|
|
1482
|
+
return await operation();
|
|
1483
|
+
} finally {
|
|
1484
|
+
this.activeOperations -= 1;
|
|
1485
|
+
this.lastActivityAt = Date.now();
|
|
1486
|
+
}
|
|
1195
1487
|
}
|
|
1196
1488
|
handleBargeIn(reason = "barge-in") {
|
|
1197
1489
|
this.playback.handleBargeIn(reason);
|
|
@@ -1224,14 +1516,16 @@ var DiscordRealtimeVoiceSession = class {
|
|
|
1224
1516
|
if (!(event.direction === "client" && event.type === "session.continuity.reset")) this.markProviderGenerationObserved();
|
|
1225
1517
|
const detail = event.detail ? ` ${event.detail}` : "";
|
|
1226
1518
|
if (event.direction === "client" && event.type === "session.continuity.reset") this.resetProviderContinuity(event.type);
|
|
1519
|
+
if (event.direction === "server" && event.type === "response.created") this.playback.beginResponse();
|
|
1227
1520
|
if (event.direction === "server" && event.type === "input_audio_buffer.speech_started") this.turns.resetPartialWakeNameTracking();
|
|
1228
1521
|
if (shouldLogRealtimeVerboseEvent(event)) logVoiceVerbose(`realtime ${event.direction}:${event.type}${detail}`);
|
|
1229
1522
|
const interruptionLog = formatRealtimeInterruptionLog(event);
|
|
1230
|
-
if (interruptionLog) logger.info(interruptionLog);
|
|
1523
|
+
if (interruptionLog) logger$1.info(interruptionLog);
|
|
1231
1524
|
const lifecycleLog = formatRealtimeLifecycleLog(event);
|
|
1232
|
-
if (lifecycleLog) logger.info(lifecycleLog);
|
|
1525
|
+
if (lifecycleLog) logger$1.info(lifecycleLog);
|
|
1233
1526
|
}
|
|
1234
1527
|
markProviderGenerationObserved() {
|
|
1528
|
+
this.lastActivityAt = Date.now();
|
|
1235
1529
|
this.providerGenerationObserved = true;
|
|
1236
1530
|
}
|
|
1237
1531
|
resetProviderContinuity(reason) {
|
|
@@ -1255,11 +1549,11 @@ var DiscordRealtimeVoiceSession = class {
|
|
|
1255
1549
|
suppressed: 0,
|
|
1256
1550
|
lastLoggedAt: now
|
|
1257
1551
|
};
|
|
1258
|
-
logger.warn(`discord voice: realtime error: ${message}`);
|
|
1552
|
+
logger$1.warn(`discord voice: realtime error: ${message}`);
|
|
1259
1553
|
}
|
|
1260
1554
|
flushSuppressedRealtimeErrors() {
|
|
1261
1555
|
if (!this.lastRealtimeError || this.lastRealtimeError.suppressed === 0) return;
|
|
1262
|
-
logger.warn(`discord voice: suppressed ${this.lastRealtimeError.suppressed} duplicate realtime errors: ${this.lastRealtimeError.message}`);
|
|
1556
|
+
logger$1.warn(`discord voice: suppressed ${this.lastRealtimeError.suppressed} duplicate realtime errors: ${this.lastRealtimeError.message}`);
|
|
1263
1557
|
this.lastRealtimeError.suppressed = 0;
|
|
1264
1558
|
}
|
|
1265
1559
|
};
|
|
@@ -1276,4 +1570,120 @@ function buildProviderConfigOverrides(realtimeConfig) {
|
|
|
1276
1570
|
return Object.keys(overrides).length > 0 ? overrides : void 0;
|
|
1277
1571
|
}
|
|
1278
1572
|
//#endregion
|
|
1573
|
+
//#region extensions/discord/src/voice/realtime-session.runtime.ts
|
|
1574
|
+
const logger = createSubsystemLogger("discord/voice");
|
|
1575
|
+
const MAX_REALTIME_SPEAKERS = 8;
|
|
1576
|
+
const REALTIME_SPEAKER_IDLE_MS = 6e4;
|
|
1577
|
+
/** The room shares its agent and player; each provider connection has one immutable speaker. */
|
|
1578
|
+
var DiscordRealtimeVoiceSession = class {
|
|
1579
|
+
constructor(params) {
|
|
1580
|
+
this.params = params;
|
|
1581
|
+
this.speakers = /* @__PURE__ */ new Map();
|
|
1582
|
+
this.sessions = /* @__PURE__ */ new Set();
|
|
1583
|
+
this.nextSessionId = 0;
|
|
1584
|
+
this.closed = false;
|
|
1585
|
+
this.player = new DiscordRealtimePlayer(params.entry.player);
|
|
1586
|
+
}
|
|
1587
|
+
async connect() {
|
|
1588
|
+
if (this.closed) throw new Error("Discord realtime voice session is closed");
|
|
1589
|
+
const session = this.createSession();
|
|
1590
|
+
this.warmSession = session;
|
|
1591
|
+
await session.connect();
|
|
1592
|
+
if (this.closed) {
|
|
1593
|
+
session.close();
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
this.idleTimer = setInterval(() => this.releaseIdleSpeakers(), REALTIME_SPEAKER_IDLE_MS);
|
|
1597
|
+
this.idleTimer.unref?.();
|
|
1598
|
+
}
|
|
1599
|
+
close() {
|
|
1600
|
+
if (this.closed) return;
|
|
1601
|
+
this.closed = true;
|
|
1602
|
+
clearInterval(this.idleTimer);
|
|
1603
|
+
this.idleTimer = void 0;
|
|
1604
|
+
this.player.close();
|
|
1605
|
+
this.warmSession?.close();
|
|
1606
|
+
this.warmSession = void 0;
|
|
1607
|
+
for (const { session } of this.sessions) session.close();
|
|
1608
|
+
this.speakers.clear();
|
|
1609
|
+
this.sessions.clear();
|
|
1610
|
+
}
|
|
1611
|
+
beginSpeakerTurn(context, userId) {
|
|
1612
|
+
if (this.closed) throw new Error("Discord realtime voice session is closed");
|
|
1613
|
+
for (const previous of this.sessions) if (previous.userId === userId && previous.senderIsOwner !== context.senderIsOwner) this.retireSpeaker(previous, "admission-changed");
|
|
1614
|
+
let speaker = this.speakers.get(userId);
|
|
1615
|
+
if (speaker && speaker.transcripts !== this.params.entry.transcripts) {
|
|
1616
|
+
this.speakers.delete(userId);
|
|
1617
|
+
speaker.session.drain();
|
|
1618
|
+
speaker = void 0;
|
|
1619
|
+
}
|
|
1620
|
+
if (!speaker) {
|
|
1621
|
+
this.releaseIdleSpeakers();
|
|
1622
|
+
if (this.sessions.size >= MAX_REALTIME_SPEAKERS) {
|
|
1623
|
+
const message = "Voice is busy with other speakers. Please try again after their replies.";
|
|
1624
|
+
this.notify(message);
|
|
1625
|
+
throw new Error(message);
|
|
1626
|
+
}
|
|
1627
|
+
const warm = this.warmSession;
|
|
1628
|
+
this.warmSession = void 0;
|
|
1629
|
+
const session = warm ?? this.createSession();
|
|
1630
|
+
speaker = {
|
|
1631
|
+
userId,
|
|
1632
|
+
senderIsOwner: context.senderIsOwner,
|
|
1633
|
+
transcripts: this.params.entry.transcripts,
|
|
1634
|
+
session
|
|
1635
|
+
};
|
|
1636
|
+
this.speakers.set(userId, speaker);
|
|
1637
|
+
this.sessions.add(speaker);
|
|
1638
|
+
if (!warm) session.connect().catch((error) => this.handleSpeakerFailure(session, error));
|
|
1639
|
+
}
|
|
1640
|
+
return speaker.session.beginSpeakerTurn(context, userId);
|
|
1641
|
+
}
|
|
1642
|
+
handleBargeIn(reason = "barge-in") {
|
|
1643
|
+
this.player.handleBargeIn(reason);
|
|
1644
|
+
}
|
|
1645
|
+
isBargeInEnabled() {
|
|
1646
|
+
return (this.warmSession ?? this.sessions.values().next().value?.session)?.isBargeInEnabled() ?? false;
|
|
1647
|
+
}
|
|
1648
|
+
createSession() {
|
|
1649
|
+
const session = new DiscordRealtimeSpeakerSession({
|
|
1650
|
+
...this.params,
|
|
1651
|
+
player: this.player,
|
|
1652
|
+
sessionId: `discord:${this.params.entry.voiceSessionKey}:realtime:${++this.nextSessionId}`,
|
|
1653
|
+
onTerminalError: (error) => this.handleSpeakerFailure(session, error)
|
|
1654
|
+
});
|
|
1655
|
+
return session;
|
|
1656
|
+
}
|
|
1657
|
+
handleSpeakerFailure(session, error) {
|
|
1658
|
+
if (this.closed) return;
|
|
1659
|
+
if (this.warmSession === session) {
|
|
1660
|
+
this.params.onTerminalError(error instanceof Error ? error : new Error(String(error)));
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
for (const speaker of this.sessions) {
|
|
1664
|
+
if (speaker.session !== session) continue;
|
|
1665
|
+
logger.warn(`discord voice: realtime speaker failed user=${speaker.userId}: ${formatErrorMessage(error)}`);
|
|
1666
|
+
this.retireSpeaker(speaker, "provider-failed");
|
|
1667
|
+
this.notify("I lost a speaker's voice connection. Please try speaking again.");
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
notify(text) {
|
|
1672
|
+
(this.warmSession ?? this.sessions.values().next().value?.session)?.notify(text);
|
|
1673
|
+
}
|
|
1674
|
+
releaseIdleSpeakers() {
|
|
1675
|
+
const cutoff = Date.now() - REALTIME_SPEAKER_IDLE_MS;
|
|
1676
|
+
for (const speaker of this.sessions) {
|
|
1677
|
+
const reason = speaker.session.releaseReasonBefore(cutoff);
|
|
1678
|
+
if (reason) this.retireSpeaker(speaker, reason);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
retireSpeaker(speaker, reason) {
|
|
1682
|
+
if (this.speakers.get(speaker.userId) === speaker) this.speakers.delete(speaker.userId);
|
|
1683
|
+
this.sessions.delete(speaker);
|
|
1684
|
+
speaker.session.close();
|
|
1685
|
+
logger.info(`discord voice: realtime speaker retired user=${speaker.userId} reason=${reason}${reason === "input-timeout" ? "; idle speaker input expired; speak again to reconnect" : ""}`);
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
//#endregion
|
|
1279
1689
|
export { DiscordRealtimeVoiceSession };
|