





















@@ -459,8 +459,7 @@ describe("applyMediaUnderstanding", () => {
459459expect(ctx.Body).toBe("[Audio]\nTranscript:\nwhatsapp transcript");
460460});
461461462-it("skips URL-only audio when remote file is too small", async () => {
463-// Override the default mock to return a tiny buffer (below MIN_AUDIO_FILE_BYTES)
462+it("injects a placeholder transcript when URL-only audio is too small", async () => {
464463mockedFetchRemoteMedia.mockResolvedValueOnce({
465464buffer: Buffer.alloc(100),
466465contentType: "audio/ogg",
@@ -499,7 +498,66 @@ describe("applyMediaUnderstanding", () => {
499498});
500499501500expect(transcribeAudio).not.toHaveBeenCalled();
502-expect(result.appliedAudio).toBe(false);
501+expect(result.appliedAudio).toBe(true);
502+expect(result.outputs).toEqual([
503+expect.objectContaining({
504+kind: "audio.transcription",
505+text: "[Voice note could not be transcribed because the audio attachment was too small]",
506+provider: "openclaw",
507+model: "synthetic-empty-audio",
508+}),
509+]);
510+expect(ctx.Transcript).toBe(
511+"[Voice note could not be transcribed because the audio attachment was too small]",
512+);
513+expect(ctx.Body).toBe(
514+"[Audio]\nTranscript:\n[Voice note could not be transcribed because the audio attachment was too small]",
515+);
516+});
517+518+it("injects a placeholder transcript when local-path audio is too small", async () => {
519+const ctx = await createAudioCtx({
520+fileName: "tiny.ogg",
521+mediaType: "audio/ogg",
522+content: Buffer.alloc(100),
523+});
524+const transcribeAudio = vi.fn(async () => ({ text: "should-not-run" }));
525+const cfg: OpenClawConfig = {
526+tools: {
527+media: {
528+audio: {
529+enabled: true,
530+maxBytes: 1024 * 1024,
531+models: [{ provider: "groq" }],
532+},
533+},
534+},
535+};
536+537+const result = await applyMediaUnderstanding({
538+ ctx,
539+ cfg,
540+providers: {
541+groq: { id: "groq", transcribeAudio },
542+},
543+});
544+545+expect(transcribeAudio).not.toHaveBeenCalled();
546+expect(result.appliedAudio).toBe(true);
547+expect(result.outputs).toEqual([
548+expect.objectContaining({
549+kind: "audio.transcription",
550+text: "[Voice note could not be transcribed because the audio attachment was too small]",
551+provider: "openclaw",
552+model: "synthetic-empty-audio",
553+}),
554+]);
555+expect(ctx.Transcript).toBe(
556+"[Voice note could not be transcribed because the audio attachment was too small]",
557+);
558+expect(ctx.Body).toBe(
559+"[Audio]\nTranscript:\n[Voice note could not be transcribed because the audio attachment was too small]",
560+);
503561});
504562505563it("skips audio transcription when attachment exceeds maxBytes", async () => {
@@ -969,6 +1027,56 @@ describe("applyMediaUnderstanding", () => {
9691027);
9701028});
97110291030+it("adds placeholder for tooSmall audio while preserving real transcript for valid audio", async () => {
1031+const dir = await createTempMediaDir();
1032+const validAudio = createSafeAudioFixtureBuffer(2048);
1033+const tinyAudio = Buffer.alloc(100);
1034+const validPath = path.join(dir, "valid.ogg");
1035+const tinyPath = path.join(dir, "tiny.ogg");
1036+await fs.writeFile(validPath, validAudio);
1037+await fs.writeFile(tinyPath, tinyAudio);
1038+1039+const ctx: MsgContext = {
1040+Body: "<media:audio>",
1041+MediaPaths: [validPath, tinyPath],
1042+MediaTypes: ["audio/ogg", "audio/ogg"],
1043+};
1044+const cfg: OpenClawConfig = {
1045+tools: {
1046+media: {
1047+audio: {
1048+enabled: true,
1049+attachments: { mode: "all", maxAttachments: 2 },
1050+models: [{ provider: "groq" }],
1051+},
1052+},
1053+},
1054+};
1055+1056+const result = await applyMediaUnderstanding({
1057+ ctx,
1058+ cfg,
1059+providers: {
1060+groq: {
1061+id: "groq",
1062+transcribeAudio: async (req) => ({ text: `transcribed ${req.fileName ?? "unknown"}` }),
1063+},
1064+},
1065+});
1066+1067+expect(result.appliedAudio).toBe(true);
1068+expect(ctx.Transcript).toContain("transcribed valid.ogg");
1069+expect(ctx.Transcript).toContain(
1070+"[Voice note could not be transcribed because the audio attachment was too small]",
1071+);
1072+expect(ctx.Body).toContain("[Audio 1/2]");
1073+expect(ctx.Body).toContain("transcribed valid.ogg");
1074+expect(ctx.Body).toContain("[Audio 2/2]");
1075+expect(ctx.Body).toContain(
1076+"[Voice note could not be transcribed because the audio attachment was too small]",
1077+);
1078+});
1079+9721080it("orders mixed media outputs as image, audio, video", async () => {
9731081const dir = await createTempMediaDir();
9741082const imagePath = path.join(dir, "photo.jpg");
@@ -1028,6 +1136,68 @@ describe("applyMediaUnderstanding", () => {
10281136expect(ctx.BodyForCommands).toBe("audio ok");
10291137});
103011381139+it("orders synthetic too-small audio output between image and video", async () => {
1140+const dir = await createTempMediaDir();
1141+const imagePath = path.join(dir, "photo.jpg");
1142+const audioPath = path.join(dir, "silent.ogg");
1143+const videoPath = path.join(dir, "clip.mp4");
1144+await fs.writeFile(imagePath, "image-bytes");
1145+await fs.writeFile(audioPath, Buffer.alloc(100));
1146+await fs.writeFile(videoPath, "video-bytes");
1147+1148+const ctx: MsgContext = {
1149+Body: "<media:mixed>",
1150+MediaPaths: [imagePath, audioPath, videoPath],
1151+MediaTypes: ["image/jpeg", "audio/ogg", "video/mp4"],
1152+};
1153+const cfg: OpenClawConfig = {
1154+tools: {
1155+media: {
1156+image: { enabled: true, models: [{ provider: "openai", model: "gpt-5.4" }] },
1157+audio: { enabled: true, models: [{ provider: "groq" }] },
1158+video: { enabled: true, models: [{ provider: "google", model: "gemini-3" }] },
1159+},
1160+},
1161+};
1162+1163+const result = await applyMediaUnderstanding({
1164+ ctx,
1165+ cfg,
1166+agentDir: dir,
1167+providers: {
1168+openai: {
1169+id: "openai",
1170+describeImage: async () => ({ text: "image ok" }),
1171+},
1172+groq: {
1173+id: "groq",
1174+transcribeAudio: async () => ({ text: "audio should not run" }),
1175+},
1176+google: {
1177+id: "google",
1178+describeVideo: async () => ({ text: "video ok" }),
1179+},
1180+},
1181+});
1182+1183+const placeholder =
1184+"[Voice note could not be transcribed because the audio attachment was too small]";
1185+1186+expect(result.appliedImage).toBe(true);
1187+expect(result.appliedAudio).toBe(true);
1188+expect(result.appliedVideo).toBe(true);
1189+expect(ctx.Body).toBe(
1190+[
1191+"[Image]\nDescription:\nimage ok",
1192+`[Audio]\nTranscript:\n${placeholder}`,
1193+"[Video]\nDescription:\nvideo ok",
1194+].join("\n\n"),
1195+);
1196+expect(ctx.Transcript).toBe(placeholder);
1197+expect(ctx.CommandBody).toBe(placeholder);
1198+expect(ctx.BodyForCommands).toBe(placeholder);
1199+});
1200+10311201it("treats text-like attachments as CSV (comma wins over tabs)", async () => {
10321202const csvText = '"a","b"\t"c"\n"1","2"\t"3"';
10331203const csvPath = await createTempMediaFile({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。