mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-06 13:54:10 +08:00
fix: 修复导出的音频文件变调、变速的问题
This commit is contained in:
+19
-39
@@ -584,19 +584,6 @@ const detectAssetExtension = (buffer: Buffer): string | null => {
|
|||||||
return 'webp'
|
return 'webp'
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const htmlArchiveStickerResourceIsValid = async (
|
|
||||||
outputDir: string,
|
|
||||||
value?: string
|
|
||||||
): Promise<boolean> => {
|
|
||||||
const filePath = htmlArchiveResourcePath(outputDir, value)
|
|
||||||
if (!filePath) return false
|
|
||||||
try {
|
|
||||||
return detectAssetExtension(await fs.readFile(filePath)) !== null
|
|
||||||
} catch (error) {
|
|
||||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function readAvatarAsset(
|
async function readAvatarAsset(
|
||||||
source: string
|
source: string
|
||||||
): Promise<{ extension: string; buffer: Buffer } | null> {
|
): Promise<{ extension: string; buffer: Buffer } | null> {
|
||||||
@@ -941,10 +928,7 @@ async function runSingleExport(
|
|||||||
request.format === 'html'
|
request.format === 'html'
|
||||||
? options.outputFolderName || safeFilePart(request.outputName)
|
? options.outputFolderName || safeFilePart(request.outputName)
|
||||||
: `${safeFilePart(request.outputName)}_${exportStamp()}`
|
: `${safeFilePart(request.outputName)}_${exportStamp()}`
|
||||||
const root =
|
const root = options.outputRoot || request.outputDirectory || (await resolveDefaultExportRoot(outputFolder))
|
||||||
options.outputRoot ||
|
|
||||||
request.outputDirectory ||
|
|
||||||
(await resolveDefaultExportRoot(outputFolder))
|
|
||||||
await fs.mkdir(root, { recursive: true })
|
await fs.mkdir(root, { recursive: true })
|
||||||
const outputDir = join(root, outputFolder)
|
const outputDir = join(root, outputFolder)
|
||||||
const outputPath =
|
const outputPath =
|
||||||
@@ -1236,7 +1220,16 @@ async function runSingleExport(
|
|||||||
markResourceExists(voiceUrl)
|
markResourceExists(voiceUrl)
|
||||||
}
|
}
|
||||||
message.voiceDataUrl = 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) {
|
} catch (error) {
|
||||||
keepMediaError(
|
keepMediaError(
|
||||||
request,
|
request,
|
||||||
@@ -1345,28 +1338,12 @@ async function runSingleExport(
|
|||||||
const reusableImageQuality =
|
const reusableImageQuality =
|
||||||
previous?.exportMediaQuality === 'original' ||
|
previous?.exportMediaQuality === 'original' ||
|
||||||
(request.preferOriginal === false && previous?.exportMediaQuality === 'thumbnail')
|
(request.preferOriginal === false && previous?.exportMediaQuality === 'thumbnail')
|
||||||
let reusableMediaExists = false
|
|
||||||
if (reusableMediaType && previous?.exportMediaUrl) {
|
|
||||||
reusableMediaExists = await resourceExists(previous.exportMediaUrl)
|
|
||||||
if (
|
|
||||||
reusableMediaExists &&
|
|
||||||
reusableMediaType === 'sticker' &&
|
|
||||||
!(await htmlArchiveStickerResourceIsValid(outputDir, previous.exportMediaUrl))
|
|
||||||
) {
|
|
||||||
reusableMediaExists = false
|
|
||||||
resourceExistence.set(previous.exportMediaUrl, Promise.resolve(false))
|
|
||||||
delete previous.exportMediaUrl
|
|
||||||
delete previous.exportMediaType
|
|
||||||
delete previous.exportMediaName
|
|
||||||
delete previous.exportMediaQuality
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
reusableMediaType &&
|
reusableMediaType &&
|
||||||
previous?.exportMediaUrl &&
|
previous?.exportMediaUrl &&
|
||||||
(!previous.exportMediaType || previous.exportMediaType === reusableMediaType) &&
|
(!previous.exportMediaType || previous.exportMediaType === reusableMediaType) &&
|
||||||
(reusableMediaType !== 'image' || reusableImageQuality) &&
|
(reusableMediaType !== 'image' || reusableImageQuality) &&
|
||||||
reusableMediaExists
|
(await resourceExists(previous.exportMediaUrl))
|
||||||
) {
|
) {
|
||||||
message.exportMediaUrl = previous.exportMediaUrl
|
message.exportMediaUrl = previous.exportMediaUrl
|
||||||
message.exportMediaType = reusableMediaType
|
message.exportMediaType = reusableMediaType
|
||||||
@@ -1478,10 +1455,13 @@ async function runSingleExport(
|
|||||||
} else if (message.contentData.type === 'sticker' && stickerService) {
|
} else if (message.contentData.type === 'sticker' && stickerService) {
|
||||||
const stickerSource = message.contentData.url || message.contentData.thumbUrl
|
const stickerSource = message.contentData.url || message.contentData.thumbUrl
|
||||||
const result = await stickerService.resolveSticker(stickerSource, message.contentData.md5)
|
const result = await stickerService.resolveSticker(stickerSource, message.contentData.md5)
|
||||||
const decoded = result.data ? decodeDataUrl(result.data) : null
|
const decoded = result.data
|
||||||
const stickerExtension = decoded ? detectAssetExtension(decoded.buffer) : null
|
? decodeDataUrl(result.data)
|
||||||
if (decoded && stickerExtension) {
|
: stickerSource
|
||||||
const name = `sticker_${bufferHashPart(decoded.buffer)}.${stickerExtension}`
|
? await readAvatarAsset(stickerSource)
|
||||||
|
: null
|
||||||
|
if (decoded) {
|
||||||
|
const name = `sticker_${bufferHashPart(decoded.buffer)}.${decoded.extension}`
|
||||||
const mediaUrl = `media/${name}`
|
const mediaUrl = `media/${name}`
|
||||||
if (!(await resourceExists(mediaUrl))) {
|
if (!(await resourceExists(mediaUrl))) {
|
||||||
await fs.writeFile(join(outputDir, 'media', name), decoded.buffer)
|
await fs.writeFile(join(outputDir, 'media', name), decoded.buffer)
|
||||||
|
|||||||
@@ -125,7 +125,11 @@ export class VoiceService {
|
|||||||
codec: 'silk',
|
codec: 'silk',
|
||||||
sourceHash: createHash('sha256').update(silkData).digest('hex')
|
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 data = wavData.toString('base64')
|
||||||
const cacheKey = this.buildCacheKey(
|
const cacheKey = this.buildCacheKey(
|
||||||
reference.sessionId,
|
reference.sessionId,
|
||||||
@@ -239,7 +243,7 @@ export class VoiceService {
|
|||||||
|
|
||||||
private createWavBuffer(
|
private createWavBuffer(
|
||||||
pcmData: Buffer,
|
pcmData: Buffer,
|
||||||
sampleRate: number = 24000,
|
sampleRate: number = 16000,
|
||||||
channels: number = 1
|
channels: number = 1
|
||||||
): Buffer {
|
): Buffer {
|
||||||
const pcmLength = pcmData.length
|
const pcmLength = pcmData.length
|
||||||
|
|||||||
Reference in New Issue
Block a user