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 => { 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 = `${raw}`; // 使用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 = ` ${scriptElements.filter(el => el.type === 'audio') .map(el => `${el.segments.join('')}`) .join('')} ` 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' ? `(${el.content})` : 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); }); }); }