fix(discord): keep realtime partial transcript tail UTF-16 safe (#104896)

* fix(discord): keep realtime partial transcript tail UTF-16 safe

* refactor(discord): isolate realtime transcript merging

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
dwc1997
2026-07-13 01:47:05 +08:00
committed by GitHub
parent 7ea0af14f4
commit 2ebf0422db
3 changed files with 44 additions and 10 deletions

View File

@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { mergeRealtimePartialTranscript } from "./realtime-transcript.js";
describe("mergeRealtimePartialTranscript", () => {
it("returns previous transcript when the next chunk is blank", () => {
expect(mergeRealtimePartialTranscript("hello", " ")).toBe("hello");
});
it("replaces with the growing chunk when it extends the previous prefix", () => {
expect(mergeRealtimePartialTranscript("hel", "hello world")).toBe("hello world");
});
it("appends when the next chunk is not a continuation of previous", () => {
expect(mergeRealtimePartialTranscript("hello", " there")).toBe("hello there");
});
it("does not split a surrogate pair at the tail cap boundary", () => {
const tail = "x".repeat(239);
const next = `${"y".repeat(50)}🦞${tail}`;
expect(mergeRealtimePartialTranscript("", next)).toBe(tail);
});
it("keeps an intact surrogate pair that sits just inside the cap", () => {
const tail = `🦞${"w".repeat(238)}`;
const next = `${"z".repeat(10)}${tail}`;
expect(mergeRealtimePartialTranscript("", next)).toBe(tail);
});
});

View File

@@ -0,0 +1,13 @@
// Discord plugin module owns realtime transcript accumulation.
import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
const PARTIAL_TRANSCRIPT_MAX_CHARS = 240;
export function mergeRealtimePartialTranscript(previous: string, next: string): string {
const trimmed = next.trim();
if (!trimmed) {
return previous;
}
const merged = trimmed.startsWith(previous) ? trimmed : `${previous}${next}`;
return sliceUtf16Safe(merged, -PARTIAL_TRANSCRIPT_MAX_CHARS);
}

View File

@@ -53,6 +53,7 @@ import {
} from "./audio.js";
import { formatVoiceLogPreview } from "./log-preview.js";
import { formatVoiceIngressPrompt } from "./prompt.js";
import { mergeRealtimePartialTranscript } from "./realtime-transcript.js";
import { loadDiscordVoiceSdk } from "./sdk-runtime.js";
import {
logVoiceVerbose,
@@ -88,7 +89,6 @@ const DISCORD_REALTIME_DUPLICATE_ERROR_SUPPRESS_MS = 60_000;
const DISCORD_REALTIME_CONTROL_SPEECH_DEDUPE_MS = 5_000;
const DISCORD_REALTIME_OUTPUT_PLAYBACK_WATCHDOG_MARGIN_MS = 1_500;
const DISCORD_REALTIME_WAKE_ACKS = ["Yeah.", "Mm-hmm.", "Got it.", "One sec."];
const DISCORD_REALTIME_PARTIAL_TRANSCRIPT_MAX_CHARS = 240;
const REALTIME_PCM16_BYTES_PER_SAMPLE = 2;
const DISCORD_RAW_PCM_FRAME_BYTES = 3_840;
const DISCORD_REALTIME_OUTPUT_PREROLL_FRAMES = 25;
@@ -314,15 +314,6 @@ function normalizeControlSpeechText(text: string): string {
return text.toLowerCase().replace(/\s+/g, " ").trim();
}
function mergeRealtimePartialTranscript(previous: string, next: string): string {
const trimmed = next.trim();
if (!trimmed) {
return previous;
}
const merged = trimmed.startsWith(previous) ? trimmed : `${previous}${next}`;
return merged.slice(-DISCORD_REALTIME_PARTIAL_TRANSCRIPT_MAX_CHARS);
}
function resolveDiscordRealtimeWakeNames(params: {
config: DiscordRealtimeVoiceConfig;
cfg: OpenClawConfig;