* Adjust fix path to use ESM import * normalize fix-path imports and usage across the app * extract path fix logic to utils for server and collector * add helpers * repin strip-ansi in collector * fix log for localWhisper lint
115 lines
3.4 KiB
JavaScript
115 lines
3.4 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
const { execSync, spawnSync } = require("child_process");
|
|
const { patchShellEnvironmentPath } = require("../../shell");
|
|
/**
|
|
* Custom FFMPEG wrapper class for audio file conversion.
|
|
* Replaces deprecated fluent-ffmpeg package.
|
|
* Locates ffmpeg binary and converts audio files to required
|
|
* WAV format (16k hz mono 32f) for Whisper transcription.
|
|
*
|
|
* @class FFMPEGWrapper
|
|
*/
|
|
class FFMPEGWrapper {
|
|
static _instance;
|
|
|
|
constructor() {
|
|
if (FFMPEGWrapper._instance) return FFMPEGWrapper._instance;
|
|
FFMPEGWrapper._instance = this;
|
|
this._ffmpegPath = null;
|
|
}
|
|
|
|
log(text, ...args) {
|
|
console.log(`\x1b[35m[FFMPEG]\x1b[0m ${text}`, ...args);
|
|
}
|
|
|
|
/**
|
|
* Locates ffmpeg binary.
|
|
* Uses fix-path on non-Windows platforms to ensure we can find ffmpeg.
|
|
*
|
|
* @returns {Promise<string>} Path to ffmpeg binary
|
|
* @throws {Error}
|
|
*/
|
|
async ffmpegPath() {
|
|
if (this._ffmpegPath) return this._ffmpegPath;
|
|
await patchShellEnvironmentPath();
|
|
|
|
try {
|
|
const which = process.platform === "win32" ? "where" : "which";
|
|
const result = execSync(`${which} ffmpeg`, { encoding: "utf8" }).trim();
|
|
const candidatePath = result?.split("\n")?.[0]?.trim();
|
|
if (!candidatePath) throw new Error("FFMPEG candidate path not found.");
|
|
if (!this.isValidFFMPEG(candidatePath))
|
|
throw new Error("FFMPEG candidate path is not valid ffmpeg binary.");
|
|
|
|
this.log(`Found FFMPEG binary at ${candidatePath}`);
|
|
this._ffmpegPath = candidatePath;
|
|
return this._ffmpegPath;
|
|
} catch (error) {
|
|
this.log(error.message);
|
|
}
|
|
|
|
throw new Error("FFMPEG binary not found.");
|
|
}
|
|
|
|
/**
|
|
* Validates that path points to a valid ffmpeg binary.
|
|
* Runs ffmpeg -version command.
|
|
*
|
|
* @param {string} pathToTest - Path of ffmpeg binary
|
|
* @returns {boolean}
|
|
*/
|
|
isValidFFMPEG(pathToTest) {
|
|
try {
|
|
if (!pathToTest || !fs.existsSync(pathToTest)) return false;
|
|
execSync(`"${pathToTest}" -version`, { encoding: "utf8", stdio: "pipe" });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts audio file to WAV format with required parameters for Whisper.
|
|
* Output: 16k hz, mono, 32bit float.
|
|
*
|
|
* @param {string} inputPath - Input path for audio file (any format supported by ffmpeg)
|
|
* @param {string} outputPath - Output path for converted file
|
|
* @returns {Promise<boolean>}
|
|
* @throws {Error} If ffmpeg binary cannot be found or conversion fails
|
|
*/
|
|
async convertAudioToWav(inputPath, outputPath) {
|
|
if (!fs.existsSync(inputPath))
|
|
throw new Error(`Input file ${inputPath} does not exist.`);
|
|
const outputDir = path.dirname(outputPath);
|
|
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
|
|
this.log(`Converting ${path.basename(inputPath)} to WAV format...`);
|
|
// Convert to 16k hz mono 32f
|
|
const result = spawnSync(
|
|
await this.ffmpegPath(),
|
|
[
|
|
"-i",
|
|
inputPath,
|
|
"-ar",
|
|
"16000",
|
|
"-ac",
|
|
"1",
|
|
"-acodec",
|
|
"pcm_f32le",
|
|
"-y",
|
|
outputPath,
|
|
],
|
|
{ encoding: "utf8" }
|
|
);
|
|
|
|
// ffmpeg writes progress to stderr
|
|
if (result.stderr) this.log(result.stderr.trim());
|
|
if (result.status !== 0) throw new Error(`FFMPEG conversion failed`);
|
|
this.log(`Conversion complete: ${path.basename(outputPath)}`);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
module.exports = { FFMPEGWrapper };
|