Merge pull request #262 from LLOneBot/dev

get_record 支持 out_format 进行转码,和其他小修复
This commit is contained in:
linyuchen 2024-06-21 17:39:53 +08:00 committed by GitHub
commit 1a015ac8d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 69 additions and 19 deletions

View File

@ -1,10 +1,10 @@
{ {
"manifest_version": 4, "manifest_version": 4,
"type": "extension", "type": "extension",
"name": "LLOneBot v3.26.6", "name": "LLOneBot v3.26.7",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发, 不支持商店在线更新", "description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发, 不支持商店在线更新",
"version": "3.26.6", "version": "3.26.7",
"icon": "./icon.jpg", "icon": "./icon.jpg",
"authors": [ "authors": [
{ {

View File

@ -23,7 +23,7 @@
"file-type": "^19.0.0", "file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"level": "^8.0.1", "level": "^8.0.1",
"silk-wasm": "^3.3.4", "silk-wasm": "^3.6.0",
"utf-8-validate": "^6.0.3", "utf-8-validate": "^6.0.3",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"ws": "^8.16.0" "ws": "^8.16.0"

View File

@ -1,9 +1,9 @@
import fs from 'fs' import fs from 'fs'
import { encode, getDuration, getWavFileInfo, isWav } from 'silk-wasm'
import fsPromise from 'fs/promises' import fsPromise from 'fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk } from 'silk-wasm'
import { log } from './log' import { log } from './log'
import path from 'node:path' import path from 'node:path'
import { DATA_DIR, TEMP_DIR } from './index' import { TEMP_DIR } from './index'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { getConfigUtil } from '../config' import { getConfigUtil } from '../config'
import { spawn } from 'node:child_process' import { spawn } from 'node:child_process'
@ -60,10 +60,11 @@ export async function encodeSilk(filePath: string) {
// } // }
try { try {
const file = await fsPromise.readFile(filePath)
const pttPath = path.join(TEMP_DIR, uuidv4()) const pttPath = path.join(TEMP_DIR, uuidv4())
if (getFileHeader(filePath) !== '02232153494c4b') { if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`) log(`语音文件${filePath}需要转换成silk`)
const _isWav = await isWavFile(filePath) const _isWav = isWav(file)
const pcmPath = pttPath + '.pcm' const pcmPath = pttPath + '.pcm'
let sampleRate = 0 let sampleRate = 0
const convert = () => { const convert = () => {
@ -79,7 +80,8 @@ export async function encodeSilk(filePath: string) {
if (code == null || EXIT_CODES.includes(code)) { if (code == null || EXIT_CODES.includes(code)) {
sampleRate = 24000 sampleRate = 24000
const data = fs.readFileSync(pcmPath) const data = fs.readFileSync(pcmPath)
fs.unlink(pcmPath, (err) => {}) fs.unlink(pcmPath, (err) => {
})
return resolve(data) return resolve(data)
} }
log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`) log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`)
@ -91,7 +93,7 @@ export async function encodeSilk(filePath: string) {
if (!_isWav) { if (!_isWav) {
input = await convert() input = await convert()
} else { } else {
input = fs.readFileSync(filePath) input = file
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000] const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
const { fmt } = getWavFileInfo(input) const { fmt } = getWavFileInfo(input)
// log(`wav文件信息`, fmt) // log(`wav文件信息`, fmt)
@ -108,7 +110,7 @@ export async function encodeSilk(filePath: string) {
duration: silk.duration / 1000, duration: silk.duration / 1000,
} }
} else { } else {
const silk = fs.readFileSync(filePath) const silk = file
let duration = 0 let duration = 0
try { try {
duration = getDuration(silk) / 1000 duration = getDuration(silk) / 1000
@ -128,3 +130,41 @@ export async function encodeSilk(filePath: string) {
return {} return {}
} }
} }
export async function decodeSilk(inputFilePath: string, outFormat: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' = 'mp3') {
const silkArrayBuffer = await fsPromise.readFile(inputFilePath)
const data = (await decode(silkArrayBuffer, 24000)).data
const fileName = path.join(TEMP_DIR, path.basename(inputFilePath))
const outPCMPath = fileName + '.pcm'
const outFilePath = fileName + '.' + outFormat
await fsPromise.writeFile(outPCMPath, data)
const convert = () => {
return new Promise<string>((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg || process.env.FFMPEG_PATH || 'ffmpeg'
const cp = spawn(ffmpegPath, [
'-y',
'-f', 's16le', // PCM format
'-ar', '24000', // Sample rate
'-ac', '1', // Number of audio channels
'-i', outPCMPath,
outFilePath,
])
cp.on('error', (err) => {
log(`FFmpeg处理转换出错: `, err.message)
return reject(err)
})
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255]
if (code == null || EXIT_CODES.includes(code)) {
fs.unlink(outPCMPath, (err) => {
})
return resolve(outFilePath)
}
const exitErr = `FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`
log(exitErr)
reject(Error(`FFmpeg处理转换失败,${exitErr}`))
})
})
}
return convert()
}

View File

@ -2,7 +2,7 @@ import BaseAction from '../BaseAction'
import fs from 'fs/promises' import fs from 'fs/promises'
import { dbUtil } from '@/common/db' import { dbUtil } from '@/common/db'
import { getConfigUtil } from '@/common/config' import { getConfigUtil } from '@/common/config'
import { log, sleep, uri2local } from '@/common/utils' import { checkFileReceived, log, sleep, uri2local } from '@/common/utils'
import { NTQQFileApi } from '@/ntqqapi/api' import { NTQQFileApi } from '@/ntqqapi/api'
import { ActionName } from '../types' import { ActionName } from '../types'
import { FileElement, RawMessage, VideoElement } from '@/ntqqapi/types' import { FileElement, RawMessage, VideoElement } from '@/ntqqapi/types'
@ -38,20 +38,21 @@ export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
log('找到了文件 element', element) log('找到了文件 element', element)
// 构建下载函数 // 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, cache.elementId, '', '', true) await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, cache.elementId, '', '', true)
await sleep(1000) // 这里延时是为何? // 等待文件下载完成
msg = await dbUtil.getMsgByLongId(cache.msgId) msg = await dbUtil.getMsgByLongId(cache.msgId)
log('下载完成后的msg', msg) log('下载完成后的msg', msg)
cache.filePath = this.getElement(msg, cache.elementId).filePath cache.filePath = this.getElement(msg, cache.elementId).filePath
await checkFileReceived(cache.filePath, 10 * 1000)
dbUtil.addFileCache(file, cache).then() dbUtil.addFileCache(file, cache).then()
} }
} }
} }
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> { protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const cache = await dbUtil.getFileCache(payload.file) let cache = await dbUtil.getFileCache(payload.file)
const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig()
if (!cache) { if (!cache) {
throw new Error('file not found') throw new Error('file not found')
} }
const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig()
if (cache.downloadFunc) { if (cache.downloadFunc) {
await cache.downloadFunc() await cache.downloadFunc()
} }

View File

@ -1,5 +1,9 @@
import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile' import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile'
import { ActionName } from '../types' import { ActionName } from '../types'
import {decodeSilk} from "@/common/utils/audio";
import { getConfigUtil } from '@/common/config'
import path from 'node:path'
import fs from 'node:fs'
interface Payload extends GetFilePayload { interface Payload extends GetFilePayload {
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
@ -9,7 +13,13 @@ export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord actionName = ActionName.GetRecord
protected async _handle(payload: Payload): Promise<GetFileResponse> { protected async _handle(payload: Payload): Promise<GetFileResponse> {
let res = super._handle(payload) let res = await super._handle(payload)
res.file = await decodeSilk(res.file, payload.out_format)
res.file_name = path.basename(res.file)
res.file_size = fs.statSync(res.file).size.toString()
if (getConfigUtil().getConfig().enableLocalFile2Url){
res.base64 = fs.readFileSync(res.file, 'base64')
}
return res return res
} }
} }

View File

@ -188,9 +188,7 @@ export class OB11Constructor {
element.picElement.sourcePath, element.picElement.sourcePath,
) )
}, },
}) }).then()
.then()
// 不在自动下载图片
} }
else if (element.videoElement || element.fileElement) { else if (element.videoElement || element.fileElement) {
const videoOrFileElement = element.videoElement || element.fileElement const videoOrFileElement = element.videoElement || element.fileElement

View File

@ -27,6 +27,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
} }
try { try {
let handleResult = await action.websocketHandle(params, echo) let handleResult = await action.websocketHandle(params, echo)
handleResult.echo = echo
wsReply(wsClient, handleResult) wsReply(wsClient, handleResult)
} catch (e) { } catch (e) {
wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo)) wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo))

View File

@ -1 +1 @@
export const version = '3.26.6' export const version = '3.26.7'