fix(media): remap AAC uploads to M4A (#66446)

* fix(media): remap AAC uploads to M4A

* fix(media): remap AAC uploads to M4A
This commit is contained in:
Vincent Koc
2026-04-14 11:00:28 +01:00
committed by GitHub
parent e58d50b7a8
commit f4372613d8
3 changed files with 38 additions and 1 deletions

View File

@@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
- Agents/context engine: compact engine-owned sessions from the first tool-loop delta and preserve ingest fallback when `afterTurn` is absent, so long-running tool loops can stay bounded without dropping engine state. (#63555) Thanks @Bikkies.
- Discord/native commands: return the real status card for native `/status` interactions instead of falling through to the synthetic `✅ Done.` ack when the generic dispatcher produces no visible reply. (#54629) Thanks @tkozzer and @vincentkoc.
- Hooks/Ollama: let LLM-backed session-memory slug generation honor an explicit `agents.defaults.timeoutSeconds` override instead of always aborting after 15 seconds, so slow local Ollama runs stop silently dropping back to generic filenames. (#66237) Thanks @dmak and @vincentkoc.
- Media/transcription: remap `.aac` filenames to `.m4a` for OpenAI-compatible audio uploads so AAC voice notes stop failing MIME-sensitive transcription endpoints. (#66446) Thanks @ben-z.
## 2026.4.14-beta.1

View File

@@ -48,4 +48,26 @@ describe("transcribeOpenAiCompatibleAudio", () => {
expect(headers.get("version")).toBeNull();
expect(headers.get("user-agent")).toBeNull();
});
it("remaps AAC uploads to an M4A filename before submitting the form", async () => {
const { fetchFn, getRequest } = createRequestCaptureJsonFetch({ text: "ok" });
await transcribeOpenAiCompatibleAudio({
buffer: Buffer.from("audio"),
fileName: "voice-note.aac",
mime: "audio/aac",
apiKey: "test-key",
timeoutMs: 1000,
fetchFn,
provider: "openai",
defaultBaseUrl: "https://api.openai.com/v1",
defaultModel: "gpt-4o-transcribe",
});
const form = getRequest().init?.body;
expect(form).toBeInstanceOf(FormData);
const file = (form as FormData).get("file");
expect(file).toBeInstanceOf(File);
expect((file as File).name).toBe("voice-note.m4a");
});
});

View File

@@ -18,6 +18,20 @@ function resolveModel(model: string | undefined, fallback: string): string {
return trimmed || fallback;
}
function resolveUploadFileName(fileName?: string, mime?: string): string {
const trimmed = fileName?.trim();
const baseName = trimmed ? path.basename(trimmed) : "audio";
const lowerMime = mime?.trim().toLowerCase();
if (/\.aac$/i.test(baseName)) {
return `${baseName.slice(0, -4) || "audio"}.m4a`;
}
if (!path.extname(baseName) && lowerMime === "audio/aac") {
return `${baseName || "audio"}.m4a`;
}
return baseName;
}
export async function transcribeOpenAiCompatibleAudio(
params: OpenAiCompatibleAudioParams,
): Promise<AudioTranscriptionResult> {
@@ -40,7 +54,7 @@ export async function transcribeOpenAiCompatibleAudio(
const model = resolveModel(params.model, params.defaultModel);
const form = new FormData();
const fileName = params.fileName?.trim() || path.basename(params.fileName) || "audio";
const fileName = resolveUploadFileName(params.fileName, params.mime);
const bytes = new Uint8Array(params.buffer);
const blob = new Blob([bytes], {
type: params.mime ?? "application/octet-stream",