From 17d5110069a78bd54a4c1c776bb66acbef502d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Tue, 13 Jan 2026 16:18:32 +0800 Subject: [PATCH] Add convertToNTSilkTct to FFmpeg adapters and update usage (#1517) Introduces the convertToNTSilkTct method to FFmpeg adapter interfaces and implementations, updating audio conversion logic to use this new method for Silk format conversion. Refactors FFmpegService to rename convertFile to convertAudioFmt and updates related usages. Removes 'audio-worker' entry from vite configs in napcat-framework and napcat-shell. Also fixes a typo in appid.json. Remove silk-wasm dependency and refactor audio handling Eliminated the silk-wasm package and related code, including audio-worker and direct Silk encoding/decoding logic. Audio format conversion and Silk detection are now handled via FFmpeg adapters. Updated related OneBot actions and configuration files to remove all references to silk-wasm and streamline audio processing. --- package.json | 1 - packages/napcat-common/package.json | 3 +- packages/napcat-common/src/audio-worker.ts | 20 -------- packages/napcat-core/external/appid.json | 5 ++ .../napcat-core/external/napi2native.json | 4 ++ packages/napcat-core/external/packet.json | 4 ++ packages/napcat-core/helper/audio.ts | 51 +++---------------- .../helper/ffmpeg/ffmpeg-adapter-interface.ts | 20 +++++--- .../helper/ffmpeg/ffmpeg-addon-adapter.ts | 23 ++++++++- .../napcat-core/helper/ffmpeg/ffmpeg-addon.ts | 2 + .../helper/ffmpeg/ffmpeg-exec-adapter.ts | 22 +++++++- packages/napcat-core/helper/ffmpeg/ffmpeg.ts | 23 ++++++++- packages/napcat-framework/vite.config.ts | 2 - .../napcat-onebot/action/file/GetRecord.ts | 20 +------- .../action/stream/DownloadFileRecordStream.ts | 22 +------- packages/napcat-onebot/api/msg.ts | 3 +- packages/napcat-shell/vite.config.ts | 2 - 17 files changed, 105 insertions(+), 122 deletions(-) delete mode 100644 packages/napcat-common/src/audio-worker.ts diff --git a/package.json b/package.json index cf91a7ad..f959a9fd 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ }, "dependencies": { "express": "^5.0.0", - "silk-wasm": "^3.6.1", "ws": "^8.18.3" } } \ No newline at end of file diff --git a/packages/napcat-common/package.json b/packages/napcat-common/package.json index a3f6018c..44b76115 100644 --- a/packages/napcat-common/package.json +++ b/packages/napcat-common/package.json @@ -17,8 +17,7 @@ }, "dependencies": { "ajv": "^8.13.0", - "file-type": "^21.0.0", - "silk-wasm": "^3.6.1" + "file-type": "^21.0.0" }, "devDependencies": { "@types/node": "^22.0.1" diff --git a/packages/napcat-common/src/audio-worker.ts b/packages/napcat-common/src/audio-worker.ts deleted file mode 100644 index 1a20e185..00000000 --- a/packages/napcat-common/src/audio-worker.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { encode } from 'silk-wasm'; -import { parentPort } from 'worker_threads'; - -export interface EncodeArgs { - input: ArrayBufferView | ArrayBuffer - sampleRate: number -} -export function recvTask (cb: (taskData: T) => Promise) { - parentPort?.on('message', async (taskData: T) => { - try { - const ret = await cb(taskData); - parentPort?.postMessage(ret); - } catch (error: unknown) { - parentPort?.postMessage({ error: (error as Error).message }); - } - }); -} -recvTask(async ({ input, sampleRate }) => { - return await encode(input, sampleRate); -}); diff --git a/packages/napcat-core/external/appid.json b/packages/napcat-core/external/appid.json index 2e069750..bf0e1d60 100644 --- a/packages/napcat-core/external/appid.json +++ b/packages/napcat-core/external/appid.json @@ -510,5 +510,10 @@ "3.2.23-44343": { "appid": 537336639, "qua": "V1_LNX_NQ_3.2.23_44343_GW_B" + }, + "9.9.26-44498": { + "appid": 537337416, + "offset": "0x1809C2810", + "qua": "V1_WIN_NQ_9.9.26_44498_GW_B" } } \ No newline at end of file diff --git a/packages/napcat-core/external/napi2native.json b/packages/napcat-core/external/napi2native.json index bd9bc895..9fe3a696 100644 --- a/packages/napcat-core/external/napi2native.json +++ b/packages/napcat-core/external/napi2native.json @@ -142,5 +142,9 @@ "3.2.23-44343-x64": { "send": "59A27B0", "recv": "2FFBE90" + }, + "9.9.26-44498-x64": { + "send": "0A1051C", + "recv": "1D3BC0D" } } \ No newline at end of file diff --git a/packages/napcat-core/external/packet.json b/packages/napcat-core/external/packet.json index ca9e21a5..d5c618f9 100644 --- a/packages/napcat-core/external/packet.json +++ b/packages/napcat-core/external/packet.json @@ -654,5 +654,9 @@ "3.2.23-44343-arm64": { "send": "6926F60", "recv": "692A910" + }, + "9.9.26-44498-x64": { + "send": "2CDAE40", + "recv": "2CDE3C0" } } \ No newline at end of file diff --git a/packages/napcat-core/helper/audio.ts b/packages/napcat-core/helper/audio.ts index 82439de9..a4736d0e 100644 --- a/packages/napcat-core/helper/audio.ts +++ b/packages/napcat-core/helper/audio.ts @@ -1,19 +1,8 @@ import fsPromise from 'fs/promises'; import path from 'node:path'; import { randomUUID } from 'crypto'; -import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm'; import { LogWrapper } from '@/napcat-core/helper/log'; -import { EncodeArgs } from 'napcat-common/src/audio-worker'; import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg'; -import { runTask } from 'napcat-common/src/worker'; -import { fileURLToPath } from 'node:url'; - -const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000]; - -function getWorkerPath () { - // return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href; - return path.join(path.dirname(fileURLToPath(import.meta.url)), 'audio-worker.mjs'); -} async function guessDuration (pttPath: string, logger: LogWrapper) { const pttFileInfo = await fsPromise.stat(pttPath); @@ -22,51 +11,23 @@ async function guessDuration (pttPath: string, logger: LogWrapper) { return duration; } -async function handleWavFile ( - file: Buffer, - filePath: string, - pcmPath: string -): Promise<{ input: Buffer; sampleRate: number }> { - const { fmt } = getWavFileInfo(file); - if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) { - const result = await FFmpegService.convert(filePath, pcmPath); - return { input: await fsPromise.readFile(pcmPath), sampleRate: result.sampleRate }; - } - return { input: file, sampleRate: fmt.sampleRate }; -} - export async function encodeSilk (filePath: string, TEMP_DIR: string, logger: LogWrapper) { try { - const file = await fsPromise.readFile(filePath); const pttPath = path.join(TEMP_DIR, randomUUID()); - if (!isSilk(file)) { + if (!(await FFmpegService.isSilk(filePath))) { logger.log(`语音文件${filePath}需要转换成silk`); - const pcmPath = `${pttPath}.pcm`; - // const { input, sampleRate } = isWav(file) ? await handleWavFile(file, filePath, pcmPath): { input: await FFmpegService.convert(filePath, pcmPath) ? await fsPromise.readFile(pcmPath) : Buffer.alloc(0), sampleRate: 24000 }; - let input: Buffer; - let sampleRate: number; - if (isWav(file)) { - const result = await handleWavFile(file, filePath, pcmPath); - input = result.input; - sampleRate = result.sampleRate; - } else { - const result = await FFmpegService.convert(filePath, pcmPath); - input = await fsPromise.readFile(pcmPath); - sampleRate = result.sampleRate; - } - const silk = await runTask(getWorkerPath(), { input, sampleRate }); - fsPromise.unlink(pcmPath).catch((e) => logger.logError('删除临时文件失败', pcmPath, e)); - await fsPromise.writeFile(pttPath, Buffer.from(silk.data)); - logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration); + await FFmpegService.convertToNTSilkTct(filePath, pttPath); + const duration = await FFmpegService.getDuration(filePath); + logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', duration); return { converted: true, path: pttPath, - duration: silk.duration / 1000, + duration: duration, }; } else { let duration = 0; try { - duration = getDuration(file) / 1000; + duration = await FFmpegService.getDuration(filePath); } catch (e: unknown) { logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, (e as Error).stack); duration = await guessDuration(filePath, logger); diff --git a/packages/napcat-core/helper/ffmpeg/ffmpeg-adapter-interface.ts b/packages/napcat-core/helper/ffmpeg/ffmpeg-adapter-interface.ts index 9b795bf7..79e6dfb9 100644 --- a/packages/napcat-core/helper/ffmpeg/ffmpeg-adapter-interface.ts +++ b/packages/napcat-core/helper/ffmpeg/ffmpeg-adapter-interface.ts @@ -27,21 +27,27 @@ export interface IFFmpegAdapter { readonly name: string; /** 是否可用 */ - isAvailable(): Promise; + isAvailable (): Promise; /** * 获取视频信息(包含缩略图) * @param videoPath 视频文件路径 * @returns 视频信息 */ - getVideoInfo(videoPath: string): Promise; + getVideoInfo (videoPath: string): Promise; /** * 获取音视频文件时长 * @param filePath 文件路径 * @returns 时长(秒) */ - getDuration(filePath: string): Promise; + getDuration (filePath: string): Promise; + + /** + * 判断是否为 Silk 格式 + * @param filePath 文件路径 + */ + isSilk (filePath: string): Promise; /** * 转换音频为 PCM 格式 @@ -49,7 +55,7 @@ export interface IFFmpegAdapter { * @param pcmPath 输出 PCM 文件路径 * @returns PCM 数据 Buffer */ - convertToPCM(filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number }>; + convertToPCM (filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number; }>; /** * 转换音频文件 @@ -57,12 +63,14 @@ export interface IFFmpegAdapter { * @param outputFile 输出文件路径 * @param format 目标格式 ('amr' | 'silk' 等) */ - convertFile(inputFile: string, outputFile: string, format: string): Promise; + convertFile (inputFile: string, outputFile: string, format: string): Promise; /** * 提取视频缩略图 * @param videoPath 视频文件路径 * @param thumbnailPath 缩略图输出路径 */ - extractThumbnail(videoPath: string, thumbnailPath: string): Promise; + extractThumbnail (videoPath: string, thumbnailPath: string): Promise; + + convertToNTSilkTct (inputFile: string, outputFile: string): Promise; } diff --git a/packages/napcat-core/helper/ffmpeg/ffmpeg-addon-adapter.ts b/packages/napcat-core/helper/ffmpeg/ffmpeg-addon-adapter.ts index 3f131511..dd4b24e4 100644 --- a/packages/napcat-core/helper/ffmpeg/ffmpeg-addon-adapter.ts +++ b/packages/napcat-core/helper/ffmpeg/ffmpeg-addon-adapter.ts @@ -5,7 +5,7 @@ import { platform, arch } from 'node:os'; import path from 'node:path'; -import { existsSync } from 'node:fs'; +import { existsSync, readFileSync, openSync, readSync, closeSync } from 'node:fs'; import { writeFile } from 'node:fs/promises'; import type { FFmpeg } from './ffmpeg-addon'; import type { IFFmpegAdapter, VideoInfoResult } from './ffmpeg-adapter-interface'; @@ -87,6 +87,22 @@ export class FFmpegAddonAdapter implements IFFmpegAdapter { return addon.getDuration(filePath); } + /** + * 判断是否为 Silk 格式 + */ + async isSilk (filePath: string): Promise { + try { + const fd = openSync(filePath, 'r'); + const buffer = Buffer.alloc(10); + readSync(fd, buffer, 0, 10, 0); + closeSync(fd); + const header = buffer.toString(); + return header.includes('#!SILK') || header.includes('\x02#!SILK'); + } catch { + return false; + } + } + /** * 转换为 PCM */ @@ -106,6 +122,11 @@ export class FFmpegAddonAdapter implements IFFmpegAdapter { await addon.decodeAudioToFmt(inputFile, outputFile, format); } + async convertToNTSilkTct (inputFile: string, outputFile: string): Promise { + const addon = this.ensureAddon(); + await addon.convertToNTSilkTct(inputFile, outputFile); + } + /** * 提取缩略图 */ diff --git a/packages/napcat-core/helper/ffmpeg/ffmpeg-addon.ts b/packages/napcat-core/helper/ffmpeg/ffmpeg-addon.ts index 3ad82bd3..dace1022 100644 --- a/packages/napcat-core/helper/ffmpeg/ffmpeg-addon.ts +++ b/packages/napcat-core/helper/ffmpeg/ffmpeg-addon.ts @@ -70,4 +70,6 @@ export interface FFmpeg { */ decodeAudioToPCM (filePath: string, pcmPath: string, sampleRate?: number): Promise<{ result: boolean, sampleRate: number; }>; decodeAudioToFmt (filePath: string, pcmPath: string, format: string): Promise<{ channels: number; sampleRate: number; format: string; }>; + + convertToNTSilkTct (inputFile: string, outputFile: string): Promise; } diff --git a/packages/napcat-core/helper/ffmpeg/ffmpeg-exec-adapter.ts b/packages/napcat-core/helper/ffmpeg/ffmpeg-exec-adapter.ts index a1f21915..80f7cbab 100644 --- a/packages/napcat-core/helper/ffmpeg/ffmpeg-exec-adapter.ts +++ b/packages/napcat-core/helper/ffmpeg/ffmpeg-exec-adapter.ts @@ -3,7 +3,7 @@ * 使用 execFile 调用 FFmpeg 命令行工具的适配器实现 */ -import { readFileSync, existsSync, mkdirSync } from 'fs'; +import { readFileSync, existsSync, mkdirSync, openSync, readSync, closeSync } from 'fs'; import { dirname, join } from 'path'; import { execFile } from 'child_process'; import { promisify } from 'util'; @@ -154,6 +154,22 @@ export class FFmpegExecAdapter implements IFFmpegAdapter { } } + /** + * 判断是否为 Silk 格式 + */ + async isSilk (filePath: string): Promise { + try { + const fd = openSync(filePath, 'r'); + const buffer = Buffer.alloc(10); + readSync(fd, buffer, 0, 10, 0); + closeSync(fd); + const header = buffer.toString(); + return header.includes('#!SILK') || header.includes('\x02#!SILK'); + } catch { + return false; + } + } + /** * 转换为 PCM */ @@ -241,4 +257,8 @@ export class FFmpegExecAdapter implements IFFmpegAdapter { throw new Error(`提取缩略图失败: ${(error as Error).message}`); } } + + async convertToNTSilkTct (inputFile: string, outputFile: string): Promise { + throw new Error('convertToNTSilkTct is not implemented in FFmpegExecAdapter'); + } } diff --git a/packages/napcat-core/helper/ffmpeg/ffmpeg.ts b/packages/napcat-core/helper/ffmpeg/ffmpeg.ts index f8cbee44..fdfe4365 100644 --- a/packages/napcat-core/helper/ffmpeg/ffmpeg.ts +++ b/packages/napcat-core/helper/ffmpeg/ffmpeg.ts @@ -64,7 +64,10 @@ export class FFmpegService { } return this.adapter; } - + public static async convertToNTSilkTct (inputFile: string, outputFile: string): Promise { + const adapter = await this.getAdapter(); + await adapter.convertToNTSilkTct(inputFile, outputFile); + } /** * 设置 FFmpeg 路径并更新适配器 * @deprecated 建议使用 init() 方法初始化 @@ -92,11 +95,27 @@ export class FFmpegService { /** * 转换音频文件 */ - public static async convertFile (inputFile: string, outputFile: string, format: string): Promise { + public static async convertAudioFmt (inputFile: string, outputFile: string, format: string): Promise { const adapter = await this.getAdapter(); await adapter.convertFile(inputFile, outputFile, format); } + /** + * 获取音频时长 + */ + public static async getDuration (filePath: string): Promise { + const adapter = await this.getAdapter(); + return adapter.getDuration(filePath); + } + + /** + * 判断是否为 Silk 格式 + */ + public static async isSilk (filePath: string): Promise { + const adapter = await this.getAdapter(); + return adapter.isSilk(filePath); + } + /** * 转换为 PCM 格式 */ diff --git a/packages/napcat-framework/vite.config.ts b/packages/napcat-framework/vite.config.ts index 9e4121a1..3e15c1e3 100644 --- a/packages/napcat-framework/vite.config.ts +++ b/packages/napcat-framework/vite.config.ts @@ -8,7 +8,6 @@ import react from '@vitejs/plugin-react-swc'; import napcatVersion from 'napcat-vite/vite-plugin-version.js'; // 依赖排除 const external = [ - 'silk-wasm', 'ws', 'express', ]; @@ -60,7 +59,6 @@ const FrameworkBaseConfig = () => lib: { entry: { napcat: path.resolve(__dirname, 'napcat.ts'), - 'audio-worker': path.resolve(__dirname, '../napcat-common/src/audio-worker.ts'), 'worker/conoutSocketWorker': path.resolve(__dirname, '../napcat-pty/worker/conoutSocketWorker.ts'), }, formats: ['es'], diff --git a/packages/napcat-onebot/action/file/GetRecord.ts b/packages/napcat-onebot/action/file/GetRecord.ts index 19d6eed1..b444f44f 100644 --- a/packages/napcat-onebot/action/file/GetRecord.ts +++ b/packages/napcat-onebot/action/file/GetRecord.ts @@ -1,7 +1,6 @@ import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile'; import { ActionName } from '@/napcat-onebot/action/router'; import { promises as fs } from 'fs'; -import { decode } from 'silk-wasm'; import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg'; const out_format = ['mp3', 'amr', 'wma', 'm4a', 'spx', 'ogg', 'wav', 'flac']; @@ -21,19 +20,13 @@ export default class GetRecord extends GetFileBase { if (!out_format.includes(payload.out_format)) { throw new Error('转换失败 out_format 字段可能格式不正确'); } - const pcmFile = `${inputFile}.pcm`; const outputFile = `${inputFile}.${payload.out_format}`; try { await fs.access(inputFile); try { await fs.access(outputFile); } catch { - if (FFmpegService.getAdapterName() === 'FFmpegAddon') { - await FFmpegService.convertFile(inputFile, outputFile, payload.out_format); - } else { - await this.decodeFile(inputFile, pcmFile); - await FFmpegService.convertFile(pcmFile, outputFile, payload.out_format); - } + await FFmpegService.convertAudioFmt(inputFile, outputFile, payload.out_format); } const base64Data = await fs.readFile(outputFile, { encoding: 'base64' }); res.file = outputFile; @@ -46,15 +39,4 @@ export default class GetRecord extends GetFileBase { } return res; } - - private async decodeFile (inputFile: string, outputFile: string): Promise { - try { - const inputData = await fs.readFile(inputFile); - const decodedData = await decode(inputData, 24000); - await fs.writeFile(outputFile, Buffer.from(decodedData.data)); - } catch (error) { - console.error('Error decoding file:', error); - throw error; // 重新抛出错误以便调用者可以处理 - } - } } diff --git a/packages/napcat-onebot/action/stream/DownloadFileRecordStream.ts b/packages/napcat-onebot/action/stream/DownloadFileRecordStream.ts index 8f16d09d..e5482eab 100644 --- a/packages/napcat-onebot/action/stream/DownloadFileRecordStream.ts +++ b/packages/napcat-onebot/action/stream/DownloadFileRecordStream.ts @@ -4,7 +4,6 @@ import { Static, Type } from '@sinclair/typebox'; import { NetworkAdapterConfig } from '@/napcat-onebot/config/config'; import { StreamPacket, StreamStatus } from './StreamBasic'; import fs from 'fs'; -import { decode } from 'silk-wasm'; import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg'; import { BaseDownloadStream, DownloadResult } from './BaseDownloadStream'; @@ -38,7 +37,6 @@ export class DownloadFileRecordStream extends BaseDownloadStream { - try { - const inputData = await fs.promises.readFile(inputFile); - const decodedData = await decode(inputData, 24000); - await fs.promises.writeFile(outputFile, Buffer.from(decodedData.data)); - } catch (error) { - console.error('Error decoding file:', error); - throw error; - } - } } diff --git a/packages/napcat-onebot/api/msg.ts b/packages/napcat-onebot/api/msg.ts index 417f8d38..94f948aa 100644 --- a/packages/napcat-onebot/api/msg.ts +++ b/packages/napcat-onebot/api/msg.ts @@ -1075,7 +1075,8 @@ export class OneBotMsgApi { resMsg.sub_type = 'group'; const ret = await this.core.apis.MsgApi.getTempChatInfo(ChatType.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid); if (ret.result === 0) { - const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUin, msg.senderUin); + // 避免uin:'' uid非空,uid一般不空 + const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUin, await this.core.apis.UserApi.getUinByUidV2(msg.senderUid)); resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode); resMsg.sender.nickname = member?.nick ?? member?.cardName ?? '临时会话'; resMsg.temp_source = 0; diff --git a/packages/napcat-shell/vite.config.ts b/packages/napcat-shell/vite.config.ts index f07fa251..e97bc597 100644 --- a/packages/napcat-shell/vite.config.ts +++ b/packages/napcat-shell/vite.config.ts @@ -9,7 +9,6 @@ import react from '@vitejs/plugin-react-swc'; // 依赖排除 const external = [ - 'silk-wasm', 'ws', 'express', ]; @@ -56,7 +55,6 @@ const ShellBaseConfig = (source_map: boolean = false) => lib: { entry: { napcat: path.resolve(__dirname, 'napcat.ts'), - 'audio-worker': path.resolve(__dirname, '../napcat-common/src/audio-worker.ts'), 'worker/conoutSocketWorker': path.resolve(__dirname, '../napcat-pty/worker/conoutSocketWorker.ts'), }, formats: ['es'],