@openclaw/discord 2026.9.1-beta.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/account-inspect-BY-ve3yH.js +45 -0
- package/dist/account-inspect-api.js +1 -1
- package/dist/{accounts-CfQ-vSyf.js → accounts-CwJQoLjM.js} +89 -45
- package/dist/action-runtime-api.js +1 -1
- package/dist/{activities-api-q-9PaaHH.js → activities-api-CTcGq3Hf.js} +30 -17
- package/dist/activities-api.js +1 -1
- package/dist/{allow-list-BDMimASD.js → allow-list-DBTtR2dz.js} +13 -35
- package/dist/{api-DNVbJyEy.js → api-BfmEP6wp.js} +6 -5
- package/dist/api.js +18 -18
- package/dist/{approval-handler.runtime-B7qhK3fR.js → approval-handler.runtime-DUSe5fyH.js} +19 -12
- package/dist/{audit-CBEv_ERu.js → audit-DpPfwoJM.js} +8 -7
- package/dist/{channel-Dx1tMTfx.js → channel-BHajPgwR.js} +29 -23
- package/dist/{channel-actions.runtime-CLWWu9nC.js → channel-actions.runtime-DS8pXo0S.js} +38 -38
- package/dist/channel-config-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.setup-DOzXS2Pk.js → channel.setup-BWmMe1tT.js} +18 -18
- package/dist/command-owners-D78sAoOz.js +10 -0
- package/dist/{components-B9V5o361.js → components-BimX47Mq.js} +111 -149
- package/dist/{config-schema-BR13IAx6.js → config-schema-B797-Spt.js} +19 -12
- package/dist/{conversation-binding-route-DJYrY2TQ.js → conversation-binding-route-DuR5NGIx.js} +1 -1
- package/dist/{directory-config-BK6eqekJ.js → directory-config-CD2v2Szs.js} +2 -2
- package/dist/directory-contract-api.js +1 -1
- package/dist/{directory-live-DSAJSrJk.js → directory-live-DaJzWJ2N.js} +4 -4
- package/dist/{doctor-Cm7Y1pJ2.js → doctor-D4HH1inA.js} +9 -6
- package/dist/{doctor-contract-DsWvKIXF.js → doctor-contract-Co5vRCZ6.js} +9 -6
- package/dist/doctor-contract-api.js +31 -28
- package/dist/{handle-action.guild-admin-BdwCRHmm.js → handle-action.guild-admin-4We6wWQU.js} +6 -3
- package/dist/index.js +1 -1
- package/dist/ingress-identity-XNmW-mZS.js +60 -0
- package/dist/{message-handler-BXpWZyni.js → message-handler-DhgBabyU.js} +61 -70
- package/dist/{message-handler.preflight-Ba5LAY_X.js → message-handler.preflight-oZ7dQCd0.js} +41 -101
- package/dist/{message-handler.process-3HEzmemS.js → message-handler.process-BzCYf1pa.js} +82 -108
- package/dist/{message-text-aiwO5R2F.js → message-text-DK4NnBg-.js} +11 -6
- package/dist/{outbound-adapter-BE4jSq2d.js → outbound-adapter-CtggmGh9.js} +52 -67
- package/dist/{outbound-components-oLpg8Qzb.js → outbound-components-Dn8SJWMS.js} +5 -4
- package/dist/{outbound-session-route-Cg15ZFJd.js → outbound-session-route-Q_HEusBQ.js} +57 -15
- package/dist/{owner-access-UlYi9t0M.js → owner-access-Cp8r6Ipf.js} +9 -7
- package/dist/{pluralkit-Tiyl70dx.js → pluralkit-BY75YcK0.js} +2 -2
- package/dist/{probe-n6ZXxK8V.js → probe-3ovFAp3b.js} +3 -3
- package/dist/{probe.runtime-CN4bdPfZ.js → probe.runtime-BckHn3cJ.js} +1 -1
- package/dist/{provider-session.runtime-B9xe-Qaf.js → provider-session.runtime-BL2lHBH7.js} +3 -3
- package/dist/{provider-B11xwilq.js → provider-uKmmrDM4.js} +299 -136
- package/dist/provider.runtime-C-IkWF2w.js +2 -0
- package/dist/{realtime-session.runtime-CU8IQUS-.js → realtime-session.runtime-CGYm1t_0.js} +798 -448
- package/dist/{resolve-allowlist-common-BLoYaxEG.js → resolve-allowlist-common-sKIKy-cF.js} +3 -3
- package/dist/{resolve-channels-C2sT_n67.js → resolve-channels-CoiMlWy0.js} +4 -4
- package/dist/{resolve-users-CzpjLwjy.js → resolve-users-DJpiokfW.js} +5 -4
- package/dist/{retry-Cv4jCU5Z.js → retry-DQt1zDgH.js} +356 -214
- package/dist/{retry-after-DdLPmPoa.js → retry-after-BrLjLUJi.js} +2 -1
- package/dist/{runtime-CY4PRMF-.js → runtime-BHsdEdr5.js} +2 -2
- package/dist/{runtime-BHrTlFkf.js → runtime-Ghtfh5ZA.js} +151 -109
- package/dist/runtime-api.js +20 -20
- package/dist/{secret-config-contract-B3i6WxV5.js → secret-config-contract-DFu9hfU8.js} +14 -6
- package/dist/secret-contract-api.js +1 -1
- package/dist/{security-audit-BfJhAXZp.js → security-audit-DKfK-iPY.js} +10 -11
- package/dist/security-audit-contract-api.js +1 -1
- package/dist/{security-audit.runtime-BVKF9VtY.js → security-audit.runtime-sKZyKgpx.js} +1 -1
- package/dist/{send-C49MFF9j.js → send-CZeWKUiA.js} +132 -100
- package/dist/{send.components-DKKAw5VX.js → send.components-BG5IDABj.js} +21 -15
- package/dist/{send.outbound-zam0TrZx.js → send.outbound-DHuFUL6W.js} +42 -63
- package/dist/{send.shared-JjljcV4O.js → send.shared-B0n_fWzu.js} +77 -149
- package/dist/{sender-identity-BJtfyise.js → sender-identity-B8Ncfi63.js} +1 -1
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{shared-_5EcP4jW.js → shared-C3dKun_X.js} +24 -15
- package/dist/{subagent-hooks-DQzvOptn.js → subagent-hooks-ZoQXEI1E.js} +2 -2
- package/dist/subagent-hooks-api.js +1 -1
- package/dist/{system-events-C7yxaHWP.js → system-events-DFF_awl7.js} +2 -2
- package/dist/{target-resolver-vxIh6O3-.js → target-resolver-DLtHDwMK.js} +6 -5
- package/dist/targets-BObRyaKX.js +3 -0
- package/dist/{thread-bindings-DW5F9vat.js → thread-bindings-DKvsNjQR.js} +7 -6
- package/dist/{thread-bindings.manager-ByOtwjo_.js → thread-bindings.manager-pbbRT5gb.js} +38 -29
- package/dist/{thread-bindings.session-updates-CJb-BpPL.js → thread-bindings.session-updates-t6jeFB1-.js} +3 -2
- package/dist/{thread-bindings.state-D8OMdk4O.js → thread-bindings.state-DtgOqsbX.js} +34 -1
- package/dist/{threading-agH1T_6s.js → threading-BCx49X-6.js} +8 -6
- package/dist/{timeouts-B9cy6RvQ.js → timeouts-D86uWLt_.js} +1 -1
- package/dist/{transcripts-source-BR11iHA4.js → transcripts-source-Dc6Nnli5.js} +55 -7
- package/dist/transcripts-source-api.js +1 -1
- package/dist/{typing-Dc_YMY2e.js → typing-CuopV-Hi.js} +2 -2
- package/dist/{voice-runtime-DIeaa8fJ.js → voice-runtime-BQnILUbG.js} +492 -105
- package/node_modules/@discord/embedded-app-sdk/node_modules/@types/lodash/README.md +1 -1
- package/node_modules/@discord/embedded-app-sdk/node_modules/@types/lodash/conforms.d.ts +2 -0
- package/node_modules/@discord/embedded-app-sdk/node_modules/@types/lodash/package.json +3 -3
- package/node_modules/@discord/embedded-app-sdk/node_modules/@types/lodash/stubArray.d.ts +2 -0
- package/node_modules/@discord/embedded-app-sdk/node_modules/@types/lodash/stubObject.d.ts +2 -0
- package/node_modules/@discord/embedded-app-sdk/node_modules/@types/lodash/stubString.d.ts +2 -0
- 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/email.mjs +1 -1
- package/node_modules/typebox/build/format/idna/idn-hostname.mjs +19 -1
- 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 +15 -8
- package/package.json +7 -6
- package/skills/discord/SKILL.md +1 -1
- package/dist/account-inspect-BDfKyuvL.js +0 -89
- package/dist/provider.runtime-DiYaHvOz.js +0 -2
- package/dist/sdk-runtime-4geQHtXc.js +0 -338
- package/dist/targets-CtTKvntJ.js +0 -3
|
@@ -1,24 +1,368 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { M as VoiceStateUpdateListener, O as ReadyListener, R as isUnknownDiscordVoiceStateError, Xt as getGuildVoiceState, b as discord_exports, k as ResumedListener, sn as __exportAll, v as parseDiscordTarget } from "./retry-DQt1zDgH.js";
|
|
2
2
|
import { t as getDiscordRuntime } from "./runtime-DgnVQ7zW.js";
|
|
3
|
-
import { a as normalizeDiscordSlug,
|
|
4
|
-
import { a as formatMention } from "./send.outbound-
|
|
5
|
-
import { n as resolveDiscordVoiceEnabled, r as authorizeDiscordVoiceIngress, t as resolveDiscordVoiceAccess } from "./owner-access-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { a as normalizeDiscordSlug, b as formatDiscordUserTag, m as resolveDiscordOwnerAccess } from "./allow-list-DBTtR2dz.js";
|
|
4
|
+
import { a as formatMention } from "./send.outbound-DHuFUL6W.js";
|
|
5
|
+
import { n as resolveDiscordVoiceEnabled, r as authorizeDiscordVoiceIngress, t as resolveDiscordVoiceAccess } from "./owner-access-Cp8r6Ipf.js";
|
|
6
|
+
import { b as resolveFetchedDiscordThreadLikeChannelContext, m as buildDiscordGroupSystemPrompt } from "./provider-uKmmrDM4.js";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import fs from "node:fs/promises";
|
|
8
9
|
import { asDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
9
10
|
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
11
|
+
import { readByteStreamWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
|
10
12
|
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
11
13
|
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
|
12
14
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
13
|
-
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
|
15
|
+
import { createSubsystemLogger, logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
|
|
14
16
|
import { escapeRegExp, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
15
17
|
import { stripInlineDirectiveTagsForDisplay } from "openclaw/plugin-sdk/text-chunking";
|
|
16
|
-
import { unlinkIfExists } from "openclaw/plugin-sdk/media-runtime";
|
|
18
|
+
import { resolveFfmpegBin, unlinkIfExists } from "openclaw/plugin-sdk/media-runtime";
|
|
19
|
+
import { resolvePreferredOpenClawTmpDir, tempWorkspace } from "openclaw/plugin-sdk/temp-path";
|
|
17
20
|
import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
|
|
18
21
|
import { enqueueRoutedSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
22
|
+
import { controlRealtimeVoiceAgentRun, resamplePcm, shouldAutoControlRealtimeVoiceAgentText } from "openclaw/plugin-sdk/realtime-voice";
|
|
23
|
+
import { spawn } from "node:child_process";
|
|
24
|
+
import { Readable, Transform } from "node:stream";
|
|
25
|
+
import { StringDecoder } from "node:string_decoder";
|
|
26
|
+
import { Application, OpusError, createDecoder, createEncoder } from "libopus-wasm";
|
|
21
27
|
import { resolveRealtimeBootstrapContextInstructions } from "openclaw/plugin-sdk/realtime-bootstrap-context";
|
|
28
|
+
//#region extensions/discord/src/voice/sdk-runtime.ts
|
|
29
|
+
let cachedDiscordVoiceSdk = null;
|
|
30
|
+
function loadDiscordVoiceSdk() {
|
|
31
|
+
if (cachedDiscordVoiceSdk) return cachedDiscordVoiceSdk;
|
|
32
|
+
cachedDiscordVoiceSdk = createRequire(import.meta.url)("@discordjs/voice");
|
|
33
|
+
return cachedDiscordVoiceSdk;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region extensions/discord/src/voice/log-preview.ts
|
|
37
|
+
const DISCORD_VOICE_LOG_PREVIEW_CHARS = 500;
|
|
38
|
+
function formatVoiceLogPreview(text) {
|
|
39
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
40
|
+
if (oneLine.length <= DISCORD_VOICE_LOG_PREVIEW_CHARS) return oneLine;
|
|
41
|
+
return `${truncateUtf16Safe(oneLine, DISCORD_VOICE_LOG_PREVIEW_CHARS)}...`;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region extensions/discord/src/voice/agent-control.ts
|
|
45
|
+
async function maybeControlDiscordVoiceAgentRun(params) {
|
|
46
|
+
if (!shouldAutoControlRealtimeVoiceAgentText(params.text)) return { handled: false };
|
|
47
|
+
const result = await controlRealtimeVoiceAgentRun({
|
|
48
|
+
sessionKey: params.entry.route.sessionKey,
|
|
49
|
+
text: params.text
|
|
50
|
+
});
|
|
51
|
+
if (!result.active) return {
|
|
52
|
+
handled: false,
|
|
53
|
+
result
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
handled: true,
|
|
57
|
+
result,
|
|
58
|
+
...result.speak && !result.suppress ? { speakText: result.message } : {}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region extensions/discord/src/voice/prompt.ts
|
|
63
|
+
const DISCORD_VOICE_SPOKEN_OUTPUT_CONTRACT = [
|
|
64
|
+
"You are OpenClaw's Discord voice interface in a live voice channel.",
|
|
65
|
+
"Discord voice reply requirements:",
|
|
66
|
+
"- Return only the concise text that should be spoken aloud in the voice channel.",
|
|
67
|
+
"- Treat the transcript as speech-to-text from a live conversation; repair obvious transcription artifacts and ignore repeated partial fragments caused by voice buffering.",
|
|
68
|
+
"- If the transcript is garbled, incomplete, or missing the user's intent, ask one brief clarifying question instead of guessing.",
|
|
69
|
+
"- If the request needs deeper reasoning, current information, or tools, use the available tools before answering.",
|
|
70
|
+
"- Do not call the tts tool; Discord voice will synthesize and play the returned text.",
|
|
71
|
+
"- Do not reply with NO_REPLY unless no spoken response is appropriate.",
|
|
72
|
+
"- Keep the response brief, natural, and conversational. Prefer one to three short sentences.",
|
|
73
|
+
"- Avoid markdown tables, code fences, citations, and visual formatting unless the user explicitly asks for something that cannot be spoken naturally."
|
|
74
|
+
].join("\n");
|
|
75
|
+
function formatVoiceIngressPrompt(transcript, speakerLabel) {
|
|
76
|
+
const cleanedTranscript = transcript.trim();
|
|
77
|
+
const cleanedLabel = speakerLabel?.trim();
|
|
78
|
+
const voiceInput = cleanedLabel ? [`Voice transcript from speaker "${cleanedLabel}":`, cleanedTranscript].join("\n") : cleanedTranscript;
|
|
79
|
+
return [DISCORD_VOICE_SPOKEN_OUTPUT_CONTRACT, voiceInput].join("\n\n");
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region extensions/discord/src/voice/audio.ts
|
|
83
|
+
const SAMPLE_RATE = 48e3;
|
|
84
|
+
const CHANNELS = 2;
|
|
85
|
+
const BIT_DEPTH = 16;
|
|
86
|
+
const FFMPEG_ERROR_OUTPUT_BYTES = 8192;
|
|
87
|
+
const DISCORD_OPUS_FRAME_SIZE = 960;
|
|
88
|
+
const DISCORD_OPUS_FRAME_BYTES = DISCORD_OPUS_FRAME_SIZE * CHANNELS * (BIT_DEPTH / 8);
|
|
89
|
+
const FFMPEG_PCM_ARGUMENTS = [
|
|
90
|
+
"-analyzeduration",
|
|
91
|
+
"0",
|
|
92
|
+
"-loglevel",
|
|
93
|
+
"error",
|
|
94
|
+
"-vn",
|
|
95
|
+
"-sn",
|
|
96
|
+
"-dn",
|
|
97
|
+
"-f",
|
|
98
|
+
"s16le",
|
|
99
|
+
"-ar",
|
|
100
|
+
String(SAMPLE_RATE),
|
|
101
|
+
"-ac",
|
|
102
|
+
String(CHANNELS)
|
|
103
|
+
];
|
|
104
|
+
let warnedOpusMissing = false;
|
|
105
|
+
function buildWavBuffer(pcm) {
|
|
106
|
+
const blockAlign = 4;
|
|
107
|
+
const byteRate = SAMPLE_RATE * blockAlign;
|
|
108
|
+
const header = Buffer.alloc(44);
|
|
109
|
+
header.write("RIFF", 0);
|
|
110
|
+
header.writeUInt32LE(36 + pcm.length, 4);
|
|
111
|
+
header.write("WAVE", 8);
|
|
112
|
+
header.write("fmt ", 12);
|
|
113
|
+
header.writeUInt32LE(16, 16);
|
|
114
|
+
header.writeUInt16LE(1, 20);
|
|
115
|
+
header.writeUInt16LE(CHANNELS, 22);
|
|
116
|
+
header.writeUInt32LE(SAMPLE_RATE, 24);
|
|
117
|
+
header.writeUInt32LE(byteRate, 28);
|
|
118
|
+
header.writeUInt16LE(blockAlign, 32);
|
|
119
|
+
header.writeUInt16LE(BIT_DEPTH, 34);
|
|
120
|
+
header.write("data", 36);
|
|
121
|
+
header.writeUInt32LE(pcm.length, 40);
|
|
122
|
+
return Buffer.concat([header, pcm]);
|
|
123
|
+
}
|
|
124
|
+
function createDiscordOpusEncodeStream() {
|
|
125
|
+
return new DiscordOpusEncodeStream();
|
|
126
|
+
}
|
|
127
|
+
function createDiscordOpusPlaybackStream(input) {
|
|
128
|
+
const inputSource = typeof input === "string" ? input : "pipe:0";
|
|
129
|
+
const ffmpeg = spawn(resolveFfmpegBin(), [
|
|
130
|
+
"-i",
|
|
131
|
+
inputSource,
|
|
132
|
+
...FFMPEG_PCM_ARGUMENTS,
|
|
133
|
+
"pipe:1"
|
|
134
|
+
], {
|
|
135
|
+
stdio: [
|
|
136
|
+
"pipe",
|
|
137
|
+
"pipe",
|
|
138
|
+
"pipe"
|
|
139
|
+
],
|
|
140
|
+
windowsHide: true
|
|
141
|
+
});
|
|
142
|
+
const opusStream = createDiscordOpusEncodeStream();
|
|
143
|
+
const stderr = Buffer.alloc(FFMPEG_ERROR_OUTPUT_BYTES);
|
|
144
|
+
let stderrBytes = 0;
|
|
145
|
+
let ffmpegClosed = false;
|
|
146
|
+
const killFfmpeg = (signal = "SIGTERM") => {
|
|
147
|
+
if (!ffmpegClosed && !ffmpeg.killed) ffmpeg.kill(signal);
|
|
148
|
+
};
|
|
149
|
+
ffmpeg.stderr.on("data", (chunk) => {
|
|
150
|
+
if (stderrBytes < FFMPEG_ERROR_OUTPUT_BYTES) stderrBytes += chunk.copy(stderr, stderrBytes, 0, FFMPEG_ERROR_OUTPUT_BYTES - stderrBytes);
|
|
151
|
+
});
|
|
152
|
+
ffmpeg.once("error", (err) => {
|
|
153
|
+
opusStream.destroy(err);
|
|
154
|
+
});
|
|
155
|
+
ffmpeg.once("close", (code, signal) => {
|
|
156
|
+
ffmpegClosed = true;
|
|
157
|
+
if (code && code !== 0) {
|
|
158
|
+
const stderrText = new StringDecoder("utf8").write(stderr.subarray(0, stderrBytes)).trim();
|
|
159
|
+
const suffix = stderrText ? `: ${stderrText}` : "";
|
|
160
|
+
opusStream.destroy(/* @__PURE__ */ new Error(`ffmpeg exited with code ${code}${suffix}`));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (signal) opusStream.destroy(/* @__PURE__ */ new Error(`ffmpeg exited with signal ${signal}`));
|
|
164
|
+
});
|
|
165
|
+
for (const readable of [ffmpeg.stdout, ffmpeg.stderr]) readable.on("error", (err) => {
|
|
166
|
+
killFfmpeg("SIGKILL");
|
|
167
|
+
opusStream.destroy(err);
|
|
168
|
+
});
|
|
169
|
+
ffmpeg.stdin.on("error", (err) => {
|
|
170
|
+
if (err.code !== "EPIPE") opusStream.destroy(err);
|
|
171
|
+
});
|
|
172
|
+
ffmpeg.stdout.pipe(opusStream);
|
|
173
|
+
opusStream.once("close", () => {
|
|
174
|
+
if (!opusStream.readableEnded) killFfmpeg();
|
|
175
|
+
});
|
|
176
|
+
if (typeof input !== "string") {
|
|
177
|
+
input.on("error", (err) => {
|
|
178
|
+
ffmpeg.stdin.destroy(err);
|
|
179
|
+
opusStream.destroy(err);
|
|
180
|
+
});
|
|
181
|
+
input.pipe(ffmpeg.stdin);
|
|
182
|
+
} else ffmpeg.stdin.end();
|
|
183
|
+
return opusStream;
|
|
184
|
+
}
|
|
185
|
+
var DiscordOpusEncodeStream = class extends Transform {
|
|
186
|
+
#buffer = Buffer.alloc(0);
|
|
187
|
+
#encoder;
|
|
188
|
+
#packetPcmBytes = /* @__PURE__ */ new WeakMap();
|
|
189
|
+
constructor() {
|
|
190
|
+
super({ readableObjectMode: true });
|
|
191
|
+
}
|
|
192
|
+
_construct(done) {
|
|
193
|
+
createEncoder({
|
|
194
|
+
application: Application.Audio,
|
|
195
|
+
channels: CHANNELS,
|
|
196
|
+
sampleRate: SAMPLE_RATE
|
|
197
|
+
}).then((encoder) => {
|
|
198
|
+
this.#encoder = encoder;
|
|
199
|
+
done();
|
|
200
|
+
}, (err) => done(err instanceof Error ? err : new Error(formatErrorMessage(err))));
|
|
201
|
+
}
|
|
202
|
+
_transform(chunk, _encoding, done) {
|
|
203
|
+
try {
|
|
204
|
+
this.#buffer = this.#buffer.length > 0 ? Buffer.concat([this.#buffer, chunk]) : Buffer.from(chunk);
|
|
205
|
+
while (this.#buffer.length >= DISCORD_OPUS_FRAME_BYTES) {
|
|
206
|
+
const frame = this.#buffer.subarray(0, DISCORD_OPUS_FRAME_BYTES);
|
|
207
|
+
this.#buffer = this.#buffer.subarray(DISCORD_OPUS_FRAME_BYTES);
|
|
208
|
+
this.#encodeFrame(frame);
|
|
209
|
+
}
|
|
210
|
+
done();
|
|
211
|
+
} catch (err) {
|
|
212
|
+
done(err instanceof Error ? err : new Error(formatErrorMessage(err)));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
_flush(done) {
|
|
216
|
+
try {
|
|
217
|
+
this.flushPartialFrame();
|
|
218
|
+
done();
|
|
219
|
+
} catch (err) {
|
|
220
|
+
done(err instanceof Error ? err : new Error(formatErrorMessage(err)));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
flushPartialFrame() {
|
|
224
|
+
if (this.destroyed || this.#buffer.length === 0) return false;
|
|
225
|
+
const pcmBytes = this.#buffer.length;
|
|
226
|
+
const frame = Buffer.alloc(DISCORD_OPUS_FRAME_BYTES);
|
|
227
|
+
this.#buffer.copy(frame);
|
|
228
|
+
this.#buffer = Buffer.alloc(0);
|
|
229
|
+
this.#encodeFrame(frame, pcmBytes);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
takePcmBytes(packet) {
|
|
233
|
+
const bytes = this.#packetPcmBytes.get(packet) ?? 0;
|
|
234
|
+
this.#packetPcmBytes.delete(packet);
|
|
235
|
+
return bytes;
|
|
236
|
+
}
|
|
237
|
+
_destroy(err, done) {
|
|
238
|
+
this.#encoder?.free();
|
|
239
|
+
this.#buffer = Buffer.alloc(0);
|
|
240
|
+
done(err);
|
|
241
|
+
}
|
|
242
|
+
#encodeFrame(frame, pcmBytes = frame.length) {
|
|
243
|
+
const packet = Buffer.from(this.#encoder.encode(frame, { frameSize: DISCORD_OPUS_FRAME_SIZE }));
|
|
244
|
+
this.#packetPcmBytes.set(packet, pcmBytes);
|
|
245
|
+
this.push(packet);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
function pcmInt16ToBuffer(pcm) {
|
|
249
|
+
return Buffer.from(pcm.buffer, pcm.byteOffset, pcm.byteLength);
|
|
250
|
+
}
|
|
251
|
+
async function decodeOpusStream(stream, params) {
|
|
252
|
+
return await readByteStreamWithLimit(decodeOpusFrames(stream, params), {
|
|
253
|
+
maxBytes: params.maxBytes,
|
|
254
|
+
onOverflow: () => /* @__PURE__ */ new Error(`Discord voice capture exceeds the transcription PCM limit (${params.maxBytes} bytes); speak a shorter segment.`)
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
async function decodeOpusStreamChunks(stream, params) {
|
|
258
|
+
try {
|
|
259
|
+
for await (const pcm of decodeOpusFrames(stream, params)) params.onChunk(pcm);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
params.onError?.(err);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function* decodeOpusFrames(stream, params) {
|
|
265
|
+
let decoder;
|
|
266
|
+
try {
|
|
267
|
+
decoder = await createDecoder({
|
|
268
|
+
channels: CHANNELS,
|
|
269
|
+
sampleRate: SAMPLE_RATE
|
|
270
|
+
});
|
|
271
|
+
} catch (err) {
|
|
272
|
+
if (!warnedOpusMissing) {
|
|
273
|
+
warnedOpusMissing = true;
|
|
274
|
+
params.onWarn(`discord voice: no usable opus decoder available (libopus-wasm: ${formatErrorMessage(err)}); cannot decode voice audio`);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
params.onVerbose("opus decoder: libopus-wasm");
|
|
279
|
+
try {
|
|
280
|
+
for await (const chunk of stream) {
|
|
281
|
+
if (!chunk || !(chunk instanceof Buffer) || chunk.length === 0) continue;
|
|
282
|
+
const decoded = decoder.decode(chunk, { maxFrameSize: DISCORD_OPUS_FRAME_SIZE });
|
|
283
|
+
if (decoded.length > 0) yield pcmInt16ToBuffer(decoded);
|
|
284
|
+
}
|
|
285
|
+
} catch (err) {
|
|
286
|
+
params.onError?.(err);
|
|
287
|
+
if (shouldLogVerbose()) logVerbose(`discord voice: opus decode failed: ${formatErrorMessage(err)}`);
|
|
288
|
+
} finally {
|
|
289
|
+
decoder.free();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function convertDiscordPcm48kStereoToRealtimePcm24kMono(pcm) {
|
|
293
|
+
const frameCount = Math.floor(pcm.length / 4);
|
|
294
|
+
if (frameCount === 0) return Buffer.alloc(0);
|
|
295
|
+
const mono48k = Buffer.alloc(frameCount * 2);
|
|
296
|
+
for (let frame = 0; frame < frameCount; frame += 1) {
|
|
297
|
+
const offset = frame * 4;
|
|
298
|
+
const left = pcm.readInt16LE(offset);
|
|
299
|
+
const right = pcm.readInt16LE(offset + 2);
|
|
300
|
+
mono48k.writeInt16LE(Math.round((left + right) / 2), frame * 2);
|
|
301
|
+
}
|
|
302
|
+
return resamplePcm(mono48k, SAMPLE_RATE, 24e3);
|
|
303
|
+
}
|
|
304
|
+
function convertRealtimePcm24kMonoToDiscordPcm48kStereo(pcm) {
|
|
305
|
+
const mono48k = resamplePcm(pcm, 24e3, SAMPLE_RATE);
|
|
306
|
+
const sampleCount = Math.floor(mono48k.length / 2);
|
|
307
|
+
if (sampleCount === 0) return Buffer.alloc(0);
|
|
308
|
+
const stereo = Buffer.alloc(sampleCount * 4);
|
|
309
|
+
for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
|
|
310
|
+
const sample = mono48k.readInt16LE(sampleIndex * 2);
|
|
311
|
+
const offset = sampleIndex * 4;
|
|
312
|
+
stereo.writeInt16LE(sample, offset);
|
|
313
|
+
stereo.writeInt16LE(sample, offset + 2);
|
|
314
|
+
}
|
|
315
|
+
return stereo;
|
|
316
|
+
}
|
|
317
|
+
function estimateDurationSeconds(pcm) {
|
|
318
|
+
const bytesPerSample = BIT_DEPTH / 8 * CHANNELS;
|
|
319
|
+
return pcm.length / (bytesPerSample * SAMPLE_RATE);
|
|
320
|
+
}
|
|
321
|
+
async function writeVoiceWavFile(pcm) {
|
|
322
|
+
const workspace = await tempWorkspace({
|
|
323
|
+
rootDir: resolvePreferredOpenClawTmpDir(),
|
|
324
|
+
prefix: "discord-voice-"
|
|
325
|
+
});
|
|
326
|
+
scheduleTempCleanup(workspace.dir);
|
|
327
|
+
const wav = buildWavBuffer(pcm);
|
|
328
|
+
return {
|
|
329
|
+
path: await workspace.write("segment.wav", wav),
|
|
330
|
+
durationSeconds: estimateDurationSeconds(pcm)
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function scheduleTempCleanup(tempDir, delayMs = 18e5) {
|
|
334
|
+
setTimeout(() => {
|
|
335
|
+
fs.rm(tempDir, {
|
|
336
|
+
recursive: true,
|
|
337
|
+
force: true
|
|
338
|
+
}).catch((err) => {
|
|
339
|
+
if (shouldLogVerbose()) logVerbose(`discord voice: temp cleanup failed for ${tempDir}: ${formatErrorMessage(err)}`);
|
|
340
|
+
});
|
|
341
|
+
}, delayMs).unref();
|
|
342
|
+
}
|
|
343
|
+
const CAPTURE_FINALIZE_GRACE_MS = 2e3;
|
|
344
|
+
const VOICE_CONNECT_READY_TIMEOUT_MS = 3e4;
|
|
345
|
+
const VOICE_RECONNECT_GRACE_MS = 15e3;
|
|
346
|
+
const PLAYBACK_READY_TIMEOUT_MS = 6e4;
|
|
347
|
+
function resolveVoiceTimeoutMs(value, fallbackMs) {
|
|
348
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return fallbackMs;
|
|
349
|
+
return Math.floor(value);
|
|
350
|
+
}
|
|
351
|
+
function resolveDiscordVoiceMode(voice) {
|
|
352
|
+
const mode = voice?.mode;
|
|
353
|
+
if (mode === "stt-tts" || mode === "bidi") return mode;
|
|
354
|
+
return "agent-proxy";
|
|
355
|
+
}
|
|
356
|
+
function isDiscordRealtimeVoiceMode(mode) {
|
|
357
|
+
return mode === "agent-proxy" || mode === "bidi";
|
|
358
|
+
}
|
|
359
|
+
function logVoiceVerbose(message) {
|
|
360
|
+
logVerbose(`discord voice: ${message}`);
|
|
361
|
+
}
|
|
362
|
+
function isVoiceChannel(type) {
|
|
363
|
+
return type === discord_exports.ChannelType.GuildVoice || type === discord_exports.ChannelType.GuildStageVoice;
|
|
364
|
+
}
|
|
365
|
+
//#endregion
|
|
22
366
|
//#region extensions/discord/src/voice/ingress.ts
|
|
23
367
|
const DISCORD_VOICE_MESSAGE_PROVIDER = "discord-voice";
|
|
24
368
|
const logger$7 = createSubsystemLogger("discord/voice");
|
|
@@ -471,7 +815,11 @@ var DiscordVoiceSpeakerContextResolver = class {
|
|
|
471
815
|
label: identity.label,
|
|
472
816
|
name: identity.name,
|
|
473
817
|
tag: identity.tag,
|
|
474
|
-
senderIsOwner:
|
|
818
|
+
senderIsOwner: resolveDiscordOwnerAccess({
|
|
819
|
+
allowFrom: this.params.ownerAllowFrom,
|
|
820
|
+
sender: identity,
|
|
821
|
+
allowNameMatching: false
|
|
822
|
+
}).ownerAllowed
|
|
475
823
|
};
|
|
476
824
|
this.setCachedContext(guildId, userId, context);
|
|
477
825
|
return context;
|
|
@@ -507,17 +855,6 @@ var DiscordVoiceSpeakerContextResolver = class {
|
|
|
507
855
|
}
|
|
508
856
|
}
|
|
509
857
|
}
|
|
510
|
-
resolveIsOwner(identity) {
|
|
511
|
-
return resolveDiscordOwnerAccess({
|
|
512
|
-
allowFrom: this.params.ownerAllowFrom,
|
|
513
|
-
sender: {
|
|
514
|
-
id: identity.id,
|
|
515
|
-
name: identity.name,
|
|
516
|
-
tag: identity.tag
|
|
517
|
-
},
|
|
518
|
-
allowNameMatching: false
|
|
519
|
-
}).ownerAllowed;
|
|
520
|
-
}
|
|
521
858
|
resolveCacheKey(guildId, userId) {
|
|
522
859
|
return `${guildId}:${userId}`;
|
|
523
860
|
}
|
|
@@ -1030,70 +1367,43 @@ var DiscordVoiceFollowing = class {
|
|
|
1030
1367
|
//#endregion
|
|
1031
1368
|
//#region extensions/discord/src/voice/capture-state.ts
|
|
1032
1369
|
function createVoiceCaptureState() {
|
|
1033
|
-
return
|
|
1034
|
-
activeSpeakers: /* @__PURE__ */ new Set(),
|
|
1035
|
-
activeCaptureStreams: /* @__PURE__ */ new Map(),
|
|
1036
|
-
captureFinalizeTimers: /* @__PURE__ */ new Map(),
|
|
1037
|
-
captureGenerations: /* @__PURE__ */ new Map()
|
|
1038
|
-
};
|
|
1370
|
+
return /* @__PURE__ */ new Map();
|
|
1039
1371
|
}
|
|
1040
1372
|
function stopVoiceCaptureState(state) {
|
|
1041
|
-
|
|
1042
|
-
state.
|
|
1043
|
-
for (const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}
|
|
1048
|
-
function
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
return state.activeSpeakers.has(userId);
|
|
1053
|
-
}
|
|
1054
|
-
function clearVoiceCaptureFinalizeTimer(state, userId, generation) {
|
|
1055
|
-
const scheduled = state.captureFinalizeTimers.get(userId);
|
|
1056
|
-
if (!scheduled || generation !== void 0 && scheduled.generation !== generation) return false;
|
|
1057
|
-
clearTimeout(scheduled.timer);
|
|
1058
|
-
state.captureFinalizeTimers.delete(userId);
|
|
1373
|
+
const captures = [...state.values()];
|
|
1374
|
+
state.clear();
|
|
1375
|
+
for (const capture of captures) {
|
|
1376
|
+
clearVoiceCaptureFinalizeTimer(capture);
|
|
1377
|
+
capture.stream?.destroy();
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function clearVoiceCaptureFinalizeTimer(capture) {
|
|
1381
|
+
if (!capture.finalizeTimer) return false;
|
|
1382
|
+
clearTimeout(capture.finalizeTimer);
|
|
1383
|
+
delete capture.finalizeTimer;
|
|
1059
1384
|
return true;
|
|
1060
1385
|
}
|
|
1061
1386
|
function beginVoiceCapture(state, userId, stream) {
|
|
1062
|
-
const
|
|
1063
|
-
state.
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
return generation;
|
|
1071
|
-
}
|
|
1072
|
-
function finishVoiceCapture(state, userId, generation) {
|
|
1073
|
-
clearVoiceCaptureFinalizeTimer(state, userId, generation);
|
|
1074
|
-
if (state.activeCaptureStreams.get(userId)?.generation !== generation) return false;
|
|
1075
|
-
state.activeCaptureStreams.delete(userId);
|
|
1076
|
-
state.activeSpeakers.delete(userId);
|
|
1387
|
+
const capture = { stream };
|
|
1388
|
+
state.set(userId, capture);
|
|
1389
|
+
return capture;
|
|
1390
|
+
}
|
|
1391
|
+
function finishVoiceCapture(state, userId, capture) {
|
|
1392
|
+
clearVoiceCaptureFinalizeTimer(capture);
|
|
1393
|
+
if (state.get(userId) !== capture) return false;
|
|
1394
|
+
state.delete(userId);
|
|
1077
1395
|
return true;
|
|
1078
1396
|
}
|
|
1079
1397
|
function scheduleVoiceCaptureFinalize(params) {
|
|
1080
1398
|
const { state, userId, delayMs, onFinalize } = params;
|
|
1081
|
-
const capture = state.
|
|
1399
|
+
const capture = state.get(userId);
|
|
1082
1400
|
if (!capture) return false;
|
|
1083
|
-
clearVoiceCaptureFinalizeTimer(
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
state.activeCaptureStreams.delete(userId);
|
|
1089
|
-
state.activeSpeakers.delete(userId);
|
|
1090
|
-
onFinalize?.(activeCapture);
|
|
1091
|
-
activeCapture.stream.destroy();
|
|
1401
|
+
clearVoiceCaptureFinalizeTimer(capture);
|
|
1402
|
+
capture.finalizeTimer = setTimeout(() => {
|
|
1403
|
+
if (!finishVoiceCapture(state, userId, capture)) return;
|
|
1404
|
+
onFinalize?.(capture);
|
|
1405
|
+
capture.stream?.destroy();
|
|
1092
1406
|
}, delayMs);
|
|
1093
|
-
state.captureFinalizeTimers.set(userId, {
|
|
1094
|
-
generation: capture.generation,
|
|
1095
|
-
timer
|
|
1096
|
-
});
|
|
1097
1407
|
return true;
|
|
1098
1408
|
}
|
|
1099
1409
|
//#endregion
|
|
@@ -1115,12 +1425,13 @@ function sanitizeVoiceReplyTextForSpeech(text, speakerLabel) {
|
|
|
1115
1425
|
//#endregion
|
|
1116
1426
|
//#region extensions/discord/src/voice/tts.ts
|
|
1117
1427
|
async function transcribeVoiceAudio(params) {
|
|
1118
|
-
|
|
1428
|
+
const result = await getDiscordRuntime().mediaUnderstanding.transcribeAudioFile({
|
|
1119
1429
|
filePath: params.filePath,
|
|
1120
1430
|
cfg: params.cfg,
|
|
1121
1431
|
agentDir: resolveAgentDir(params.cfg, params.agentId),
|
|
1122
1432
|
mime: "audio/wav"
|
|
1123
|
-
})
|
|
1433
|
+
});
|
|
1434
|
+
return normalizeOptionalString(result.text);
|
|
1124
1435
|
}
|
|
1125
1436
|
async function synthesizeVoiceReplyAudio(params) {
|
|
1126
1437
|
const runtime = getDiscordRuntime();
|
|
@@ -1199,6 +1510,7 @@ async function processDiscordVoiceSegment(params) {
|
|
|
1199
1510
|
logVoiceVerbose(`transcription ok (${transcript.length} chars): guild ${entry.guildId} channel ${entry.channelId}`);
|
|
1200
1511
|
logVoiceVerbose(`transcript from ${ingress.speakerLabel} (${userId}) in guild ${entry.guildId} channel ${entry.channelId}: ${formatVoiceLogPreview(transcript)}`);
|
|
1201
1512
|
if (params.transcripts) {
|
|
1513
|
+
if (entry.sessionLifecycle.status === "stopped" || entry.transcripts !== params.transcripts) return;
|
|
1202
1514
|
await params.transcripts.onUtterance({
|
|
1203
1515
|
sessionId: params.transcripts.sessionId,
|
|
1204
1516
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1335,38 +1647,22 @@ var DiscordVoiceReceive = class {
|
|
|
1335
1647
|
const botUserId = this.params.botUserId();
|
|
1336
1648
|
if (botUserId && userId === botUserId) return;
|
|
1337
1649
|
this.params.membership.notePresent(entry, userId);
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
const extended =
|
|
1650
|
+
const activeCapture = entry.capture.get(userId);
|
|
1651
|
+
if (activeCapture) {
|
|
1652
|
+
const extended = clearVoiceCaptureFinalizeTimer(activeCapture);
|
|
1341
1653
|
logVoiceVerbose(`capture start ignored (already active): guild ${entry.guildId} channel ${entry.channelId} user ${userId}${extended ? " (finalize canceled)" : ""}`);
|
|
1342
1654
|
return;
|
|
1343
1655
|
}
|
|
1344
1656
|
logVoiceVerbose(`capture start: guild ${entry.guildId} channel ${entry.channelId} user ${userId}`);
|
|
1345
1657
|
const voiceSdk = loadDiscordVoiceSdk();
|
|
1658
|
+
const transcripts = entry.transcripts;
|
|
1346
1659
|
const voiceMode = resolveDiscordVoiceMode(this.params.discordConfig.voice);
|
|
1347
1660
|
const realtime = entry.realtimeLifecycle.status === "active" && isDiscordRealtimeVoiceMode(voiceMode) ? entry.realtimeLifecycle.instance : void 0;
|
|
1348
1661
|
if (entry.player.state.status === voiceSdk.AudioPlayerStatus.Playing && !realtime) {
|
|
1349
1662
|
logVoiceVerbose(`capture ignored during playback: guild ${entry.guildId} channel ${entry.channelId} user ${userId}`);
|
|
1350
1663
|
return;
|
|
1351
1664
|
}
|
|
1352
|
-
const
|
|
1353
|
-
if (realtime && !realtimeIngress) {
|
|
1354
|
-
logVoiceVerbose(`realtime capture unauthorized: guild ${entry.guildId} channel ${entry.channelId} user ${userId}`);
|
|
1355
|
-
return;
|
|
1356
|
-
}
|
|
1357
|
-
if (!this.params.isEntryCurrent(entry)) return;
|
|
1358
|
-
if (entry.player.state.status === voiceSdk.AudioPlayerStatus.Playing && realtime) {
|
|
1359
|
-
if (!realtime.isBargeInEnabled()) {
|
|
1360
|
-
logger$3.info(`discord voice: realtime capture ignored during playback (barge-in disabled): guild ${entry.guildId} channel ${entry.channelId} user ${userId}`);
|
|
1361
|
-
return;
|
|
1362
|
-
}
|
|
1363
|
-
logVoiceVerbose(`realtime barge-in: guild ${entry.guildId} channel ${entry.channelId} user ${userId}`);
|
|
1364
|
-
logger$3.info(`discord voice: realtime barge-in detected source=speaker-start guild=${entry.guildId} channel=${entry.channelId} user=${userId} playerStatus=${entry.player.state.status}`);
|
|
1365
|
-
realtime.handleBargeIn("speaker-start");
|
|
1366
|
-
}
|
|
1367
|
-
this.enableDaveReceivePassthrough(entry, `speaker ${userId} start`, 15);
|
|
1368
|
-
const stream = entry.connection.receiver.subscribe(userId, { end: { behavior: voiceSdk.EndBehaviorType.Manual } });
|
|
1369
|
-
const generation = beginVoiceCapture(entry.capture, userId, stream);
|
|
1665
|
+
const capture = beginVoiceCapture(entry.capture, userId);
|
|
1370
1666
|
let streamAborted = false;
|
|
1371
1667
|
let receiveFailureHandled = false;
|
|
1372
1668
|
let receiveStreamEndHandled = false;
|
|
@@ -1383,8 +1679,25 @@ var DiscordVoiceReceive = class {
|
|
|
1383
1679
|
receiveFailureHandled = true;
|
|
1384
1680
|
this.handleReceiveError(entry, err);
|
|
1385
1681
|
};
|
|
1386
|
-
stream.on("error", handleStreamError);
|
|
1387
1682
|
try {
|
|
1683
|
+
const realtimeIngress = realtime ? await this.resolveDiscordVoiceIngressContext(entry, userId) : void 0;
|
|
1684
|
+
if (realtime && !realtimeIngress) {
|
|
1685
|
+
logVoiceVerbose(`realtime capture unauthorized: guild ${entry.guildId} channel ${entry.channelId} user ${userId}`);
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
if (!this.params.isEntryCurrent(entry) || entry.capture.get(userId) !== capture) return;
|
|
1689
|
+
if (entry.player.state.status === voiceSdk.AudioPlayerStatus.Playing && realtime) {
|
|
1690
|
+
if (!realtime.isBargeInEnabled()) {
|
|
1691
|
+
logger$3.info(`discord voice: realtime capture ignored during playback (barge-in disabled): guild ${entry.guildId} channel ${entry.channelId} user ${userId}`);
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
logVoiceVerbose(`realtime barge-in: guild ${entry.guildId} channel ${entry.channelId} user ${userId}`);
|
|
1695
|
+
logger$3.info(`discord voice: realtime barge-in detected source=speaker-start guild=${entry.guildId} channel=${entry.channelId} user=${userId} playerStatus=${entry.player.state.status}`);
|
|
1696
|
+
realtime.handleBargeIn("speaker-start");
|
|
1697
|
+
}
|
|
1698
|
+
this.enableDaveReceivePassthrough(entry, `speaker ${userId} start`, 15);
|
|
1699
|
+
const stream = capture.stream = entry.connection.receiver.subscribe(userId, { end: { behavior: voiceSdk.EndBehaviorType.Manual } });
|
|
1700
|
+
stream.on("error", handleStreamError);
|
|
1388
1701
|
if (realtime && realtimeIngress) {
|
|
1389
1702
|
const turn = realtime.beginSpeakerTurn(realtimeIngress, userId);
|
|
1390
1703
|
try {
|
|
@@ -1399,7 +1712,12 @@ var DiscordVoiceReceive = class {
|
|
|
1399
1712
|
}
|
|
1400
1713
|
return;
|
|
1401
1714
|
}
|
|
1715
|
+
if (!entry.audioInputBudget.enabled) {
|
|
1716
|
+
logger$3.warn("discord voice: capture skipped: audio understanding is disabled; enable tools.media.audio.enabled to transcribe voice.");
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1402
1719
|
const pcm = await decodeOpusStream(stream, {
|
|
1720
|
+
maxBytes: Math.max(0, entry.audioInputBudget.maxBytes - 44),
|
|
1403
1721
|
onError: handleStreamError,
|
|
1404
1722
|
onVerbose: logVoiceVerbose,
|
|
1405
1723
|
onWarn: (message) => logger$3.warn(message)
|
|
@@ -1420,6 +1738,7 @@ var DiscordVoiceReceive = class {
|
|
|
1420
1738
|
logVoiceVerbose(`capture ready (${durationSeconds.toFixed(2)}s): guild ${entry.guildId} channel ${entry.channelId} user ${userId}`);
|
|
1421
1739
|
entry.processingQueue = entry.processingQueue.then(async () => {
|
|
1422
1740
|
if (!this.params.isEntryCurrent(entry)) return;
|
|
1741
|
+
if (entry.transcripts !== transcripts) return;
|
|
1423
1742
|
await this.processSegment({
|
|
1424
1743
|
entry,
|
|
1425
1744
|
wavPath,
|
|
@@ -1431,8 +1750,9 @@ var DiscordVoiceReceive = class {
|
|
|
1431
1750
|
if (!receiveFailureHandled) this.handleReceiveError(entry, err);
|
|
1432
1751
|
throw err;
|
|
1433
1752
|
} finally {
|
|
1434
|
-
stream.
|
|
1435
|
-
|
|
1753
|
+
const stream = capture.stream;
|
|
1754
|
+
stream?.off?.("error", handleStreamError);
|
|
1755
|
+
if (finishVoiceCapture(entry.capture, userId, capture) && stream && !stream.destroyed) stream.destroy();
|
|
1436
1756
|
}
|
|
1437
1757
|
}
|
|
1438
1758
|
async processSegment(params) {
|
|
@@ -1613,6 +1933,17 @@ const REALTIME_PLAYBACK_MAX_MISSED_FRAMES = 100;
|
|
|
1613
1933
|
function isVoiceSessionStopped(entry) {
|
|
1614
1934
|
return entry.sessionLifecycle.status === "stopped";
|
|
1615
1935
|
}
|
|
1936
|
+
function retireTranscripts(entry) {
|
|
1937
|
+
const transcripts = entry.transcripts;
|
|
1938
|
+
entry.transcripts = void 0;
|
|
1939
|
+
try {
|
|
1940
|
+
Promise.resolve(transcripts?.onStop?.()).catch((error) => {
|
|
1941
|
+
logger$2.warn(`discord voice: transcripts retirement failed: ${formatErrorMessage(error)}`);
|
|
1942
|
+
});
|
|
1943
|
+
} catch (error) {
|
|
1944
|
+
logger$2.warn(`discord voice: transcripts retirement failed: ${formatErrorMessage(error)}`);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1616
1947
|
function isVoiceConnectionDestroyed(connection, voiceSdk) {
|
|
1617
1948
|
return connection.state.status === voiceSdk.VoiceConnectionStatus.Destroyed;
|
|
1618
1949
|
}
|
|
@@ -1699,7 +2030,10 @@ var DiscordVoiceSessions = class {
|
|
|
1699
2030
|
if (existing && existing.channelId === channelId) {
|
|
1700
2031
|
existing.autoJoinWhenOccupied = options?.autoJoinWhenOccupied === true;
|
|
1701
2032
|
if (authority) existing.generation = authority.generation;
|
|
1702
|
-
if (options?.transcripts)
|
|
2033
|
+
if (options?.transcripts) {
|
|
2034
|
+
retireTranscripts(existing);
|
|
2035
|
+
existing.transcripts = options.transcripts;
|
|
2036
|
+
}
|
|
1703
2037
|
if (!options?.transcripts && isDiscordRealtimeVoiceMode(voiceMode) && existing.realtimeLifecycle.status !== "active" && existing.realtimeLifecycle.status !== "starting") {
|
|
1704
2038
|
const realtimeResult = await this.attachRealtimeSession(existing, voiceMode, {
|
|
1705
2039
|
requireLiveEntry: true,
|
|
@@ -1751,6 +2085,8 @@ var DiscordVoiceSessions = class {
|
|
|
1751
2085
|
ok: false,
|
|
1752
2086
|
message: "Discord voice plugin is not available."
|
|
1753
2087
|
};
|
|
2088
|
+
const audioInputBudget = await getDiscordRuntime().mediaUnderstanding.resolveAudioInputBudget({ cfg: this.params.cfg });
|
|
2089
|
+
if (authority && !authority.isCurrent()) return cancelledJoinResult();
|
|
1754
2090
|
const adapterCreator = voicePlugin.getGatewayAdapterCreator(guildId);
|
|
1755
2091
|
const daveEncryption = voiceConfig?.daveEncryption;
|
|
1756
2092
|
const decryptionFailureTolerance = voiceConfig?.decryptionFailureTolerance;
|
|
@@ -1862,6 +2198,7 @@ var DiscordVoiceSessions = class {
|
|
|
1862
2198
|
status: "stopped",
|
|
1863
2199
|
reason: optionsLocal.reason
|
|
1864
2200
|
};
|
|
2201
|
+
retireTranscripts(entry);
|
|
1865
2202
|
if (this.params.sessions.get(guildId) === entry) this.params.sessions.delete(guildId);
|
|
1866
2203
|
this.params.membership.deactivate(entry);
|
|
1867
2204
|
connection.receiver.speaking.off("start", speakingHandler);
|
|
@@ -1900,6 +2237,7 @@ var DiscordVoiceSessions = class {
|
|
|
1900
2237
|
player,
|
|
1901
2238
|
playbackQueue: Promise.resolve(),
|
|
1902
2239
|
processingQueue: Promise.resolve(),
|
|
2240
|
+
audioInputBudget,
|
|
1903
2241
|
ttsStreamFallbackWarned: false,
|
|
1904
2242
|
capture: createVoiceCaptureState(),
|
|
1905
2243
|
transcripts: options?.transcripts,
|
|
@@ -2004,7 +2342,7 @@ var DiscordVoiceSessions = class {
|
|
|
2004
2342
|
channelId: entry.channelId
|
|
2005
2343
|
};
|
|
2006
2344
|
if (entry.realtimeLifecycle.status === "active" || entry.realtimeLifecycle.status === "starting") {
|
|
2007
|
-
entry
|
|
2345
|
+
retireTranscripts(entry);
|
|
2008
2346
|
return {
|
|
2009
2347
|
ok: true,
|
|
2010
2348
|
message: `Stopped transcripts for ${formatMention({ channelId: entry.channelId })}.`,
|
|
@@ -2034,7 +2372,7 @@ var DiscordVoiceSessions = class {
|
|
|
2034
2372
|
ok: false,
|
|
2035
2373
|
message: "Discord realtime voice session stopped before startup completed."
|
|
2036
2374
|
};
|
|
2037
|
-
const { DiscordRealtimeVoiceSession } = await import("./realtime-session.runtime-
|
|
2375
|
+
const { DiscordRealtimeVoiceSession } = await import("./realtime-session.runtime-CGYm1t_0.js");
|
|
2038
2376
|
const realtime = new DiscordRealtimeVoiceSession({
|
|
2039
2377
|
accountId: this.params.accountId,
|
|
2040
2378
|
bootstrapContextInstructions,
|
|
@@ -2138,6 +2476,13 @@ var DiscordVoiceStateUpdateListener = class extends VoiceStateUpdateListener {
|
|
|
2138
2476
|
};
|
|
2139
2477
|
//#endregion
|
|
2140
2478
|
//#region extensions/discord/src/voice/voice-runtime.ts
|
|
2479
|
+
var voice_runtime_exports = /* @__PURE__ */ __exportAll({
|
|
2480
|
+
DiscordVoiceGuildCreateListener: () => DiscordVoiceGuildCreateListener,
|
|
2481
|
+
DiscordVoiceManager: () => DiscordVoiceManager,
|
|
2482
|
+
DiscordVoiceReadyListener: () => DiscordVoiceReadyListener,
|
|
2483
|
+
DiscordVoiceResumedListener: () => DiscordVoiceResumedListener,
|
|
2484
|
+
DiscordVoiceStateUpdateListener: () => DiscordVoiceStateUpdateListener
|
|
2485
|
+
});
|
|
2141
2486
|
const logger = createSubsystemLogger("discord/voice");
|
|
2142
2487
|
const DISCORD_VOICE_FATAL_AUTOJOIN_ERROR_PATTERNS = [
|
|
2143
2488
|
"api key missing",
|
|
@@ -2166,6 +2511,7 @@ var DiscordVoiceManager = class {
|
|
|
2166
2511
|
this.joinTasks = /* @__PURE__ */ new Map();
|
|
2167
2512
|
this.autoJoinTasks = /* @__PURE__ */ new Map();
|
|
2168
2513
|
this.fatalAutoJoinFailures = /* @__PURE__ */ new Map();
|
|
2514
|
+
this.occupancyWatchers = /* @__PURE__ */ new Set();
|
|
2169
2515
|
this.destroyed = false;
|
|
2170
2516
|
this.client = params.client;
|
|
2171
2517
|
this.botUserId = params.botUserId;
|
|
@@ -2245,8 +2591,42 @@ var DiscordVoiceManager = class {
|
|
|
2245
2591
|
refreshGuildRoster(guildId) {
|
|
2246
2592
|
this.voiceSessions.refreshGuildRoster(guildId);
|
|
2247
2593
|
}
|
|
2594
|
+
watchChannelOccupancy(params, listener) {
|
|
2595
|
+
if (this.destroyed) return () => void 0;
|
|
2596
|
+
const guildId = params.guildId.trim();
|
|
2597
|
+
const channelId = params.channelId.trim();
|
|
2598
|
+
let wasOccupied = false;
|
|
2599
|
+
const watcher = {
|
|
2600
|
+
guildId,
|
|
2601
|
+
refresh: () => {
|
|
2602
|
+
const states = listDiscordVoiceParticipantStates({
|
|
2603
|
+
client: this.client,
|
|
2604
|
+
guildId,
|
|
2605
|
+
channelId
|
|
2606
|
+
});
|
|
2607
|
+
if (states === null) return;
|
|
2608
|
+
const occupied = countDiscordVoiceHumanParticipants({
|
|
2609
|
+
states,
|
|
2610
|
+
botUserId: this.botUserId
|
|
2611
|
+
}) > 0;
|
|
2612
|
+
if (occupied !== wasOccupied) {
|
|
2613
|
+
wasOccupied = occupied;
|
|
2614
|
+
listener({ occupied });
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
};
|
|
2618
|
+
this.occupancyWatchers.add(watcher);
|
|
2619
|
+
watcher.refresh();
|
|
2620
|
+
return () => {
|
|
2621
|
+
this.occupancyWatchers.delete(watcher);
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
reconcileChannelOccupancy(guildId) {
|
|
2625
|
+
for (const watcher of this.occupancyWatchers) if (!guildId || watcher.guildId === guildId) watcher.refresh();
|
|
2626
|
+
}
|
|
2248
2627
|
async autoJoin() {
|
|
2249
2628
|
if (!this.voiceEnabled || this.destroyed) return;
|
|
2629
|
+
this.reconcileChannelOccupancy();
|
|
2250
2630
|
const entriesByGuild = /* @__PURE__ */ new Map();
|
|
2251
2631
|
const duplicateGuilds = /* @__PURE__ */ new Set();
|
|
2252
2632
|
for (const entry of this.autoJoinChannels) {
|
|
@@ -2262,6 +2642,7 @@ var DiscordVoiceManager = class {
|
|
|
2262
2642
|
await this.following.startReconciliation();
|
|
2263
2643
|
}
|
|
2264
2644
|
async reconcileAutoJoinGuild(guildId) {
|
|
2645
|
+
this.reconcileChannelOccupancy(guildId);
|
|
2265
2646
|
const entry = this.resolveAutoJoinTarget(guildId);
|
|
2266
2647
|
if (!entry?.whenOccupied || !this.voiceEnabled || this.destroyed) return;
|
|
2267
2648
|
await this.enqueueAutoJoin(entry);
|
|
@@ -2386,6 +2767,10 @@ var DiscordVoiceManager = class {
|
|
|
2386
2767
|
guildId,
|
|
2387
2768
|
channelId
|
|
2388
2769
|
}));
|
|
2770
|
+
return {
|
|
2771
|
+
...result,
|
|
2772
|
+
...entry.channelName ? { channelName: entry.channelName } : {}
|
|
2773
|
+
};
|
|
2389
2774
|
} else if (!result.ok && isCurrent()) this.guildLifecycles.set(guildId, {
|
|
2390
2775
|
status: "inactive",
|
|
2391
2776
|
generation
|
|
@@ -2453,6 +2838,7 @@ var DiscordVoiceManager = class {
|
|
|
2453
2838
|
return;
|
|
2454
2839
|
}
|
|
2455
2840
|
this.membership.track(this.sessions.get(guildId), data, previousVoiceState);
|
|
2841
|
+
this.reconcileChannelOccupancy(guildId);
|
|
2456
2842
|
if (this.following.isFollowedUser(userId)) await this.following.handleFollowedUserVoiceStateUpdate({
|
|
2457
2843
|
guildId,
|
|
2458
2844
|
channelId,
|
|
@@ -2463,6 +2849,7 @@ var DiscordVoiceManager = class {
|
|
|
2463
2849
|
}
|
|
2464
2850
|
async destroy() {
|
|
2465
2851
|
this.destroyed = true;
|
|
2852
|
+
this.occupancyWatchers.clear();
|
|
2466
2853
|
this.following.destroy();
|
|
2467
2854
|
for (const entry of this.sessions.values()) entry.stop();
|
|
2468
2855
|
for (const [guildId, lifecycle] of this.guildLifecycles) this.guildLifecycles.set(guildId, {
|
|
@@ -2538,4 +2925,4 @@ var DiscordVoiceManager = class {
|
|
|
2538
2925
|
}
|
|
2539
2926
|
};
|
|
2540
2927
|
//#endregion
|
|
2541
|
-
export { DiscordVoiceGuildCreateListener, DiscordVoiceManager, DiscordVoiceReadyListener, DiscordVoiceResumedListener, DiscordVoiceStateUpdateListener };
|
|
2928
|
+
export { DiscordVoiceGuildCreateListener, DiscordVoiceManager, DiscordVoiceReadyListener, DiscordVoiceResumedListener, DiscordVoiceStateUpdateListener, createDiscordOpusEncodeStream as a, formatVoiceLogPreview as c, convertRealtimePcm24kMonoToDiscordPcm48kStereo as i, loadDiscordVoiceSdk as l, logVoiceVerbose as n, formatVoiceIngressPrompt as o, convertDiscordPcm48kStereoToRealtimePcm24kMono as r, maybeControlDiscordVoiceAgentRun as s, voice_runtime_exports as t };
|