fix: WeChat login Content-Type header, ASR tiny model, re-upload mini-program v1.0.11
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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, '')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user