2025-06-27 08:44:35 +08:00

239 lines
6.8 KiB
TypeScript

import express from 'express';
import cors from 'cors';
import { Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import Readline from 'readline';
import { createHash } from 'crypto';
import ttsSdk from "microsoft-cognitiveservices-speech-sdk"
import { JSDOM } from 'jsdom';
const app = express();
const PORT = process.env.PORT || 3000;
// 启用CORS
app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
app.use(express.json());
app.use('/audio', express.static(path.join(__dirname, 'audio')));
// 从文本文件读取游戏事件
const prompts = [
{ voice: "zh-CN-XiaoxiaoNeural" },
{ voice: "zh-CN-YunxiNeural" },
{ voice: "zh-CN-XiaohanNeural" },
{ voice: "zh-CN-YunjianNeural" },
{ voice: "zh-CN-XiaoyiNeural" },
{ voice: "zh-CN-XiaochenNeural" },
]
/* const prompts = [
{ voice: "zh-CN-XiaoxiaoNeural" },
{ voice: "zh-CN-XiaochenNeural" },
{ voice: "zh-CN-YunyangNeural" },
{ voice: "zh-CN-XiaohanNeural" },
{ voice: "zh-CN-XiaomengNeural" },
{ voice: "zh-CN-XiaomoNeural" }
] */
const readEventsFromFile = async (filePath: string): Promise<any[]> => {
let lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(line => line.trim() !== '').map(line => JSON.parse(line));
for (const line of lines) {
if (line.type == "speak" || line.type == "wolfChat") {
let promptIndex = line.data.player - 1;
if (promptIndex < 0 || promptIndex >= prompts.length) {
console.error(`Invalid prompt index ${promptIndex} for line ${JSON.stringify(line)}`);
process.exit(1);
}
let speech = await resolveSpeech(line.data.message, prompts[promptIndex]!.voice)
line.data.voice = speech.voice;
line.data.message = speech.text;
}
}
return lines;
}
function createResolvablePromise() {
let resolve;
const promise = new Promise(r => {
resolve = r;
});
return { promise, resolve };
}
let continueController = createResolvablePromise();
function waitForContinue() {
return continueController.promise;
}
const rl = Readline.createInterface({
input: process.stdin,
output: process.stdout
});
function setupEnterKeyListener() {
console.log("按回车键继续...");
rl.once('line', (input) => {
// 解析等待的 Promise
//@ts-ignore
continueController.resolve(input || 'continued');
continueController = createResolvablePromise();
});
}
app.post('/*', (req: Request, res: Response) => {
res.end('ok')
})
const filePath = path.join(__dirname, 'events_0.jsonl');
const events = await readEventsFromFile(filePath);
app.get('/:gameId/events', async (req: Request, res: Response) => {
const gameId = req.params.gameId;
let disconnected = false;
// 设置SSE响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
const filePath = path.join(__dirname, 'events_0.jsonl');
const events = await readEventsFromFile(filePath);
console.log(`客户端已连接到游戏${gameId},准备发送${events.length}个事件`);
let eventIndex = 0;
for (let i = 0; i < events.length; i++) {
if (disconnected) break
const event = events[i];
setupEnterKeyListener(); // 设置监听回车
await waitForContinue();
res.write(`data: ${JSON.stringify(event)}\n\n`);
res.socket?.write('');
console.log(`发送事件 ${eventIndex + 1}/${events.length}:`, event.type);
eventIndex++;
}
// 处理客户端断开连接
req.on('close', () => {
console.log(`客户端已断开连接`);
disconnected = true;
});
});
app.listen(PORT, () => {
console.log(`服务器启动,监听端口 ${PORT}`);
});
async function resolveSpeech(raw: string, voice: string): Promise<{
voice: string,
text: string
}> {
const wrappedXml = `<root>${raw}</root>`;
// 使用JSDOM解析XML
const dom = new JSDOM(wrappedXml, { contentType: 'text/xml' });
const document = dom.window.document;
const scriptElements: ({
type: 'action',
content: string
} | {
type: 'audio',
style: string | null,
content: string,
text: string,
segments: string[]
})[] = [];
document.querySelectorAll('action, audio').forEach(el => {
if (el.tagName.toLowerCase() === 'action') {
scriptElements.push({
type: 'action',
content: el.textContent?.trim() || ""
});
} else if (el.tagName.toLowerCase() === 'audio') {
const style = el.getAttribute('style');
const content = el.textContent?.trim() || "";
scriptElements.push({
type: 'audio',
style,
content: content,
text: content.split('|').join(''),
segments: content.split('|')
});
}
});
let ssml = `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN">
<voice name="${voice}">
${scriptElements.filter(el => el.type === 'audio')
.map(el => `<mstts:express-as style="${el.style}" styledegree="1">${el.segments.join('')}</mstts:express-as>`)
.join('<break strength="medium" />')}
</voice>
</speak>`
const fileName = createHash('sha256')
.update(ssml)
.digest('hex');
const filePath = path.join(__dirname, `audio/${fileName}.mp3`);
console.log(`${fileName} -> ${scriptElements.filter(el => el.type === 'audio')
.map(el => `${el.segments.join('')}`)
.join('')} `);
let clientText = scriptElements.map(el => el.type == 'action' ? `<span class="action">(${el.content})</span>` : el.content).join(' ');
if (fs.existsSync(filePath)) {
return {
voice: fileName,
text: clientText
};
}
const speechConfig = ttsSdk.SpeechConfig.fromSubscription(
"6KW9CQeOlrSIHTodWJoiVD3aEtbbek3GwsKIbTfXBmVgngz2te0dJQQJ99BDACYeBjFXJ3w3AAAAACOGWMAe",
"eastus"
);
speechConfig.speechSynthesisVoiceName = voice;
const audioConfig = ttsSdk.AudioConfig.fromAudioFileOutput(filePath);
const synthesizer = new ttsSdk.SpeechSynthesizer(speechConfig, audioConfig);
console.log(`正在合成语音: ${ssml} - ${voice} - ${fileName}`);
return new Promise((resolve, reject) => {
synthesizer.speakSsmlAsync(ssml,
function (result) {
if (result.reason === ttsSdk.ResultReason.SynthesizingAudioCompleted) {
console.log("synthesis finished.");
synthesizer.close();
resolve({
voice: fileName,
text: clientText
});
} else {
console.error("Speech synthesis canceled, " + result.errorDetails);
synthesizer.close();
reject(new Error(result.errorDetails || "Speech synthesis failed"));
}
},
function (err) {
console.trace("err - " + err);
synthesizer.close();
reject(err);
});
});
}