Merge branch 'fleet/story-d1-webm-remux'
This commit is contained in:
@@ -90,6 +90,9 @@ async function runJob(
|
||||
} else {
|
||||
rawAudioPath = await saveUploadedFile(input.buffer, input.filename, jobId);
|
||||
title = input.filename.replace(/\.[^.]+$/, '');
|
||||
// Remux webm files (MediaRecorder output) to fix Cues/duration
|
||||
const { remuxUpload } = await import('./remux.js');
|
||||
rawAudioPath = await remuxUpload(rawAudioPath, jobId);
|
||||
}
|
||||
|
||||
updateJob({ id: jobId, title });
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { execFile } from 'child_process';
|
||||
import { unlink, rename, mkdir } from 'fs/promises';
|
||||
import { join, extname } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
const REMUX_DIR = join(process.env.DATA_DIR ?? '/tmp/.whisper-pwa', 'remux');
|
||||
|
||||
async function ensureTmpDir() {
|
||||
if (!existsSync(REMUX_DIR)) await mkdir(REMUX_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
/** Wrap execFile in a promise without using util.promisify */
|
||||
function execFileAsync(cmd: string, args: string[]): Promise<{ stdout: string; stderr: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(cmd, args, (err, stdout, stderr) => {
|
||||
if (err) reject(err);
|
||||
else resolve({ stdout, stderr });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remux an uploaded webm file to fix Cues/duration metadata.
|
||||
*
|
||||
* Browser MediaRecorder produces opus-in-webm output without finalized
|
||||
* Cues (seeking index) and with unset/streaming duration. Downstream
|
||||
* ffmpeg audio-prep (prepareAudio) fails on such files with "duration
|
||||
* unknown" or corrupt-container errors.
|
||||
*
|
||||
* This function uses ffmpeg stream copy (no re-encode) to rewrite
|
||||
* container headers, generating proper Cues and finalizing duration.
|
||||
* Non-webm files pass through unchanged.
|
||||
*
|
||||
* @param inputPath - Path to the uploaded file
|
||||
* @param jobId - Job ID for temp file naming
|
||||
* @returns The (possibly remuxed) path — same as inputPath if remuxed in-place
|
||||
*/
|
||||
export async function remuxUpload(inputPath: string, jobId: string): Promise<string> {
|
||||
const ext = extname(inputPath).toLowerCase();
|
||||
|
||||
// Only remux webm files — other formats pass through
|
||||
if (ext !== '.webm') {
|
||||
return inputPath;
|
||||
}
|
||||
|
||||
await ensureTmpDir();
|
||||
const tmpPath = join(REMUX_DIR, `${jobId}.webm`);
|
||||
|
||||
try {
|
||||
await execFileAsync('ffmpeg', [
|
||||
'-y',
|
||||
'-i', inputPath,
|
||||
'-c', 'copy',
|
||||
'-fflags', '+genpts',
|
||||
'-f', 'webm',
|
||||
tmpPath
|
||||
]);
|
||||
|
||||
// Replace original with remuxed file
|
||||
await unlink(inputPath);
|
||||
await rename(tmpPath, inputPath);
|
||||
|
||||
return inputPath;
|
||||
} catch (err) {
|
||||
// Clean up temp file on failure
|
||||
await unlink(tmpPath).catch(() => {});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user