diff --git a/extensions/qqbot/src/engine/utils/audio.ts b/extensions/qqbot/src/engine/utils/audio.ts index 8687b760f93..ead30c35418 100644 --- a/extensions/qqbot/src/engine/utils/audio.ts +++ b/extensions/qqbot/src/engine/utils/audio.ts @@ -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 { - 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 { try { const { MPEGDecoder } = await import("mpg123-decoder"); @@ -502,7 +437,7 @@ async function wasmDecodeMp3ToPCM(buf: Buffer, targetRate: number): Promise { 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 { homeDir, tempDir, dataDir, - ffmpeg: ffmpegPath, silkWasm, warnings, }; @@ -95,7 +80,6 @@ export async function runDiagnostics(): Promise { 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 ---"); diff --git a/extensions/qqbot/src/engine/utils/platform.ts b/extensions/qqbot/src/engine/utils/platform.ts index 826680b0f62..a22e5f95fb6 100644 --- a/extensions/qqbot/src/engine/utils/platform.ts +++ b/extensions/qqbot/src/engine/utils/platform.ts @@ -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 | null = null; - -/** Detect ffmpeg and return an executable path when available. */ -export function detectFfmpeg(): Promise { - 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 { - return new Promise((resolve) => { - execFile(cmd, args, { timeout: 5000 }, (err) => { - resolve(!err); - }); - }); -} - // ---- silk-wasm detection ---- let _silkWasmAvailable: boolean | null = null;