Merge pull request #33 from Lazy-CZ/fix/export-audio-fix

fix: 修复批量导出 wav 音频语速变快、音调升高(采样率不匹配)问题
This commit is contained in:
qingmao
2026-09-04 16:14:35 +08:00
committed by GitHub
3 changed files with 47 additions and 11 deletions
+18 -9
View File
@@ -941,10 +941,7 @@ async function runSingleExport(
request.format === 'html'
? options.outputFolderName || safeFilePart(request.outputName)
: `${safeFilePart(request.outputName)}_${exportStamp()}`
const root =
options.outputRoot ||
request.outputDirectory ||
(await resolveDefaultExportRoot(outputFolder))
const root = options.outputRoot || request.outputDirectory || (await resolveDefaultExportRoot(outputFolder))
await fs.mkdir(root, { recursive: true })
const outputDir = join(root, outputFolder)
const outputPath =
@@ -1236,7 +1233,16 @@ async function runSingleExport(
markResourceExists(voiceUrl)
}
message.voiceDataUrl = voiceUrl
message.voiceDuration = Math.max(1, Math.round(audioBuffer.length / (24000 * 2)))
// 读取 WAV 头中的采样率,避免按错误采样率计算时长(Silk 解码为 16kHz)
const wavSampleRate =
audioBuffer.length >= 44 ? audioBuffer.readUInt32LE(24) : 16000
const wavChannels =
audioBuffer.length >= 44 ? audioBuffer.readUInt16LE(22) : 1
const pcmBytes = Math.max(0, audioBuffer.length - 44)
message.voiceDuration = Math.max(
1,
Math.round(pcmBytes / (wavSampleRate * wavChannels * 2))
)
} catch (error) {
keepMediaError(
request,
@@ -1478,10 +1484,13 @@ async function runSingleExport(
} else if (message.contentData.type === 'sticker' && stickerService) {
const stickerSource = message.contentData.url || message.contentData.thumbUrl
const result = await stickerService.resolveSticker(stickerSource, message.contentData.md5)
const decoded = result.data ? decodeDataUrl(result.data) : null
const stickerExtension = decoded ? detectAssetExtension(decoded.buffer) : null
if (decoded && stickerExtension) {
const name = `sticker_${bufferHashPart(decoded.buffer)}.${stickerExtension}`
const decoded = result.data
? decodeDataUrl(result.data)
: stickerSource
? await readAvatarAsset(stickerSource)
: null
if (decoded) {
const name = `sticker_${bufferHashPart(decoded.buffer)}.${decoded.extension}`
const mediaUrl = `media/${name}`
if (!(await resourceExists(mediaUrl))) {
await fs.writeFile(join(outputDir, 'media', name), decoded.buffer)
+6 -2
View File
@@ -125,7 +125,11 @@ export class VoiceService {
codec: 'silk',
sourceHash: createHash('sha256').update(silkData).digest('hex')
})
const wavData = this.createWavBuffer(decoded.pcm, 24000)
const wavData = this.createWavBuffer(
decoded.pcm,
decoded.sampleRate,
decoded.channels
)
const data = wavData.toString('base64')
const cacheKey = this.buildCacheKey(
reference.sessionId,
@@ -239,7 +243,7 @@ export class VoiceService {
private createWavBuffer(
pcmData: Buffer,
sampleRate: number = 24000,
sampleRate: number = 16000,
channels: number = 1
): Buffer {
const pcmLength = pcmData.length
+23
View File
@@ -2,6 +2,29 @@ import { describe, expect, it, vi } from 'vitest'
import { VoiceService } from '../../src/main/voice-service'
describe('VoiceService batch lookup', () => {
it('writes the decoded sample rate and duration into batch WAV output', async () => {
const getVoiceDataBatch = vi.fn().mockResolvedValue([{ success: true, hex: '0102' }])
const service = new VoiceService({ getVoiceDataBatch } as never)
const decoder = (service as unknown as {
decoderRegistry: { decode: ReturnType<typeof vi.fn> }
}).decoderRegistry
vi.spyOn(decoder, 'decode').mockResolvedValue({
pcm: Buffer.alloc(16_000 * 2),
sampleRate: 16_000,
channels: 1
})
const [result] = await service.resolveVoices([
{ sessionId: 'session', localId: 10, createTime: 100, svrId: '1000' }
])
expect(result.success).toBe(true)
const wav = Buffer.from(result.data!, 'base64')
expect(wav.readUInt32LE(24)).toBe(16_000)
expect(wav.readUInt16LE(22)).toBe(1)
expect((wav.length - 44) / (wav.readUInt32LE(24) * wav.readUInt16LE(22) * 2)).toBe(1)
})
it('retries failed batch entries with the compatible single-item lookup', async () => {
const getVoiceDataBatch = vi.fn().mockResolvedValue([
{ success: false, error: '获取语音数据失败' },