mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix(qqbot): remove native ffmpeg subprocess fallback
This commit is contained in:
@@ -3,18 +3,16 @@
|
||||
* 音频格式转换工具。
|
||||
*
|
||||
* Handles SILK ↔ PCM ↔ WAV ↔ MP3 conversions for QQ Bot voice messaging.
|
||||
* Prefers ffmpeg when available; falls back to WASM decoders (silk-wasm,
|
||||
* mpg123-decoder) for environments without native tooling.
|
||||
* Uses WASM decoders (silk-wasm, mpg123-decoder) and direct QQ-native uploads
|
||||
* without launching native subprocesses.
|
||||
*
|
||||
* Self-contained within engine/ — no framework SDK dependency.
|
||||
*/
|
||||
|
||||
import { execFile } from "node:child_process";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { formatErrorMessage } from "./format.js";
|
||||
import { debugLog, debugError, debugWarn } from "./log.js";
|
||||
import { detectFfmpeg, isWindows } from "./platform.js";
|
||||
import { normalizeLowercaseStringOrEmpty as normalizeLowercase } from "./string-normalize.js";
|
||||
|
||||
type SilkWasm = typeof import("silk-wasm");
|
||||
@@ -184,7 +182,7 @@ function normalizeFormats(formats: string[]): string[] {
|
||||
/**
|
||||
* Convert a local audio file to Base64-encoded SILK for QQ API upload.
|
||||
*
|
||||
* Attempts conversion via ffmpeg → WASM decoders → null fallback chain.
|
||||
* Attempts conversion via direct QQ-native upload → WASM decoders → null fallback chain.
|
||||
*/
|
||||
export async function audioFileToSilkBase64(
|
||||
filePath: string,
|
||||
@@ -234,25 +232,6 @@ export async function audioFileToSilkBase64(
|
||||
|
||||
const targetRate = 24000;
|
||||
|
||||
const ffmpegCmd = await detectFfmpeg();
|
||||
if (ffmpegCmd) {
|
||||
try {
|
||||
debugLog(
|
||||
`[audio-convert] ffmpeg (${ffmpegCmd}): converting ${ext} (${buf.length} bytes) → PCM s16le ${targetRate}Hz`,
|
||||
);
|
||||
const pcmBuf = await ffmpegToPCM(ffmpegCmd, filePath, targetRate);
|
||||
if (pcmBuf.length === 0) {
|
||||
debugError(`[audio-convert] ffmpeg produced empty PCM output`);
|
||||
return null;
|
||||
}
|
||||
const { silkBuffer } = await pcmToSilk(pcmBuf, targetRate);
|
||||
debugLog(`[audio-convert] ffmpeg: ${ext} → SILK done (${silkBuffer.length} bytes)`);
|
||||
return silkBuffer.toString("base64");
|
||||
} catch (err) {
|
||||
debugError(`[audio-convert] ffmpeg conversion failed: ${formatErrorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
debugLog(`[audio-convert] fallback: trying WASM decoders for ${ext}`);
|
||||
|
||||
if (ext === ".pcm") {
|
||||
@@ -278,12 +257,9 @@ export async function audioFileToSilkBase64(
|
||||
}
|
||||
}
|
||||
|
||||
const installHint = isWindows()
|
||||
? "Install ffmpeg with choco install ffmpeg, scoop install ffmpeg, or from https://ffmpeg.org"
|
||||
: process.platform === "darwin"
|
||||
? "Install ffmpeg with brew install ffmpeg"
|
||||
: "Install ffmpeg with sudo apt install ffmpeg or sudo yum install ffmpeg";
|
||||
debugError(`[audio-convert] unsupported format: ${ext} (no ffmpeg available). ${installHint}`);
|
||||
debugError(
|
||||
`[audio-convert] unsupported format without native subprocess conversion: ${ext}. Use QQ-native voice formats or WAV/MP3/PCM inputs.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -386,48 +362,7 @@ async function pcmToSilk(
|
||||
};
|
||||
}
|
||||
|
||||
/** Use ffmpeg to convert any audio to mono 24 kHz PCM s16le. */
|
||||
function ffmpegToPCM(
|
||||
ffmpegCmd: string,
|
||||
inputPath: string,
|
||||
sampleRate: number = 24000,
|
||||
): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = [
|
||||
"-i",
|
||||
inputPath,
|
||||
"-f",
|
||||
"s16le",
|
||||
"-ar",
|
||||
String(sampleRate),
|
||||
"-ac",
|
||||
"1",
|
||||
"-acodec",
|
||||
"pcm_s16le",
|
||||
"-v",
|
||||
"error",
|
||||
"pipe:1",
|
||||
];
|
||||
execFile(
|
||||
ffmpegCmd,
|
||||
args,
|
||||
{
|
||||
maxBuffer: 50 * 1024 * 1024,
|
||||
encoding: "buffer",
|
||||
...(isWindows() ? { windowsHide: true } : {}),
|
||||
},
|
||||
(err, stdout) => {
|
||||
if (err) {
|
||||
reject(new Error(`ffmpeg failed: ${err.message}`));
|
||||
return;
|
||||
}
|
||||
resolve(stdout as unknown as Buffer);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Decode MP3 to PCM via mpg123-decoder WASM (fallback when ffmpeg is unavailable). */
|
||||
/** Decode MP3 to PCM via mpg123-decoder WASM. */
|
||||
async function wasmDecodeMp3ToPCM(buf: Buffer, targetRate: number): Promise<Buffer | null> {
|
||||
try {
|
||||
const { MPEGDecoder } = await import("mpg123-decoder");
|
||||
@@ -502,7 +437,7 @@ async function wasmDecodeMp3ToPCM(buf: Buffer, targetRate: number): Promise<Buff
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse a standard PCM WAV and extract mono 24 kHz PCM data (fallback without ffmpeg). */
|
||||
/** Parse a standard PCM WAV and extract mono 24 kHz PCM data. */
|
||||
export function parseWavFallback(buf: Buffer): Buffer | null {
|
||||
if (buf.length < 44) {
|
||||
return null;
|
||||
|
||||
@@ -12,9 +12,7 @@ import {
|
||||
getHomeDir,
|
||||
getTempDir,
|
||||
getQQBotDataDir,
|
||||
getPlatform,
|
||||
isWindows,
|
||||
detectFfmpeg,
|
||||
checkSilkWasmAvailable,
|
||||
} from "./platform.js";
|
||||
|
||||
@@ -25,7 +23,6 @@ interface DiagnosticReport {
|
||||
homeDir: string;
|
||||
tempDir: string;
|
||||
dataDir: string;
|
||||
ffmpeg: string | null;
|
||||
silkWasm: boolean;
|
||||
warnings: string[];
|
||||
}
|
||||
@@ -44,17 +41,6 @@ export async function runDiagnostics(): Promise<DiagnosticReport> {
|
||||
const tempDir = getTempDir();
|
||||
const dataDir = getQQBotDataDir();
|
||||
|
||||
const ffmpegPath = await detectFfmpeg();
|
||||
if (!ffmpegPath) {
|
||||
warnings.push(
|
||||
isWindows()
|
||||
? "⚠️ ffmpeg is not installed. Audio/video conversion will be limited. Install it with choco install ffmpeg, scoop install ffmpeg, or from https://ffmpeg.org."
|
||||
: getPlatform() === "darwin"
|
||||
? "⚠️ ffmpeg is not installed. Audio/video conversion will be limited. Install it with brew install ffmpeg."
|
||||
: "⚠️ ffmpeg is not installed. Audio/video conversion will be limited. Install it with sudo apt install ffmpeg or sudo yum install ffmpeg.",
|
||||
);
|
||||
}
|
||||
|
||||
const silkWasm = await checkSilkWasmAvailable();
|
||||
if (!silkWasm) {
|
||||
warnings.push(
|
||||
@@ -85,7 +71,6 @@ export async function runDiagnostics(): Promise<DiagnosticReport> {
|
||||
homeDir,
|
||||
tempDir,
|
||||
dataDir,
|
||||
ffmpeg: ffmpegPath,
|
||||
silkWasm,
|
||||
warnings,
|
||||
};
|
||||
@@ -95,7 +80,6 @@ export async function runDiagnostics(): Promise<DiagnosticReport> {
|
||||
debugLog(` Node: ${nodeVersion}`);
|
||||
debugLog(` Home: ${homeDir}`);
|
||||
debugLog(` Data dir: ${dataDir}`);
|
||||
debugLog(` ffmpeg: ${ffmpegPath ?? "not installed"}`);
|
||||
debugLog(` silk-wasm: ${silkWasm ? "available" : "unavailable"}`);
|
||||
if (warnings.length > 0) {
|
||||
debugLog(" --- Warnings ---");
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
* Cross-platform path and detection helpers for core/ modules.
|
||||
*
|
||||
* Provides home/data/media directory helpers, platform detection,
|
||||
* ffmpeg/silk-wasm availability checks — all without importing
|
||||
* `openclaw/plugin-sdk`. The temp-directory fallback is delegated
|
||||
* to the PlatformAdapter.
|
||||
* silk-wasm availability checks — all without importing `openclaw/plugin-sdk`.
|
||||
* The temp-directory fallback is delegated to the PlatformAdapter.
|
||||
*/
|
||||
|
||||
import { execFile } from "node:child_process";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { getPlatformAdapter } from "../adapter/index.js";
|
||||
import { formatErrorMessage } from "./format.js";
|
||||
import { debugLog, debugWarn } from "./log.js";
|
||||
import { debugWarn } from "./log.js";
|
||||
|
||||
/**
|
||||
* Resolve the current user's home directory safely across platforms.
|
||||
@@ -109,82 +107,6 @@ export function getTempDir(): string {
|
||||
return getPlatformAdapter().getTempDir();
|
||||
}
|
||||
|
||||
// ---- ffmpeg detection ----
|
||||
|
||||
let _ffmpegPath: string | null | undefined;
|
||||
let _ffmpegCheckPromise: Promise<string | null> | null = null;
|
||||
|
||||
/** Detect ffmpeg and return an executable path when available. */
|
||||
export function detectFfmpeg(): Promise<string | null> {
|
||||
if (_ffmpegPath !== undefined) {
|
||||
return Promise.resolve(_ffmpegPath);
|
||||
}
|
||||
if (_ffmpegCheckPromise) {
|
||||
return _ffmpegCheckPromise;
|
||||
}
|
||||
|
||||
_ffmpegCheckPromise = (async () => {
|
||||
const envPath = process.env.FFMPEG_PATH;
|
||||
if (envPath) {
|
||||
const ok = await testExecutable(envPath, ["-version"]);
|
||||
if (ok) {
|
||||
_ffmpegPath = envPath;
|
||||
debugLog(`[platform] ffmpeg found via FFMPEG_PATH: ${envPath}`);
|
||||
return _ffmpegPath;
|
||||
}
|
||||
debugWarn(`[platform] FFMPEG_PATH set but not working: ${envPath}`);
|
||||
}
|
||||
|
||||
const cmd = isWindows() ? "ffmpeg.exe" : "ffmpeg";
|
||||
const ok = await testExecutable(cmd, ["-version"]);
|
||||
if (ok) {
|
||||
_ffmpegPath = cmd;
|
||||
debugLog(`[platform] ffmpeg detected in PATH`);
|
||||
return _ffmpegPath;
|
||||
}
|
||||
|
||||
const commonPaths = isWindows()
|
||||
? [
|
||||
"C:\\ffmpeg\\bin\\ffmpeg.exe",
|
||||
path.join(process.env.LOCALAPPDATA || "", "Programs", "ffmpeg", "bin", "ffmpeg.exe"),
|
||||
path.join(process.env.ProgramFiles || "", "ffmpeg", "bin", "ffmpeg.exe"),
|
||||
]
|
||||
: [
|
||||
"/usr/local/bin/ffmpeg",
|
||||
"/opt/homebrew/bin/ffmpeg",
|
||||
"/usr/bin/ffmpeg",
|
||||
"/snap/bin/ffmpeg",
|
||||
];
|
||||
|
||||
for (const p of commonPaths) {
|
||||
if (p && fs.existsSync(p)) {
|
||||
const works = await testExecutable(p, ["-version"]);
|
||||
if (works) {
|
||||
_ffmpegPath = p;
|
||||
debugLog(`[platform] ffmpeg found at: ${p}`);
|
||||
return _ffmpegPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ffmpegPath = null;
|
||||
return null;
|
||||
})().finally(() => {
|
||||
_ffmpegCheckPromise = null;
|
||||
});
|
||||
|
||||
return _ffmpegCheckPromise;
|
||||
}
|
||||
|
||||
/** Return true when an executable responds successfully to the given args. */
|
||||
function testExecutable(cmd: string, args: string[]): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
execFile(cmd, args, { timeout: 5000 }, (err) => {
|
||||
resolve(!err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---- silk-wasm detection ----
|
||||
|
||||
let _silkWasmAvailable: boolean | null = null;
|
||||
|
||||
Reference in New Issue
Block a user