mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-11 23:40:24 +00:00
Compare commits
1 Commits
feature/re
...
remove-sil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e1e3a6df7 |
@@ -28,7 +28,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^5.0.0",
|
||||
"silk-wasm": "^3.6.1",
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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<T> (cb: (taskData: T) => Promise<unknown>) {
|
||||
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<EncodeArgs>(async ({ input, sampleRate }) => {
|
||||
return await encode(input, sampleRate);
|
||||
});
|
||||
5
packages/napcat-core/external/appid.json
vendored
5
packages/napcat-core/external/appid.json
vendored
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -142,5 +142,9 @@
|
||||
"3.2.23-44343-x64": {
|
||||
"send": "59A27B0",
|
||||
"recv": "2FFBE90"
|
||||
},
|
||||
"9.9.26-44498-x64": {
|
||||
"send": "0A1051C",
|
||||
"recv": "1D3BC0D"
|
||||
}
|
||||
}
|
||||
4
packages/napcat-core/external/packet.json
vendored
4
packages/napcat-core/external/packet.json
vendored
@@ -654,5 +654,9 @@
|
||||
"3.2.23-44343-arm64": {
|
||||
"send": "6926F60",
|
||||
"recv": "692A910"
|
||||
},
|
||||
"9.9.26-44498-x64": {
|
||||
"send": "2CDAE40",
|
||||
"recv": "2CDE3C0"
|
||||
}
|
||||
}
|
||||
@@ -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<EncodeArgs, EncodeResult>(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);
|
||||
|
||||
@@ -27,21 +27,27 @@ export interface IFFmpegAdapter {
|
||||
readonly name: string;
|
||||
|
||||
/** 是否可用 */
|
||||
isAvailable(): Promise<boolean>;
|
||||
isAvailable (): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 获取视频信息(包含缩略图)
|
||||
* @param videoPath 视频文件路径
|
||||
* @returns 视频信息
|
||||
*/
|
||||
getVideoInfo(videoPath: string): Promise<VideoInfoResult>;
|
||||
getVideoInfo (videoPath: string): Promise<VideoInfoResult>;
|
||||
|
||||
/**
|
||||
* 获取音视频文件时长
|
||||
* @param filePath 文件路径
|
||||
* @returns 时长(秒)
|
||||
*/
|
||||
getDuration(filePath: string): Promise<number>;
|
||||
getDuration (filePath: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* 判断是否为 Silk 格式
|
||||
* @param filePath 文件路径
|
||||
*/
|
||||
isSilk (filePath: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 转换音频为 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<void>;
|
||||
convertFile (inputFile: string, outputFile: string, format: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 提取视频缩略图
|
||||
* @param videoPath 视频文件路径
|
||||
* @param thumbnailPath 缩略图输出路径
|
||||
*/
|
||||
extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void>;
|
||||
extractThumbnail (videoPath: string, thumbnailPath: string): Promise<void>;
|
||||
|
||||
convertToNTSilkTct (inputFile: string, outputFile: string): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<void> {
|
||||
const addon = this.ensureAddon();
|
||||
await addon.convertToNTSilkTct(inputFile, outputFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取缩略图
|
||||
*/
|
||||
|
||||
@@ -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<void>;
|
||||
}
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<void> {
|
||||
throw new Error('convertToNTSilkTct is not implemented in FFmpegExecAdapter');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,10 @@ export class FFmpegService {
|
||||
}
|
||||
return this.adapter;
|
||||
}
|
||||
|
||||
public static async convertToNTSilkTct (inputFile: string, outputFile: string): Promise<void> {
|
||||
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<void> {
|
||||
public static async convertAudioFmt (inputFile: string, outputFile: string, format: string): Promise<void> {
|
||||
const adapter = await this.getAdapter();
|
||||
await adapter.convertFile(inputFile, outputFile, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频时长
|
||||
*/
|
||||
public static async getDuration (filePath: string): Promise<number> {
|
||||
const adapter = await this.getAdapter();
|
||||
return adapter.getDuration(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 Silk 格式
|
||||
*/
|
||||
public static async isSilk (filePath: string): Promise<boolean> {
|
||||
const adapter = await this.getAdapter();
|
||||
return adapter.isSilk(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 PCM 格式
|
||||
*/
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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<void> {
|
||||
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; // 重新抛出错误以便调用者可以处理
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Payload, Downlo
|
||||
throw new Error('转换失败 out_format 字段可能格式不正确');
|
||||
}
|
||||
|
||||
const pcmFile = `${downloadPath}.pcm`;
|
||||
const outputFile = `${downloadPath}.${payload.out_format}`;
|
||||
|
||||
try {
|
||||
@@ -46,13 +44,8 @@ export class DownloadFileRecordStream extends BaseDownloadStream<Payload, Downlo
|
||||
await fs.promises.access(outputFile);
|
||||
streamPath = outputFile;
|
||||
} catch {
|
||||
// 尝试解码 silk 到 pcm 再用 ffmpeg 转换
|
||||
if (FFmpegService.getAdapterName() === 'FFmpegAddon') {
|
||||
await FFmpegService.convertFile(downloadPath, outputFile, payload.out_format);
|
||||
} else {
|
||||
await this.decodeFile(downloadPath, pcmFile);
|
||||
await FFmpegService.convertFile(pcmFile, outputFile, payload.out_format);
|
||||
}
|
||||
// 尝试解码 amr 到 out format直接 ffmpeg 转换
|
||||
await FFmpegService.convertAudioFmt(downloadPath, outputFile, payload.out_format);
|
||||
streamPath = outputFile;
|
||||
}
|
||||
}
|
||||
@@ -82,15 +75,4 @@ export class DownloadFileRecordStream extends BaseDownloadStream<Payload, Downlo
|
||||
throw new Error(`Download failed: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async decodeFile (inputFile: string, outputFile: string): Promise<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Reference in New Issue
Block a user