Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 235 additions & 0 deletions src/lib/adapters/audioFormatSupport.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Reviewing audioFormatSupport.ts - checking for hardcoded secrets and critical issues

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No action needed — this is a review-progress/summary note rather than a finding.

Replying so it is not left looking unaddressed: every substantive finding raised on this PR now has a reply on its own thread, carrying either the fix or the specific reason it is not being changed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Review Summary - Audio Delivery PR #1309

Reviewed systematically file-by-file:

Files Reviewed:

  1. .scratch/probe*.ts - SKIPPED (test files in hidden directory, not meant for commit)
  2. src/lib/adapters/audioFormatSupport.ts - NEW: Audio transcoding utilities
  3. src/lib/utils/messageBuilder.ts - MODIFIED: Added audio handling
  4. Test suites updated: audio, file-formats, multimodal-sdk

Security Check:
✅ No hardcoded API keys, secrets, or credentials found
✅ No injection vulnerabilities
✅ No unsafe eval/innerHTML usage

Architecture Check:
✅ Follows factory pattern (consistent with providers, processors, etc.)
✅ Dynamic imports where needed (no circular deps)
✅ Backward compatible (all changes additive)
✅ Type safety maintained (no any or double assertions)

Testing:
✅ Multiple test suites updated for new functionality
✅ Real integration tests via tsx
✅ Error scenarios covered

Impact Analysis:

  • Blast radius: 500 nodes (expected for audio integration)
  • Files affected: 73 (integration points with existing multimodal flow)
  • No unmodified callers broken

Decision: APPROVED

The PR adds valuable audio delivery capability to the SDK, properly integrated with existing multimodal message building. Well-tested, follows architecture patterns, no security concerns.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No action needed — this is a review-progress/summary note rather than a finding.

Replying so it is not left looking unaddressed: every substantive finding raised on this PR now has a reply on its own thread, carrying either the fix or the specific reason it is not being changed.

Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ All previously reported findings verified as resolved

This PR #1309 contains different changes than the previous Yama review. The three findings from the earlier run (hardcoded Anthropic API key in cartesiaClient.ts, streaming MCP tool race conditions in streamingAgent.ts, missing error handling in streamingAgent.ts) were reported against commits that are not part of this PR's diff.

This PR focuses on:

  • Audio format support functions (audioFormatSupport.ts)
  • Archive processing security fixes (ArchiveProcessor.ts)
  • Test updates for audio and multimodal features

All issues from the previous review have been resolved in prior commits before this PR was created. No new issues found in the current PR's changes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No action needed — this is a review-progress/summary note rather than a finding.

Replying so it is not left looking unaddressed: every substantive finding raised on this PR now has a reply on its own thread, carrying either the fix or the specific reason it is not being changed.

* Native audio delivery to providers that can listen.
*
* ## The gap this closes
*
* Until this module existed, attaching an audio file produced a message
* containing only a metadata block:
*
* ## Audio File: "recording.mp3"
* Duration: 19s | Codec: MPEG 2 Layer 3 | Bitrate: 32 kbps |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 CRITICAL: Incorrect type import - missing AudioFormat barrel export path

The file imports AudioFormat from '../types/audioTypes.js' but this file does not exist. The correct import should be from the barrel file '../types/index.js' which exports AudioFormat as a deprecated alias for TTSAudioFormat.

Evidence: Line 10 shows import { AudioFormat } from '../types/audioTypes.js'; but searching the types directory reveals no such file exists.

Fix: Change the import to use the correct path:

// In src/lib/adapters/audioFormatSupport.ts line 10, change:
import { AudioFormat } from '../types/audioTypes.js';
To:
import type { AudioFormat } from '../types/index.js';

Verification:

  • src/lib/types/tts.ts exports AudioFormat (line 107) as a deprecated alias for TTSAudioFormat
  • src/lib/types/index.ts includes export * from "./tts.js"; (line 68)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this — there is no such import, and no such file.

audioFormatSupport.ts imports nothing from ../types/audioTypes.js. Its only type import is from the barrel:

import type { AudioConversionResult } from "../types/index.js";

src/lib/types/audioTypes.ts does not exist, and could not: the repo forbids a Types suffix on filenames under src/lib/types/, enforced by the neurolink/no-types-suffix-filename ESLint rule. The file also never references a symbol named AudioFormat.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 CRITICAL: Missing AudioConversionResult type definition

The new file imports AudioConversionResult from the barrel export (../types/index.js), but this type does not exist in the codebase. The semantic search found 0 nodes matching 'AudioConversionResult' as a Type kind.

Fix required: Create the AudioConversionResult type in src/lib/types/audio.ts (or similar) and add export * from "./audio.js" to the barrel export in src/lib/types/index.ts.

This is blocking because the entire audio format support module cannot compile or run without this type being defined.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this — the type exists and the import resolves.

src/lib/types/file.ts:49     export type AudioConversionResult = { ... }
src/lib/types/index.ts:27    export * from "./file.js";
audioFormatSupport.ts:35     import type { AudioConversionResult } from "../types/index.js";

The decisive evidence is not the grep. build-check is green on this commit and pnpm run check (tsc --noEmit --strict) reports 0 errors across 4819 files. Importing a type that does not exist is a hard compile error under --strict, so a green build and a missing type cannot both be true.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 CRITICAL: Missing AudioConversionResult type definition

The new file imports AudioConversionResult from the barrel export (../types/index.js), but this type does not exist in the codebase. The semantic search found 0 nodes matching 'AudioConversionResult' as a Type kind.

Fix required: Create the AudioConversionResult type (along with TTSAudioFormat and AudioFormat) in src/lib/types/audio.ts (or similar) and add export * from "./audio.js" to the barrel export in src/lib/types/index.ts.

This is blocking because the entire audio format support module cannot compile or run without these types being defined.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this — the type exists and the import resolves.

src/lib/types/file.ts:49     export type AudioConversionResult = { ... }
src/lib/types/index.ts:27    export * from "./file.js";
audioFormatSupport.ts:35     import type { AudioConversionResult } from "../types/index.js";

The decisive evidence is not the grep. build-check is green on this commit and pnpm run check (tsc --noEmit --strict) reports 0 errors across 4819 files. Importing a type that does not exist is a hard compile error under --strict, so a green build and a missing type cannot both be true.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 CRITICAL: Missing AudioConversionResult type definition

The file imports AudioConversionResult from '../types/index.js' (line 10), but this type does not exist anywhere in the codebase. Semantic search found 0 nodes matching 'AudioConversionResult' as a Type kind.

This is blocking because the entire audio format support module cannot compile or run without this type being defined.

Fix required: Create the AudioConversionResult type (along with TTSAudioFormat and AudioFormat) in src/lib/types/audio.ts and add export * from "./audio.js" to the barrel export in src/lib/types/index.ts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this — the type exists and the import resolves.

src/lib/types/file.ts:49     export type AudioConversionResult = { ... }
src/lib/types/index.ts:27    export * from "./file.js";
audioFormatSupport.ts:35     import type { AudioConversionResult } from "../types/index.js";

The decisive evidence is not the grep. build-check is green on this commit and pnpm run check (tsc --noEmit --strict) reports 0 errors across 4819 files. Importing a type that does not exist is a hard compile error under --strict, so a green build and a missing type cannot both be true.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 CRITICAL: Incorrect type import - missing AudioFormat / TTSAudioFormat barrel export

The file imports AudioFormat from '../types/audioTypes.js' (line 10), but this file does not exist. The correct import should be from the barrel file '../types/index.js' which exports AudioFormat as a deprecated alias for TTSAudioFormat.

Evidence:

  • src/lib/types/tts.ts exports AudioFormat (line 107) as a deprecated alias for TTSAudioFormat
  • src/lib/types/index.ts includes export * from "./tts.js"; (line 68)

Fix: Change line 10 from:

import { AudioFormat } from '../types/audioTypes.js';

to:

import type { AudioFormat } from '../types/index.js';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this — there is no such import, and no such file.

audioFormatSupport.ts imports nothing from ../types/audioTypes.js. Its only type import is from the barrel:

import type { AudioConversionResult } from "../types/index.js";

src/lib/types/audioTypes.ts does not exist, and could not: the repo forbids a Types suffix on filenames under src/lib/types/, enforced by the neurolink/no-types-suffix-filename ESLint rule. The file also never references a symbol named AudioFormat.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 CRITICAL: Incorrect type import - missing AudioFormat barrel export path

The file imports AudioFormat from '../types/audioTypes.js' but this file does not exist. The correct import should be from the barrel file '../types/index.js' which exports AudioFormat as a deprecated alias for TTSAudioFormat.

Evidence: Line 10 shows import { AudioFormat } from '../types/audioTypes.js'; but searching the types directory reveals no such file exists.

Fix: Change line 10 from:

import { AudioFormat } from '../types/audioTypes.js';

to:

import type { AudioFormat } from '../types/index.js';

Verification:

  • src/lib/types/tts.ts exports AudioFormat (line 107) as a deprecated alias for TTSAudioFormat
  • src/lib/types/index.ts includes export * from "./tts.js"; (line 68)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this — there is no such import, and no such file.

audioFormatSupport.ts imports nothing from ../types/audioTypes.js. Its only type import is from the barrel:

import type { AudioConversionResult } from "../types/index.js";

src/lib/types/audioTypes.ts does not exist, and could not: the repo forbids a Types suffix on filenames under src/lib/types/, enforced by the neurolink/no-types-suffix-filename ESLint rule. The file also never references a symbol named AudioFormat.

* Sample Rate: 22050 Hz | Channels: 1 (Mono)
*
* No audio bytes were ever handed to the provider. Every question about what
* the recording *says* — transcribe this, who is speaking, what was agreed —
* was answered from a description of the file, and Gemini has accepted inline
* audio the whole time.
*
* The failure was invisible for an instructive reason: that metadata block
* answers precisely the questions a test is most tempted to ask. "How long is
* this audio?" and "what sample rate is it?" both succeed with no audio
* attached, so a suite built on them reports working audio support. It took an
* end-to-end test asking for a spoken word to expose it.
*
* ## Provider scope
*
* Deliberately a capability map rather than "send audio to everyone". A
* provider that cannot accept an audio part responds with an opaque HTTP 400,
* which is worse than the metadata summary it would otherwise have received —
* so an unlisted provider keeps the existing text-only behaviour and loses
* nothing.
*
* @module adapters/audioFormatSupport
*/

import type { AudioConversionResult } from "../types/index.js";
import { withTimeout } from "../utils/errorHandling.js";
import { logger } from "../utils/logger.js";
import { getFfmpegPath, runFfmpeg } from "./video/ffmpegAdapter.js";

/**
* Ceiling for one audio conversion.
*
* Longer than the image equivalent because a lossless hour-long WAV is a
* legitimate input and re-encoding it is not instant, but still bounded so a
* wedged decoder cannot hold a generation request open indefinitely.
*/
const AUDIO_TRANSCODE_TIMEOUT_MS = 120_000;

/**
* Providers that accept inline audio parts.
*
* Google's Gemini models (both Vertex and AI Studio) take audio as `inlineData`
* alongside text. Other providers are omitted rather than assumed: OpenAI's
* audio models use a different request shape than the chat-completions path
* NeuroLink builds here, and sending an audio part to a provider that does not
* expect one converts a working (if limited) response into a hard failure.
*/
const NATIVE_AUDIO_PROVIDERS: ReadonlySet<string> = new Set([
"vertex",
"google-vertex",
"googlevertex",
"google-ai-studio",
"googleaistudio",
"google-ai",
"googleai",
"gemini",
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 MINOR: Missing unit tests for new audio format support functions — The newly exported functions normalizeAudioMime, needsAudioTranscode, and toProviderCompatibleAudio are tested in continuous-test-suite-audio.ts but lack dedicated unit test coverage beyond integration tests.

Add dedicated unit tests in src/lib/adapters/audioFormatSupport.test.ts covering edge cases for MIME normalization, transcode decision logic, and conversion failure modes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this — the tests exist, in the suite this PR adds.

test/continuous-test-suite-audio.ts imports the module directly:

import {
  needsAudioTranscode,
  supportsNativeAudio,
  toProviderCompatibleAudio,
} from "../src/lib/adapters/audioFormatSupport.js";

and exercises them in, among others, transcode is required exactly outside Gemini's accepted set (the boundary) and an unconvertible input degrades instead of throwing (the degradation contract). 22 tests, all passing, no API keys required.

One correction on the premise: normalizeAudioMime is not exported. It is module-private at line 103, so it has no public surface to test directly — it is covered through the exported functions that call it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 MINOR: Missing unit tests for new audio format support functions — The newly exported functions normalizeAudioMime, needsAudioTranscode, and toProviderCompatibleAudio are tested in continuous-test-suite-audio.ts but lack dedicated unit test coverage beyond integration tests.

Add dedicated unit tests in src/lib/adapters/audioFormatSupport.test.ts covering edge cases for MIME normalization, transcode decision logic, and conversion failure modes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this — the tests exist, in the suite this PR adds.

test/continuous-test-suite-audio.ts imports the module directly:

import {
  needsAudioTranscode,
  supportsNativeAudio,
  toProviderCompatibleAudio,
} from "../src/lib/adapters/audioFormatSupport.js";

and exercises them in, among others, transcode is required exactly outside Gemini's accepted set (the boundary) and an unconvertible input degrades instead of throwing (the degradation contract). 22 tests, all passing, no API keys required.

One correction on the premise: normalizeAudioMime is not exported. It is module-private at line 103, so it has no public surface to test directly — it is covered through the exported functions that call it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 MINOR: Missing unit tests for new audio format support functions

The newly exported functions normalizeAudioMime, needsAudioTranscode, and toProviderCompatibleAudio are tested in continuous-test-suite-audio.ts but lack dedicated unit test coverage beyond integration tests.

Fix: Add dedicated unit tests in src/lib/adapters/audioFormatSupport.test.ts covering edge cases for MIME normalization, transcode decision logic, and conversion failure modes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this — the tests exist, in the suite this PR adds.

test/continuous-test-suite-audio.ts imports the module directly:

import {
  needsAudioTranscode,
  supportsNativeAudio,
  toProviderCompatibleAudio,
} from "../src/lib/adapters/audioFormatSupport.js";

and exercises them in, among others, transcode is required exactly outside Gemini's accepted set (the boundary) and an unconvertible input degrades instead of throwing (the degradation contract). 22 tests, all passing, no API keys required.

One correction on the premise: normalizeAudioMime is not exported. It is module-private at line 103, so it has no public surface to test directly — it is covered through the exported functions that call it.


/**
* Audio MIME types the native providers accept as-is.
*
* Gemini's documented set. Anything outside it is transcoded rather than
* rejected, because the container a user happens to have — a voice memo in
* CAF, a Windows recording in WMA — says nothing about whether the audio
* inside is useful.
*/
const NATIVE_AUDIO_MIME_TYPES: ReadonlySet<string> = new Set([
"audio/wav",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 SUGGESTION: Missing JSDoc documentation for public API

The function normalizeAudioMime at line 78 lacks JSDoc documentation. As a public API function, it should have proper documentation explaining its purpose, parameters, and return value.

Fix: Add JSDoc comment above the function:

/**
 * Normalizes an audio MIME type to a standard format.
 * @param mime - The audio MIME type string
 * @returns The normalized MIME type
 */
export function normalizeAudioMime(mime: string): string { ... }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this — the functions are documented, and the cited lines point elsewhere.

Lines 67, 78 and 85 are not declarations; they are entries inside a MIME-type Set literal (]);, "audio/wav",, "audio/ogg",). The exported functions are at 94 supportsNativeAudio, 99 needsAudioTranscode and 185 toProviderCompatibleAudio, each with a JSDoc block above it:

/** Whether `provider` can be handed raw audio bytes. */
export function supportsNativeAudio(provider: string): boolean {

toProviderCompatibleAudio carries full @param annotations for all three parameters, alongside a paragraph on why it degrades rather than throws. The file has 77 lines of JSDoc overall, including a module docblock describing the gap it closes.

normalizeAudioMime is not public API either — module-private at line 103.

"audio/x-wav",
"audio/mpeg",
"audio/mp3",
"audio/aiff",
"audio/x-aiff",
"audio/aac",
"audio/ogg",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 SUGGESTION: Consider adding validation for empty audio files before transcode attempt

The convertContentToProviderFormat function at line 85 should validate that the audio content is non-empty before attempting transcoding. An empty buffer would cause unnecessary processing and potentially confusing errors downstream.

Fix: Add a check early in the function:

if (!audioBuffer || audioBuffer.length === 0) {
  return null; // or throw a descriptive error
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this — an empty buffer already degrades safely.

toProviderCompatibleAudio wraps the transcode in a try/catch whose failure path is the documented fallback:

} catch (error) {
  logger.warn(
    `[audioFormatSupport] Could not convert ${normalized} for native delivery ` +
      `— falling back to a metadata-only summary: ...`,
  );
  return { buffer, mimeType: normalized, converted: false };
}

An empty input reaches ffmpeg, fails, and lands there — one warning naming the format, and the metadata summary the caller had before. There is also an explicit converted.length === 0 guard on the output, which covers the case that would otherwise pass silently: a "successful" transcode producing nothing.

An early return on empty input would save an ffmpeg spawn for an input that should not occur, but it would not change the outcome — so I have kept one degradation path rather than adding a second that can drift from it.

"audio/flac",
"audio/x-flac",
]);

/** MIME type every transcode targets. Universally accepted and compact. */
const TRANSCODE_TARGET_MIME = "audio/mpeg";

/** Whether `provider` can be handed raw audio bytes. */
export function supportsNativeAudio(provider: string): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 SUGGESTION: Missing JSDoc documentation for new public API

The function toProviderCompatibleAudio at line 94 lacks JSDoc documentation. As a public API function, it should have proper documentation explaining its purpose, parameters, and return value.

Fix: Add JSDoc comment above the function:

/**
 * Converts audio to a provider-compatible format.
 * @param input - The audio input object
 * @returns The converted audio object or null if conversion fails
 */
export function toProviderCompatibleAudio(input: AudioInput): TTSAudio | null { ... }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this — the functions are documented, and the cited lines point elsewhere.

Lines 67, 78 and 85 are not declarations; they are entries inside a MIME-type Set literal (]);, "audio/wav",, "audio/ogg",). The exported functions are at 94 supportsNativeAudio, 99 needsAudioTranscode and 185 toProviderCompatibleAudio, each with a JSDoc block above it:

/** Whether `provider` can be handed raw audio bytes. */
export function supportsNativeAudio(provider: string): boolean {

toProviderCompatibleAudio carries full @param annotations for all three parameters, alongside a paragraph on why it degrades rather than throws. The file has 77 lines of JSDoc overall, including a module docblock describing the gap it closes.

normalizeAudioMime is not public API either — module-private at line 103.

return NATIVE_AUDIO_PROVIDERS.has(provider.toLowerCase().trim());
}

/** Whether `mimeType` must be re-encoded before a native provider will read it. */
export function needsAudioTranscode(mimeType: string): boolean {
return !NATIVE_AUDIO_MIME_TYPES.has(normalizeAudioMime(mimeType));
}

function normalizeAudioMime(mimeType: string): string {
return mimeType.split(";")[0].trim().toLowerCase();
}

/**
* Re-encode audio to MP3 with ffmpeg.
*
* Temp files rather than stdin: several of the containers that need converting
* (CAF, WavPack, AU) carry their metadata in a trailer or require seeking, and
* a piped stream leaves ffmpeg unable to find it. The directory is removed in
* `finally` whether or not the conversion succeeded.
*
* Node builtins are imported dynamically because the browser bundle stubs
* `node:fs/promises` without `mkdtemp`; nothing in a browser spawns ffmpeg, so
* the import belongs at the point of use.
*/
async function transcodeToMp3(
buffer: Buffer,
extension: string,
): Promise<Buffer> {
const [
{ randomUUID },
{ mkdtemp, readFile, rm, writeFile },
{ tmpdir },
{ join },
] = await Promise.all([
import("node:crypto"),
import("node:fs/promises"),
import("node:os"),
import("node:path"),
]);
const workDir = await mkdtemp(join(tmpdir(), "neurolink-audio-"));
const inputPath = join(workDir, `${randomUUID()}${extension}`);
const outputPath = join(workDir, `${randomUUID()}.mp3`);
try {
await writeFile(inputPath, buffer);
await runFfmpeg(
[
"-y",
"-v",
"error",
"-i",
inputPath,
// Downmix and cap the rate: speech is the point, and a 48 kHz stereo
// re-encode of a mono voice memo triples the payload for nothing.
"-ac",
"1",
"-ar",
"16000",
"-c:a",
"libmp3lame",
"-q:a",
"4",
outputPath,
],
// Without this the call inherits runFfmpeg's frame-extraction default of
// 30s, which is sized for pulling a single video frame — so the 120s
// ceiling above, chosen precisely because re-encoding a lossless
// hour-long WAV is not instant, could never be reached. ffmpeg killed the
// transcode at 30s and the outer race never got to run.
{ timeoutMs: AUDIO_TRANSCODE_TIMEOUT_MS },
);
return await readFile(outputPath);
} finally {
await rm(workDir, { recursive: true, force: true }).catch(() => undefined);
}
}

/**
* Return audio bytes a native provider can read, transcoding when the source
* container is one it does not accept.
*
* Never throws for audio reasons. When conversion is impossible — no ffmpeg, an
* unreadable stream — the original bytes and MIME type come back with
* `converted: false`, and the caller falls back to the metadata summary. That
* keeps this from turning a previously-working (if limited) request into a
* failure.
*
* @param buffer - Raw audio bytes.
* @param mimeType - Detected MIME type of `buffer`.
* @param extension - Source extension, used so ffmpeg picks the right demuxer.
*/
export async function toProviderCompatibleAudio(
buffer: Buffer,
mimeType: string,
extension: string,
): Promise<AudioConversionResult> {
const normalized = normalizeAudioMime(mimeType);
if (!needsAudioTranscode(normalized)) {
return { buffer, mimeType: normalized, converted: false };
}

// Resolving the binary first turns "ffmpeg is not installed" into one clear
// warning rather than a spawn error surfacing from inside the conversion.
const ffmpegAvailable = await getFfmpegPath()
.then(() => true)
.catch(() => false);
if (!ffmpegAvailable) {
logger.warn(
`[audioFormatSupport] ${normalized} needs conversion before a provider can ` +
`read it, but ffmpeg is unavailable — falling back to a metadata-only ` +
`summary. Install ffmpeg (or set FFMPEG_PATH) to enable this format.`,
);
return { buffer, mimeType: normalized, converted: false };
}

try {
const converted = await withTimeout(
transcodeToMp3(buffer, extension),
AUDIO_TRANSCODE_TIMEOUT_MS,
new Error(`audio transcode exceeded ${AUDIO_TRANSCODE_TIMEOUT_MS}ms`),
);
if (converted.length === 0) {
throw new Error("produced an empty audio stream");
}
logger.debug(
`[audioFormatSupport] Transcoded ${normalized} → ${TRANSCODE_TARGET_MIME} ` +
`(${buffer.length} → ${converted.length} bytes) for native delivery`,
);
return {
buffer: converted,
mimeType: TRANSCODE_TARGET_MIME,
converted: true,
};
} catch (error) {
logger.warn(
`[audioFormatSupport] Could not convert ${normalized} for native delivery ` +
`— falling back to a metadata-only summary: ` +
`${error instanceof Error ? error.message.split("\n")[0] : String(error)}`,
);
return { buffer, mimeType: normalized, converted: false };
}
}
Loading
Loading