mirror of
https://github.com/LLOneBot/LLOneBot.git
synced 2024-11-22 01:56:33 +00:00
feat: convert wav by ffmpeg
This commit is contained in:
parent
d9d7e9e830
commit
730294236c
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
node-version: 18
|
node-version: 18
|
||||||
|
|
||||||
- name: install dependenies
|
- name: install dependenies
|
||||||
run: npm install
|
run: export ELECTRON_SKIP_BINARY_DOWNLOAD=1 && npm install
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
776
package-lock.json
generated
776
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,6 @@
|
|||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"postinstall": "cross-env ELECTRON_SKIP_BINARY_DOWNLOAD=1 && npm install electron --no-save",
|
|
||||||
"build": "npm run build-main && npm run build-preload && npm run build-renderer",
|
"build": "npm run build-main && npm run build-preload && npm run build-renderer",
|
||||||
"build-main": "webpack --config webpack.main.config.js",
|
"build-main": "webpack --config webpack.main.config.js",
|
||||||
"build-preload": "webpack --config webpack.preload.config.js",
|
"build-preload": "webpack --config webpack.preload.config.js",
|
||||||
@ -19,6 +18,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"json-bigint": "^1.0.0",
|
"json-bigint": "^1.0.0",
|
||||||
"music-metadata": "^8.1.4",
|
"music-metadata": "^8.1.4",
|
||||||
"silk-wasm": "^3.2.3",
|
"silk-wasm": "^3.2.3",
|
||||||
@ -28,12 +28,14 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-env": "^7.23.2",
|
"@babel/preset-env": "^7.23.2",
|
||||||
"@types/express": "^4.17.20",
|
"@types/express": "^4.17.20",
|
||||||
|
"@types/fluent-ffmpeg": "^2.1.24",
|
||||||
"@types/node": "^20.11.19",
|
"@types/node": "^20.11.19",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"@types/ws": "^8.5.10",
|
"@types/ws": "^8.5.10",
|
||||||
"babel-loader": "^9.1.3",
|
"babel-loader": "^9.1.3",
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
"electron": "^29.0.1",
|
||||||
"ts-loader": "^9.5.0",
|
"ts-loader": "^9.5.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"webpack": "^5.89.0",
|
"webpack": "^5.89.0",
|
||||||
|
@ -5,6 +5,8 @@ import util from "util";
|
|||||||
import {encode, getDuration} from "silk-wasm";
|
import {encode, getDuration} from "silk-wasm";
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import {v4 as uuidv4} from "uuid";
|
import {v4 as uuidv4} from "uuid";
|
||||||
|
import {exec} from "node:child_process";
|
||||||
|
import ffmpeg from "fluent-ffmpeg"
|
||||||
|
|
||||||
export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
|
export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
|
||||||
|
|
||||||
@ -136,7 +138,22 @@ export function mergeNewProperties(newObj: any, oldObj: any) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkFFMPEG(): boolean {
|
||||||
|
let ffmpegExist: boolean;
|
||||||
|
exec('ffmpeg -version', (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
log('ffmpeg is not installed or not found in PATH:', error);
|
||||||
|
ffmpegExist = false
|
||||||
|
}
|
||||||
|
log('ffmpeg is installed. Version info:', stdout);
|
||||||
|
ffmpegExist = true;
|
||||||
|
});
|
||||||
|
return ffmpegExist;
|
||||||
|
}
|
||||||
|
|
||||||
export async function encodeSilk(filePath: string) {
|
export async function encodeSilk(filePath: string) {
|
||||||
|
const fsp = require("fs").promises
|
||||||
|
|
||||||
function getFileHeader(filePath: string) {
|
function getFileHeader(filePath: string) {
|
||||||
// 定义要读取的字节数
|
// 定义要读取的字节数
|
||||||
const bytesToRead = 7;
|
const bytesToRead = 7;
|
||||||
@ -154,6 +171,35 @@ export async function encodeSilk(filePath: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWavFile(filePath: string) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.open(filePath, 'r', (err, fd) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 读取前12个字节
|
||||||
|
const buffer = Buffer.alloc(12);
|
||||||
|
fs.read(fd, buffer, 0, 12, 0, (err, bytesRead, buffer) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fs.close(fd, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 检查RIFF头和WAVE格式标识
|
||||||
|
const isRIFF = buffer.toString('utf8', 0, 4) === 'RIFF';
|
||||||
|
const isWAVE = buffer.toString('utf8', 8, 12) === 'WAVE';
|
||||||
|
resolve(isRIFF && isWAVE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function getAudioSampleRate(filePath: string) {
|
async function getAudioSampleRate(filePath: string) {
|
||||||
try {
|
try {
|
||||||
const mm = await import('music-metadata');
|
const mm = await import('music-metadata');
|
||||||
@ -172,10 +218,25 @@ export async function encodeSilk(filePath: string) {
|
|||||||
const pttPath = path.join(CONFIG_DIR, uuidv4());
|
const pttPath = path.join(CONFIG_DIR, uuidv4());
|
||||||
if (getFileHeader(filePath) !== "02232153494c4b") {
|
if (getFileHeader(filePath) !== "02232153494c4b") {
|
||||||
log(`语音文件${filePath}需要转换`)
|
log(`语音文件${filePath}需要转换`)
|
||||||
|
const isWav = await isWavFile(filePath);
|
||||||
|
if (!isWav) {
|
||||||
|
log(`语音文件${filePath}正在转换成wav`)
|
||||||
|
let voiceData = await fsp.readFile(filePath)
|
||||||
|
const wavPath = pttPath + ".wav"
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
ffmpeg(filePath).toFormat("wav").on('end', function () {
|
||||||
|
// console.log('转换完成');
|
||||||
|
filePath = wavPath
|
||||||
|
})
|
||||||
|
.on('error', function (err) {
|
||||||
|
// console.log('转换出错: ' + err.message);
|
||||||
|
})
|
||||||
|
.save(wavPath);
|
||||||
|
})
|
||||||
const sampleRate = await getAudioSampleRate(filePath) || 44100;
|
const sampleRate = await getAudioSampleRate(filePath) || 44100;
|
||||||
const silk = await encode(pcm, sampleRate);
|
const silk = await encode(pcm, sampleRate);
|
||||||
fs.writeFileSync(pttPath, silk.data);
|
fs.writeFileSync(pttPath, silk.data);
|
||||||
log(`语音文件${filePath}转换成功!`)
|
log(`语音文件${filePath}转换成功!`, pttPath)
|
||||||
return {
|
return {
|
||||||
converted: true,
|
converted: true,
|
||||||
path: pttPath,
|
path: pttPath,
|
||||||
@ -189,6 +250,7 @@ export async function encodeSilk(filePath: string) {
|
|||||||
duration: duration,
|
duration: duration,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log("convert silk failed", error.stack);
|
log("convert silk failed", error.stack);
|
||||||
return {};
|
return {};
|
||||||
|
@ -8,8 +8,7 @@ import {
|
|||||||
SendTextElement
|
SendTextElement
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {NTQQApi} from "./ntcall";
|
import {NTQQApi} from "./ntcall";
|
||||||
import {encodeSilk, log} from "../common/utils";
|
import {encodeSilk} from "../common/utils";
|
||||||
import fs from "fs";
|
|
||||||
|
|
||||||
|
|
||||||
export class SendMsgElementConstructor {
|
export class SendMsgElementConstructor {
|
||||||
@ -83,9 +82,9 @@ export class SendMsgElementConstructor {
|
|||||||
static async ptt(pttPath: string): Promise<SendPttElement> {
|
static async ptt(pttPath: string): Promise<SendPttElement> {
|
||||||
const {converted, path: silkPath, duration} = await encodeSilk(pttPath);
|
const {converted, path: silkPath, duration} = await encodeSilk(pttPath);
|
||||||
// log("生成语音", silkPath, duration);
|
// log("生成语音", silkPath, duration);
|
||||||
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(silkPath);
|
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(silkPath, ElementType.PTT);
|
||||||
if (converted){
|
if (converted){
|
||||||
fs.unlink(silkPath, ()=>{});
|
// fs.unlink(silkPath, ()=>{});
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
elementType: ElementType.PTT,
|
elementType: ElementType.PTT,
|
||||||
|
@ -3,7 +3,9 @@ import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} fr
|
|||||||
import {log} from "../common/utils";
|
import {log} from "../common/utils";
|
||||||
import {
|
import {
|
||||||
ChatType,
|
ChatType,
|
||||||
Friend, FriendRequest,
|
ElementType,
|
||||||
|
Friend,
|
||||||
|
FriendRequest,
|
||||||
Group,
|
Group,
|
||||||
GroupMember,
|
GroupMember,
|
||||||
GroupNotifies,
|
GroupNotifies,
|
||||||
@ -329,7 +331,7 @@ export class NTQQApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 上传文件到QQ的文件夹
|
// 上传文件到QQ的文件夹
|
||||||
static async uploadFile(filePath: string) {
|
static async uploadFile(filePath: string, elementType: ElementType=ElementType.PIC) {
|
||||||
const md5 = await NTQQApi.getFileMd5(filePath);
|
const md5 = await NTQQApi.getFileMd5(filePath);
|
||||||
let ext = (await NTQQApi.getFileType(filePath))?.ext
|
let ext = (await NTQQApi.getFileType(filePath))?.ext
|
||||||
if (ext) {
|
if (ext) {
|
||||||
@ -344,7 +346,7 @@ export class NTQQApi {
|
|||||||
path_info: {
|
path_info: {
|
||||||
md5HexStr: md5,
|
md5HexStr: md5,
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
elementType: 2,
|
elementType: elementType,
|
||||||
elementSubType: 0,
|
elementSubType: 0,
|
||||||
thumbSize: 0,
|
thumbSize: 0,
|
||||||
needCreate: true,
|
needCreate: true,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user