fix: WeChat login Content-Type header, ASR tiny model, re-upload mini-program v1.0.11

This commit is contained in:
yuzhiran
2026-06-15 10:00:02 +08:00
parent 4fa620f0a2
commit 18c50726cd
22 changed files with 311 additions and 128 deletions
@@ -107,7 +107,7 @@ ${conversationHistory}
if (aiMsg?.content) {
try {
const tts = await this.ttsService.synthesize(aiMsg.content)
return { ...base, ttsHash: tts.hash, ttsDurationMs: tts.durationMs }
return { ...base, ttsHash: tts.hash, ttsDurationMs: tts.durationMs, ttsAmplitude: tts.amplitudeData }
} catch {
// TTS failure is non-critical, return without audio
}
@@ -21,8 +21,11 @@ export class WechatPayService {
this.logger.warn('微信支付配置不完整,支付功能不可用')
}
const certDir = path.resolve(__dirname, '../../certs')
if (!fs.existsSync(certDir)) {
this.logger.error(`证书目录不存在: ${certDir}`)
return
}
this.privateKey = fs.readFileSync(path.join(certDir, 'apiclient_key.pem'), 'utf8')
// 从证书中提取序列号
const cert = fs.readFileSync(path.join(certDir, 'apiclient_cert.pem'), 'utf8')
const certObj = new crypto.X509Certificate(cert)
this.mchSerialNo = certObj.serialNumber
@@ -122,6 +125,10 @@ export class WechatPayService {
// 1. 验签
const message = `${wechatTimestamp}\n${wechatNonce}\n${JSON.stringify(body)}\n`
const certDir = path.resolve(__dirname, '../../certs')
if (!fs.existsSync(certDir)) {
this.logger.error(`证书目录不存在: ${certDir}`)
return null
}
const platformCert = fs.readFileSync(path.join(certDir, 'pub_key.pem'), 'utf8')
const verify = crypto.createVerify('RSA-SHA256').update(message)
const isValid = verify.verify(platformCert, wechatSignature, 'base64')
+2 -3
View File
@@ -19,7 +19,7 @@ export class TtsController {
throw new HttpException('文本不能为空且不超过500字', HttpStatus.BAD_REQUEST)
}
const result = await this.ttsService.synthesize(text, voice)
return { hash: result.hash, durationMs: result.durationMs }
return { hash: result.hash, durationMs: result.durationMs, amplitudeData: result.amplitudeData }
}
@Public()
@@ -59,8 +59,7 @@ export class TtsController {
const parsed = JSON.parse(result)
if (parsed.text) return { text: parsed.text.trim() }
}
const whisperBin = '/root/.local/bin/whisper'
const whisperResult = execSync(`${whisperBin} "${dest}" --language zh --output_format txt 2>/dev/null`, { encoding: 'utf8', timeout: 60000 })
const whisperResult = execSync(`python3 -c 'import sys, whisper; model = whisper.load_model("tiny"); print(model.transcribe(sys.argv[1], language="zh")["text"].strip())' "${dest}"`, { encoding: 'utf8', timeout: 60000 })
if (whisperResult && whisperResult.trim()) {
return { text: whisperResult.trim() }
}
+47 -2
View File
@@ -10,6 +10,7 @@ interface TtsResult {
hash: string
filePath: string
durationMs: number
amplitudeData: number[]
}
const VALID_VOICES = new Set([
@@ -41,7 +42,10 @@ export class TtsService {
if (fs.existsSync(filePath)) {
const durationMs = await this.getDuration(filePath)
return { hash, filePath, durationMs }
const amplitudeData = this.loadAmplitudeData(hash)
if (amplitudeData) {
return { hash, filePath, durationMs, amplitudeData }
}
}
try {
@@ -50,8 +54,9 @@ export class TtsService {
{ timeout: 30000 },
)
const durationMs = await this.getDuration(filePath)
const amplitudeData = this.extractAmplitude(filePath, hash)
this.logger.log(`TTS generated: hash=${hash} text="${text.slice(0, 40)}..." duration=${durationMs}ms`)
return { hash, filePath, durationMs }
return { hash, filePath, durationMs, amplitudeData }
} catch (e) {
this.logger.error(`TTS failed: ${e.message}`)
throw e
@@ -63,6 +68,46 @@ export class TtsService {
return fs.existsSync(filePath) ? filePath : null
}
private extractAmplitude(mp3Path: string, hash: string): number[] {
try {
const pcmPath = `/tmp/tts-cache/${hash}.pcm`
execSync(
`ffmpeg -y -i "${mp3Path}" -f s16le -acodec pcm_s16le -ar 16000 -ac 1 "${pcmPath}" 2>/dev/null`,
{ timeout: 10000 },
)
const pcmBuf = fs.readFileSync(pcmPath)
const samples = new Int16Array(pcmBuf.buffer, pcmBuf.byteOffset, pcmBuf.byteLength / 2)
const chunkSize = Math.floor(16000 * 0.05) // 50ms
const amplitudes: number[] = []
for (let i = 0; i < samples.length; i += chunkSize) {
const end = Math.min(i + chunkSize, samples.length)
let sumSq = 0
for (let j = i; j < end; j++) {
sumSq += samples[j] * samples[j]
}
const rms = Math.sqrt(sumSq / (end - i))
amplitudes.push(Number((Math.min(1, rms / 16000)).toFixed(4)))
}
try { fs.unlinkSync(pcmPath) } catch {}
const ampPath = `/tmp/tts-cache/${hash}.amp`
fs.writeFileSync(ampPath, JSON.stringify(amplitudes))
return amplitudes
} catch (e) {
this.logger.warn(`振幅提取失败: ${e.message}`)
return []
}
}
private loadAmplitudeData(hash: string): number[] | null {
try {
const ampPath = `/tmp/tts-cache/${hash}.amp`
if (!fs.existsSync(ampPath)) return null
return JSON.parse(fs.readFileSync(ampPath, 'utf8'))
} catch {
return null
}
}
private escapeText(text: string): string {
return text.replace(/"/g, '\\"').replace(/\n/g, ' ').replace(/\r/g, '')
}