commit 1cbfe2005e67db63a15eb432f21214acf6121b64 Author: feie9456 Date: Fri Jun 27 08:44:35 2025 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a42f537 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +audio +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..4efbcab --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# backend-test + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.5. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/events.jsonl b/events.jsonl new file mode 100644 index 0000000..63d9635 --- /dev/null +++ b/events.jsonl @@ -0,0 +1,58 @@ +{"type": "gameStartRequest", "data": {"players": [{"id": 1, "name": "Alexander", "skin": 4, "observer": false, "role": "wolf"}]}} +{"type": "gameStartRequest", "data": {"players": [{"id": 1, "name": "Alexander", "skin": 4, "observer": false, "role": "wolf"},{"id": 2, "name": "Bella", "skin": 6, "observer": false, "role": "village"}]}} +{"type": "gameStartRequest", "data": {"players": [{"id": 1, "name": "Alexander", "skin": 4, "observer": false, "role": "wolf"},{"id": 2, "name": "Bella", "skin": 6, "observer": false, "role": "village"},{"id": 3, "name": "Charlie", "skin": 9, "observer": false, "role": "seer"}]}} +{"type": "gameStartRequest", "data": {"players": [{"id": 1, "name": "Alexander", "skin": 4, "observer": false, "role": "wolf"},{"id": 2, "name": "Bella", "skin": 6, "observer": false, "role": "village"},{"id": 3, "name": "Charlie", "skin": 9, "observer": false, "role": "seer"},{"id": 4, "name": "Daisy", "skin": 3, "observer": false, "role": "village"}]}} +{"type": "gameStartRequest", "data": {"players": [{"id": 1, "name": "Alexander", "skin": 4, "observer": false, "role": "wolf"},{"id": 2, "name": "Bella", "skin": 6, "observer": false, "role": "village"},{"id": 3, "name": "Charlie", "skin": 9, "observer": false, "role": "seer"},{"id": 4, "name": "Daisy", "skin": 3, "observer": false, "role": "village"},{"id": 5, "name": "Emma", "skin": 8, "observer": false, "role": "witch"}]}} +{"type": "gameStart", "data": {"players": [{"id": 1, "name": "Alexander", "skin": 4, "observer": false, "role": "wolf"},{"id": 2, "name": "Bella", "skin": 6, "observer": false, "role": "village"},{"id": 3, "name": "Charlie", "skin": 9, "observer": false, "role": "seer"},{"id": 4, "name": "Daisy", "skin": 3, "observer": false, "role": "village"},{"id": 5, "name": "Emma", "skin": 8, "observer": false, "role": "witch"},{"id": 6, "name": "Felix", "skin": 2, "observer": false, "role": "wolf"}],"role": "wolf","scene": 3}} +{"type": "phaseChange", "data": {"day": 1, "phase": "night"}} +{"type": "wolfTurn", "data": {"target": 1}} +{"type": "wolfAction", "data": {"player": 1, "action": "kill", "target": 2}} +{"type": "phaseChange", "data": {"day": 1, "phase": "day"}} +{"type": "globalNotice", "data": {"message": "天亮了,大家请睁眼。昨晚平安夜,没有人死亡。"}} +{"type": "turnToSpeak", "data": {"target": 1}} +{"type": "speak", "data": {"player": 1, "message": ""}} +{"type": "speak", "data": {"player": 2, "message": "咧嘴一笑,向后靠在椅子上挑眉看向四周耸耸肩"}} +{"type": "speak", "data": {"player": 3, "message": "身体微微前倾,表情谨慎若有所思地看着其他人轻声补充"}} +{"type": "speak", "data": {"player": 4, "message": "直视前方,表情平静环顾四周略微点头"}} +{"type": "speak", "data": {"player": 5, "message": "双手轻放桌面,表情略显犹豫目光短暂停留在2号身上轻声补充"}} +{"type": "speak", "data": {"player": 6, "message": "坐姿端正,表情平静微微向前倾身环顾四周"}} +{"type": "noteAppear", "data": {"message": "一张神秘的小纸条突然从天而降,飘到了桌子中央...3号玩家捡到了这张纸条。"}} +{"type": "speak", "data": {"player": 3, "message": "表情略显困惑环视众人轻轻摇头"}} +{"type": "speak", "data": {"player": 2, "message": "挑眉,表情困惑高举纸条环顾四周,眼神锐利"}} +{"type": "speak", "data": {"player": 6, "message": "表情略显惊讶,随后恢复冷静环视众人看向5号"}} +{"type": "turnToVote", "data": {"target": 1}} +{"type": "vote", "data": {"player": 1, "target": 3}} +{"type": "vote", "data": {"player": 2, "target": 6}} +{"type": "vote", "data": {"player": 3, "target": 6}} +{"type": "vote", "data": {"player": 4, "target": 6}} +{"type": "vote", "data": {"player": 5, "target": 6}} +{"type": "vote", "data": {"player": 6, "target": 3}} +{"type": "voteEnd", "data": {"target": 6}} +{"type": "playerKilled", "data": {"targets": [6]}} +{"type": "phaseChange", "data": {"day": 2, "phase": "night"}} +{"type": "wolfTurn", "data": {"target": 1}} +{"type": "wolfAction", "data": {"player": 1, "action": "kill", "target": 3}} +{"type": "phaseChange", "data": {"day": 2, "phase": "day"}} +{"type": "globalNotice", "data": {"message": "天亮了,大家请睁眼。昨晚3号死亡。"}} +{"type": "playerKilled", "data": {"targets": [3]}} +{"type": "speak", "data": {"player": 2, "message": "表情严肃,不再嬉笑环视众人看向1号"}} +{"type": "turnToSpeak", "data": {"target": 1}} +{"type": "speak", "data": {"player": 1, "message": ""}} +{"type": "speak", "data": {"player": 5, "message": "表情先是震惊,然后坚定环顾四周看向2号"}} +{"type": "speak", "data": {"player": 4, "message": "表情严肃,语气坚定直视1号看向5号"}} +{"type": "noteAppear", "data": {"message": "最后一张小纸条飘落到桌子中央...1号玩家捡到了这张纸条。"}} +{"type": "notePick", "data": {"player": 1, "content": "一人假面具正义,临危跳职保自身"}} +{"type": "turnToNote", "data": {"target": 1}} +{"type": "noteAction", "data": {"player": 1, "action": "modify", "originalContent": "一人假面具正义,临危跳职保自身", "newContent": "五人药水有玄机,解毒两瓶皆在握"}} +{"type": "noteAction", "data": {"player": 1, "action": "drop"}} +{"type": "speak", "data": {"player": 4, "message": "皱眉,表情怀疑看向5号转向1号"}} +{"type": "turnToSpeak", "data": {"target": 1}} +{"type": "speak", "data": {"player": 1, "message": ""}} +{"type": "vote", "data": {"player": 2, "target": 1}} +{"type": "turnToVote", "data": {"target": 1}} +{"type": "vote", "data": {"player": 1, "target": 5}} +{"type": "vote", "data": {"player": 5, "target": 1}} +{"type": "vote", "data": {"player": 4, "target": 1}} +{"type": "voteEnd", "data": {"target": 1}} +{"type": "playerKilled", "data": {"targets": [1]}} +{"type": "gameOver", "data": {"result": "defeat", "winningSide": "village", "reason": "好人阵营获胜,你失败了!"}} \ No newline at end of file diff --git a/events_0.jsonl b/events_0.jsonl new file mode 100644 index 0000000..6fefb8a --- /dev/null +++ b/events_0.jsonl @@ -0,0 +1,92 @@ +{"type": "gameStartRequest","data": {"players": [{"id": 1,"name": "Alice","skin": 0, "role": "wolf"}],"role": "observer","scene": 1}} +{"type": "gameStartRequest","data": {"players": [{"id": 1,"name": "Alice","skin": 0, "role": "wolf"},{"id": 2,"name": "Bob","skin": 2, "role": "wolf"}],"role": "observer","scene": 1}} +{"type": "gameStartRequest","data": {"players": [{"id": 1,"name": "Alice","skin": 0, "role": "wolf"},{"id": 2,"name": "Bob","skin": 2, "role": "wolf"},{"id": 5,"name": "Eve","skin": 5, "role": "witch"}],"role": "observer","scene": 1}} +{"type": "gameStartRequest","data": {"players": [{"id": 1,"name": "Alice","skin": 0, "role": "wolf"},{"id": 2,"name": "Bob","skin": 2, "role": "wolf"},{"id": 3,"name": "Charlie","skin": 3, "role": "village"},{"id": 5,"name": "Eve","skin": 5, "role": "witch"}],"role": "observer","scene": 1}} +{"type": "gameStartRequest","data": {"players": [{"id": 1,"name": "Alice","skin": 0, "role": "wolf"},{"id": 2,"name": "Bob","skin": 3, "role": "wolf"},{"id": 3,"name": "Charlie","skin": 2, "role": "village"},{"id": 4,"name": "Diana","skin": 3, "role": "village"},{"id": 5,"name": "Eve","skin": 5, "role": "witch"},{"id": 6,"name": "Frank","skin": 8, "role": "seer"}],"role": "observer","scene": 1}} +{"type": "gameStart","data": {"players": [{"id": 1,"name": "Alice","skin": 0, "role": "wolf"},{"id": 2,"name": "Bob","skin": 11, "role": "wolf"},{"id": 3,"name": "Charlie","skin": 6, "role": "village"},{"id": 4,"name": "Diana","skin": 3, "role": "village"},{"id": 5,"name": "Eve","skin": 5, "role": "witch"},{"id": 6,"name": "Frank","skin": 8, "role": "seer"}],"role": "observer","scene": 1}} +{"type": "phaseChange", "data": {"day": 1, "phase": "night"}} +{"type": "reasoning", "data": {"player": 1, "message": "第一晚|没有|信息|可供|分析|,|随机|选|一个|玩家"}} +{"type": "wolfAction", "data": {"player": 1, "action": "kill", "target": 4}} +{"type": "reasoning", "data": {"player": 6, "message": "第一晚|没有|信息|可供|分析|,|随机|选|一个|玩家"}} +{"type": "seerAction", "data": {"target": 2}} +{"type": "seerResult", "data": {"target": 2, "role": "wolf"}} +{"type": "reasoning", "data": {"player": 5, "message": "第一晚|救人|很|重要|,|能够|保留|更多|的|好人|力量|。|虽然|不知道|被杀|的|是|谁|,|但|应该|是|个|对|好人|阵营|有|价值|的|角色|。"}} +{"type": "witchAction", "data": {"usedAntidote": true, "antidoteTarget": 4, "usedPoison": false}} +{"type": "phaseChange", "data": {"day": 1, "phase": "day"}} +{"type": "globalNotice", "data": {"message": "天亮了,大家请睁眼。昨晚平安夜,没有人死亡。"}} +{"type": "reasoning", "data": {"player": 1, "message": "平安夜|意味|着|女巫|用了|解药|。|现在|不要|急|着|跳|预言家|,|先|观察|局势|,|看看|真|预言家|会|不会|跳出来|。|我|应该|先引导|大家|讨论|女巫|的|身份|,|分散|注意力|。"}} +{"type": "speak", "data": {"player": 1, "message": "双手交叉放在桌上,微微前倾环顾四周"}} +{"type": "reasoning", "data": {"player": 2, "message": "我|需要|制造|信息|干扰|,|拖延|时间|,|同时|试探|预言家|是否|会|跳出来|。|表现|得|激动|一些|,|掩盖|我|是|狼人|的|事实|。"}} +{"type": "speak", "data": {"player": 2, "message": "双手夸张地挥舞身体前倾,目光扫视每个人做出思考状"}} +{"type": "reasoning", "data": {"player": 3, "message": "伪装|成|新手|可以|降低|狼人|对|我|的|警惕性|,|同时|通过|提问|来|观察|其他|玩家|的|反应|,|特别|是|关于|被|救者|身份|的|问题|,|狼人|可能|会|有|特别|的|反应|。"}} +{"type": "speak", "data": {"player": 3, "message": "挠头,表情困惑眼睛微微睁大,恍然大悟"}} +{"type": "reasoning", "data": {"player": 4, "message": "仔细|分析|前面|玩家|的|发言|,|尤其|是|1号|和|2号|,|他们|的|发言|模式|很|可疑|。"}} +{"type": "speak", "data": {"player": 4, "message": "皱眉,目光锐利指向2号转向1号"}} +{"type": "reasoning", "data": {"player": 5, "message": "作为|女巫|,|我|已经|用了|解药|,|剩下|毒药|需要|谨慎|使用|。|现在|不要|暴露|自己|,|先|同意|4号|对|2号|的|怀疑|,|但|语气|保持|犹豫|。"}} +{"type": "speak", "data": {"player": 5, "message": "身体微微后缩,目光游移小心翼翼地看了看4号"}} +{"type": "reasoning", "data": {"player": 6, "message": "我|已经|确认|2号|是|狼人|,|考虑到|女巫|已经|用了|解药|,|如果|今晚|我|被|杀|就|没有|人|能|救|我|了|。|现在|是|跳出来|的|最佳|时机|,|必须|尽快|告诉|大家|我|的|发现|。"}} +{"type": "speak", "data": {"player": 6, "message": "坐直身体,表情严肃目光坚定地扫视全场"}} +{"type": "noteAppear", "data": {"message": "在投票前,一张神秘的小纸条从天而降,落到了场中..."}} +{"type": "notePick", "data": {"player": 3, "content": "二人饭后常散步,月下窃窃私语甚是亲密"}} +{"type": "reasoning", "data": {"player": 3, "message": "这条|信息|似乎|在|暗示|2号|的|身份|,|与|预言家|的|判断|吻合|。|但|如果|我|直接|表明|,|可能|会|暴露|信息|来源|。|我|可以|利用|这个|机会|试探|其他|人|的|反应|。"}} +{"type": "noteAction", "data": {"player": 3, "action": "modify", "originalContent": "二人饭后常散步,月下窃窃私语甚是亲密", "newContent": "四人独处时常皱眉,似有心事难言"}} +{"type": "noteAction", "data": {"player": 3, "action": "drop"}} +{"type": "notePick", "data": {"player": 4, "content": "四人独处时常皱眉,似有心事难言"}} +{"type": "reasoning", "data": {"player": 4, "message": "这张|纸条|内容|很|奇怪|,|似乎|在|暗示|4号|有|问题|,|但|这|与|我|的|直觉|不符|。|考虑到|我|刚刚|才|质疑|了|1号|和|2号|,|这|可能|是|狼人|阵营|的|诡计|。|我|应该|在|投票|前|提出来|讨论|。"}} +{"type": "noteAction", "data": {"player": 4, "action": "drop"}} +{"type": "speak", "data": {"player": 4, "message": "举起纸条,表情困惑念出内容眼神锐利地扫视全场"}} +{"type": "reasoning", "data": {"player": 3, "message": "4号|的|困惑|是|真实|的|,|这|是|我|的|机会|,|可以|继续|扮演|新手|角色|。"}} +{"type": "speak", "data": {"player": 3, "message": "尴尬地举手低头看着自己的手指抬头,天真"}} +{"type": "reasoning", "data": {"player": 2, "message": "小|纸条|暴露|了|信息|,|需要|立即|反击|混淆|局面|。"}} +{"type": "speak", "data": {"player": 2, "message": "猛地站起来,表情夸张指着3号双手叉腰"}} +{"type": "reasoning", "data": {"player": 1, "message": "必须|乘胜追击|,|引导|大家|怀疑|3号|和|6号|的|联合|。"}} +{"type": "speak", "data": {"player": 1, "message": "保持冷静,但语气坚定轻轻指向3号转向6号,眉头微皱"}} +{"type": "reasoning", "data": {"player": 6, "message": "狼人|的|联合|反击|很|强势|,|但|小|纸条|内容|恰恰|佐证|了|我|的|预言|结果|,|这|是|个|机会|。"}} +{"type": "speak", "data": {"player": 6, "message": "双手平放在桌上,表情镇定用专业的语气分析目光坚定扫视全场"}} +{"type": "reasoning", "data": {"player": 5, "message": "现在|情况|更加|复杂|了|,|但|预言家|和|小|纸条|都|指向|2号|是|狼人|,|这个|信息|很|可能|是|可靠|的|。"}} +{"type": "speak", "data": {"player": 5, "message": "身体微微颤抖,声音轻柔偷瞄了一眼2号又迅速移开视线"}} +{"type": "reasoning", "data": {"player": 1, "message": "预言家|已经|跳出来|指认|2号|,|必须|投|预言家|来|制造|混乱|,|为|后续|跳|预言家|做|准备|。"}} +{"type": "vote", "data": {"player": 1, "target": 6}} +{"type": "reasoning", "data": {"player": 2, "message": "被|预言家|指认|了|,|但|现在|跟随|狼|队友|的|策略|,|一起|投|预言家|,|争取|混淆|局面|。"}} +{"type": "vote", "data": {"player": 2, "target": 6}} +{"type": "reasoning", "data": {"player": 3, "message": "预言家|的|信息|很|有|价值|,|2号|表现|也|确实|可疑|,|我|的|小|纸条|策略|虽然|有些|冒险|,|但|最终|帮助|确认|了|这|一点|。|我|相信|预言家|的|判断|。"}} +{"type": "vote", "data": {"player": 3, "target": 2}} +{"type": "reasoning", "data": {"player": 4, "message": "预言家|给|出|了|明确|的|狼人|信息|,|而且|与|小|纸条|内容|相符|,|应该|投|出|2号|。"}} +{"type": "vote", "data": {"player": 4, "target": 2}} +{"type": "reasoning", "data": {"player": 5, "message": "预言家|的|信息|非常|关键|,|我|应该|支持|他|的|判断|,|投|出|2号|。"}} +{"type": "vote", "data": {"player": 5, "target": 2}} +{"type": "reasoning", "data": {"player": 6, "message": "我|已经|确认|2号|是|狼人|,|必须|投|出|他|。"}} +{"type": "vote", "data": {"player": 6, "target": 2}} +{"type": "voteEnd", "data": {"target": 2}} +{"type": "playerKilled", "data": {"targets": [2]}} +{"type": "phaseChange", "data": {"day": 2, "phase": "night"}} +{"type": "reasoning", "data": {"player": 1, "message": "现在|只|剩|我|一个|狼人|了|,|预言家|已经|暴露|,|必须|先|清除|他|,|否则|明天|我|就|会|被|投|出局|。"}} +{"type": "wolfAction", "data": {"player": 1, "action": "kill", "target": 6}} +{"type": "reasoning", "data": {"player": 5, "message": "我|已经|没有|解药|了|,|不能|救|预言家|。|是否|要|使用|毒药|?|现在|还|不能|确定|1号|就|是|狼人|,|如果|误|杀|好人|会|很|危险|,|先|保留|毒药|。"}} +{"type": "phaseChange", "data": {"day": 2, "phase": "day"}} +{"type": "globalNotice", "data": {"message": "天亮了,大家请睁眼。昨晚6号被刀。"}} +{"type": "playerKilled", "data": {"targets": [6]}} +{"type": "noteAppear", "data": {"message": "又一张神秘小纸条落入场中,这次是5号玩家捡到了。"}} +{"type": "notePick", "data": {"player": 5, "content": "一人故作镇定,夜间却形迹可疑"}} +{"type": "reasoning", "data": {"player": 5, "message": "这|很|可能|在|暗示|1号|是|狼人|。|结合|6号|预言家|的|死亡|,|以及|1号|之前|的|可疑|行为|,|我|应该|告诉|大家|这个|信息|,|但|要|小心|不要|暴露|自己|的|身份|。"}} +{"type": "noteAction", "data": {"player": 5, "action": "drop"}} +{"type": "speak", "data": {"player": 5, "message": "声音轻柔,有些颤抖缓缓展示纸条偷偷瞄了一眼1号"}} +{"type": "reasoning", "data": {"player": 1, "message": "预言家|被|杀|,|是|我|自救|的|最佳|时机|。|我|应该|马上|跳|预言家|,|声称|自己|查到|了|3号|是|好人|,|然后|把|怀疑|引向|4号|和|5号|。|同时|我|需要|对抗|小|纸条|带来|的|怀疑|。"}} +{"type": "speak", "data": {"player": 1, "message": "从容不迫地站起身,轻轻整理衣领竖起一根手指目光锐利地扫向4号和5号嘲讽地扬起纸条"}} +{"type": "reasoning", "data": {"player": 3, "message": "1号|的|行为|非常|可疑|,|真|预言家|已经|跳|了|,|他|为什么|第一天|不|跳|?|很|可能|是|狼人|在|自救|。|加上|小|纸条|的|暗示|,|我|更加|确定|他|有|问题|。"}} +{"type": "speak", "data": {"player": 3, "message": "挠头,表情从困惑转为怀疑歪着头,装作天真眼睛忽然变得锐利"}} +{"type": "reasoning", "data": {"player": 4, "message": "1号|明显|是|在|伪装|预言家|,|试图|混淆|视听|。|真|预言家|已经|确认|了|2号|是|狼人|,|而|1号|现在|突然|跳|预言家|,|这|是|狼人|惯用|的|自救|手段|。|小|纸条|再次|证实|了|我|的|判断|。"}} +{"type": "speak", "data": {"player": 4, "message": "拍桌而起,眼神凌厉用手指着1号冷笑一声"}} +{"type": "reasoning", "data": {"player": 5, "message": "局势|变得|更加|复杂|了|,|但|1号|的|行为|确实|很|可疑|,|我|同意|4号|的|看法|,|但|还是|保持|一定|的|谨慎|态度|。|考虑到|我|拿到|了|小|纸条|,|应该|更|坚定|一些|。"}} +{"type": "speak", "data": {"player": 5, "message": "小心地向前倾身,声音比平时稍大举起手中的纸条鼓起勇气直视1号"}} +{"type": "reasoning", "data": {"player": 1, "message": "我|必须|转移|怀疑|,|5号|的|发言|提到|了|小|纸条|,|是|引起|争议|的|关键|。|必须|把|票|投|给|她|,|同时|争取|3号|的|信任|。"}} +{"type": "vote", "data": {"player": 1, "target": 5, "message": "镇定自若地指向5号"}} +{"type": "reasoning", "data": {"player": 3, "message": "情况|很|复杂|,|1号|说|我|是|好人|,|但|他|可能|是|狼人|在|混淆|视听|...|两|天|的|小|纸条|都|指向|了|狼人|,|我|应该|相信|这些|线索|。"}} +{"type": "vote", "data": {"player": 3, "target": 1, "message": "犹豫了一下,最终坚定地指向1号"}} +{"type": "reasoning", "data": {"player": 4, "message": "我|非常|确信|1号|是|最后|的|狼人|,|必须|投|他|出局|。"}} +{"type": "vote", "data": {"player": 4, "target": 1, "message": "毫不犹豫地指向1号"}} +{"type": "reasoning", "data": {"player": 5, "message": "1号|的|行为|很|可疑|,|我|应该|投|他|。"}} +{"type": "vote", "data": {"player": 5, "target": 1, "message": "战战兢兢地举起手,指向1号"}} +{"type": "voteEnd", "data": {"target": 1}} +{"type": "playerKilled", "data": {"targets": [1]}} +{"type": "gameOver", "data": {"result": "victory/defeat", "winningSide": "village", "reason": "好人阵营获胜,你失败了!"}} \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..fc77f77 --- /dev/null +++ b/index.ts @@ -0,0 +1,239 @@ +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); + }); + }); +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..619c475 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "backend-test", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "dependencies": { + "@types/jsdom": "^21.1.7", + "@types/xml2js": "^0.4.14", + "cors": "^2.8.5", + "express": "^4.18.2", + "jsdom": "^26.0.0", + "microsoft-cognitiveservices-speech-sdk": "^1.43.1", + "openai": "^4.92.0", + "readline": "^1.3.0", + "xml2js": "^0.6.2" + }, + "peerDependencies": { + "typescript": "^5", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..330c996 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["esnext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": false, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}