first commit
This commit is contained in:
commit
1cbfe2005e
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
@ -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
|
||||||
15
README.md
Normal file
15
README.md
Normal file
@ -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.
|
||||||
58
events.jsonl
Normal file
58
events.jsonl
Normal file
@ -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": "<audio style=\"serious\">平安夜|可能|是|狼人|战略性|地|选择|了|观望|,|也|可能|是|女巫|救|了|人|。</audio><audio style=\"calm\">如果|是|后者|,|那么|女巫|的|解药|已经|用|掉|了|,|这|对|我们|接下来|的|夜晚|是|个|不利|因素|。</audio><audio style=\"newscast\">不过|现在|讨论|谁|是|狼人|还|为时过早|,|我们|需要|更多|信息|。</audio>"}}
|
||||||
|
{"type": "speak", "data": {"player": 2, "message": "<action>咧嘴一笑,向后靠在椅子上</action><audio style=\"humorous\">哇|,|平安夜|!|狼人|是|不是|昨晚|偷懒|了|?</audio><action>挑眉看向四周</action><audio style=\"sarcastic\">或者|有人|运气|好|,|被|女巫|大人|青睐|了|呢|?</audio><action>耸耸肩</action><audio style=\"casual\">不管|怎样|,|我|觉得|1号|分析|得|挺|全面|的|,|不过|平安夜|确实|让|我们|少|了|很多|线索|,|只能|靠|观察|发言|来|推测|了|。</audio>"}}
|
||||||
|
{"type": "speak", "data": {"player": 3, "message": "<action>身体微微前倾,表情谨慎</action><audio style=\"calm\">平安夜|确实|让|我们|少|了|一些|信息|,|但|也|给|了|我们|思考|的|空间|。</audio><action>若有所思地看着其他人</action><audio style=\"narration-professional\">我|认为|我们|应该|考虑|两种|可能性|:|如果|狼人|没有|行动|,|说明|他们|比较|谨慎|;|如果|有人|被|救|了|,|说明|狼人|已经|暴露|了|杀人|意图|。</audio><action>轻声补充</action><audio style=\"serious\">无论|是|哪种|情况|,|我们|都|应该|仔细|观察|每个|人|的|发言|逻辑|,|这|往往|能|揭示|更多|信息|。</audio>"}}
|
||||||
|
{"type": "speak", "data": {"player": 4, "message": "<action>直视前方,表情平静</action><audio style=\"newscast\">我|认为|狼人|昨晚|行动|了|,|只是|被|女巫|破坏|了|计划|。</audio><action>环顾四周</action><audio style=\"disgruntled\">第一晚|不|杀人|对|狼人|没有|好处|,|他们|需要|尽快|减少|村民|数量|。</audio><action>略微点头</action><audio style=\"newscast\">所以|我们|可以|假设|女巫|已经|用|掉|解药|了|,|这点|在|后面|的|讨论|中|很|重要|。</audio>"}}
|
||||||
|
{"type": "speak", "data": {"player": 5, "message": "<action>双手轻放桌面,表情略显犹豫</action><audio style=\"fearful\">4号|的|推理|很|有|道理|...|狼人|第一晚|通常|会|行动|。</audio><action>目光短暂停留在2号身上</action><audio style=\"whispering\">我|觉得|1号|和|6号|的|表现|可能|值得|关注|,|他们|似乎|都|很|克制|,|不像|是|急于|表达|观点|的|样子|。</audio><action>轻声补充</action><audio style=\"sad\">不过|这|只是|我|的|初步|观察|,|我们|还|需要|更多|线索|。</audio>"}}
|
||||||
|
{"type": "speak", "data": {"player": 6, "message": "<action>坐姿端正,表情平静</action><audio style=\"friendly\">我|觉得|大家|都|提出|了|很|好|的|观点|。</audio><action>微微向前倾身</action><audio style=\"gentle\">不过|我|想|指出|,|5号|似乎|很|快|就|把|注意力|引向|了|我|和|1号|,|这种|没有|实质|证据|的|怀疑|让|我|有些|困惑|。</audio><action>环顾四周</action><audio style=\"newscast\">我|认为|3号|的|分析|更为|客观|,|我们|确实|应该|关注|发言|逻辑|而|不是|主观|印象|。|比如|4号|的|发言|简短|但|逻辑|严密|,|而|5号|的|发言|则|更多|基于|感觉|。</audio>"}}
|
||||||
|
{"type": "noteAppear", "data": {"message": "一张神秘的小纸条突然从天而降,飘到了桌子中央...3号玩家捡到了这张纸条。"}}
|
||||||
|
{"type": "speak", "data": {"player": 3, "message": "<action>表情略显困惑</action><audio style=\"calm\">我|刚刚|看到|了|一张|纸条|,|上面|写着|'月夜|幽灵|双|影|现|,|一|静|一|动|皆|可疑|'|。</audio><action>环视众人</action><audio style=\"narration-professional\">这|似乎|在|暗示|两个|人|可能|有|问题|,|一个|行事|沉稳|,|一个|比较|活跃|。</audio><action>轻轻摇头</action><audio style=\"serious\">当然|,|这种|提示|相当|模糊|,|可以|有|多种|解读|。|我们|不能|完全|依赖|它|,|但|可以|作为|参考|。</audio>"}}
|
||||||
|
{"type": "speak", "data": {"player": 2, "message": "<action>挑眉,表情困惑</action><audio style=\"humorous\">等等|,|这|很|有趣|。</audio><action>高举纸条</action><audio style=\"humorous\">这张|纸条|上|写着|'三五|密语|暗|相通|,|真伪|难辨|需|警惕|'|,|但|3号|刚才|说|的|是|'月夜|幽灵|双|影|现|,|一|静|一|动|皆|可疑|'|。</audio><action>环顾四周,眼神锐利</action><audio style=\"sarcastic\">看来|这张|纸条|在|途中|变|了|内容|啊|。|6号|,|作为|中间|的|传递者|,|你|能|解释|一下|吗|?</audio>"}}
|
||||||
|
{"type": "speak", "data": {"player": 6, "message": "<action>表情略显惊讶,随后恢复冷静</action><audio style=\"embarrassed\">我|承认|我|确实|修改|了|纸条|内容|。</audio><action>环视众人</action><audio style=\"friendly\">但|我|这么|做|是|为了|测试|大家|的|反应|。|注意|到|3号|拿到|纸条|后|,|立即|提到|'一|静|一|动|皆|可疑|'|,|这|很|容易|被|解读|为|针对|某些|人|的|暗示|。</audio><action>看向5号</action><audio style=\"gentle\">而且|5号|之前|也|无端|指责|我|和|1号|,|我|怀疑|3号|和|5号|可能|在|合作|引导|舆论|。|我|修改|的|内容|恰恰|点出|了|这|一点|:|'三五|密语|暗|相通|'|。</audio>"}}
|
||||||
|
{"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": "<action>表情严肃,不再嬉笑</action><audio style=\"serious\">3号|被|杀|,|这|很|可能|不|是|巧合|。</audio><action>环视众人</action><audio style=\"serious\">如果|6号|是|狼人|,|那么|1号|很|可能|是|他|的|同伴|。|昨天|6号|修改|纸条|的|行为|已经|很|可疑|,|而且|1号|和|6号|的|投票|也|完全|一致|。</audio><action>看向1号</action><audio style=\"disgruntled\">1号|,|你|有|什么|要|说|的|吗|?</audio>"}}
|
||||||
|
{"type": "turnToSpeak", "data": {"target": 1}}
|
||||||
|
{"type": "speak", "data": {"player": 1, "message": "<audio style=\"serious\">我|理解|大家|的|怀疑|,|但|请|听|我|解释|。</audio><audio style=\"newscast\">事实上|,|我|是|预言家|。|昨晚|我|查验|了|5号|,|结果|是|狼人|。</audio><audio style=\"calm\">你们|投|出|6号|是|正确|的|,|但|别|忘|了|那张|被|篡改|的|纸条|上|写|的|是|'三五|密语|暗|相通|'|。|3号|被|杀|并|不能|证明|他|是|好人|,|也|可能|是|狼人|之间|的|内讧|。</audio>"}}
|
||||||
|
{"type": "speak", "data": {"player": 5, "message": "<action>表情先是震惊,然后坚定</action><audio style=\"fearful\">1号|,|你|的|预言家|身份|宣称|来|得|太|突然|了|。</audio><action>环顾四周</action><audio style=\"sad\">如果|你|真|的|是|预言家|,|为什么|不|早点|站|出来|?|而|是|等到|被|怀疑|时|才|突然|跳|预言家|?</audio><action>看向2号</action><audio style=\"whispering\">而且|,|我|想|分享|一个|重要|信息|:|第一晚|是|我|救|了|2号|。|这|意味着|2号|是|被|狼人|袭击|的|目标|,|所以|他|一定|是|好人|。|我们|现在|至少|可以|确定|2号|的|身份|是|清白|的|。</audio>"}}
|
||||||
|
{"type": "speak", "data": {"player": 4, "message": "<action>表情严肃,语气坚定</action><audio style=\"newscast\">1号|的|预言家|身份|声明|不|可信|。</audio><action>直视1号</action><audio style=\"disgruntled\">跳|预言家|的|时机|太|过|刻意|,|明显|是|狼人|的|自救|手段|。</audio><action>看向5号</action><audio style=\"newscast\">5号|自称|是|女巫|并|说|她|救|了|2号|,|这个|信息|很|关键|。|如果|属实|,|那么|2号|确实|是|好人|,|而|1号|作为|最后|的|狼人|在|尝试|转移|视线|。</audio>"}}
|
||||||
|
{"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": "<action>皱眉,表情怀疑</action><audio style=\"disgruntled\">这张|纸条|写着|'五人|药水|有|玄机|,|解毒|两瓶|皆|在|握|'|。</audio><action>看向5号</action><audio style=\"newscast\">这|似乎|在|暗示|5号|拥有|两瓶|解药|,|但|我们|都|知道|女巫|只有|一瓶|解药|和|一瓶|毒药|。</audio><action>转向1号</action><audio style=\"disgruntled\">1号|,|我|怀疑|你|篡改|了|纸条|内容|,|就|像|6号|曾经|做|的|那样|。|这|再次|证明|你|不|可信|。</audio>"}}
|
||||||
|
{"type": "turnToSpeak", "data": {"target": 1}}
|
||||||
|
{"type": "speak", "data": {"player": 1, "message": "<audio style=\"serious\">我|没有|篡改|任何|纸条|!|这张|纸条|恰恰|证明|了|5号|在|说谎|。</audio><audio style=\"calm\">女巫|只有|一瓶|解药|,|但|纸条|暗示|5号|声称|拥有|两瓶|。|这|证明|她|不|是|真正|的|女巫|,|而|是|在|试图|建立|可信度|。</audio><audio style=\"newscast\">我|作为|预言家|已经|验证|了|她|是|狼人|,|现在|这张|纸条|更加|证实|了|这|一点|。</audio>"}}
|
||||||
|
{"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": "好人阵营获胜,你失败了!"}}
|
||||||
92
events_0.jsonl
Normal file
92
events_0.jsonl
Normal file
@ -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": "<action>双手交叉放在桌上,微微前倾</action><audio style=\"calm\">女巫|第一晚|就|用了|解药|,|说明|女巫|很|谨慎|。|女巫|能|看到|被|刀|的|人|是|谁|,|这|对|我们|很|有利|。|不过|这|也|意味着|女巫|的|解药|已经|用完了|。</audio><action>环顾四周</action><audio style=\"friendly\">大家|有|什么|线索|可以|分享|吗|?</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 2, "message": "我|需要|制造|信息|干扰|,|拖延|时间|,|同时|试探|预言家|是否|会|跳出来|。|表现|得|激动|一些|,|掩盖|我|是|狼人|的|事实|。"}}
|
||||||
|
{"type": "speak", "data": {"player": 2, "message": "<action>双手夸张地挥舞</action><audio style=\"excited\">哇|!|第一晚|就|用|解药|,|这|太|紧张|了|!|我|对|女巫|的|决定|表示|敬佩|,|但|也|有点|担心|,|因为|现在|解药|已经|没了|。</audio><action>身体前倾,目光扫视每个人</action><audio style=\"shouting\">我|想|知道|预言家|有|没有|什么|发现|?|如果|查到|了|狼人|,|请|一定|要|说出来|!</audio><action>做出思考状</action><audio style=\"cheerful\">不过|也|可能|预言家|现在|不想|暴露|自己|,|这|也|可以|理解|。|大家|觉得|谁|最|可疑|?|我|个人|觉得|游戏|刚|开始|,|发言|比较|少|,|很|难|判断|...</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 3, "message": "伪装|成|新手|可以|降低|狼人|对|我|的|警惕性|,|同时|通过|提问|来|观察|其他|玩家|的|反应|,|特别|是|关于|被|救者|身份|的|问题|,|狼人|可能|会|有|特别|的|反应|。"}}
|
||||||
|
{"type": "speak", "data": {"player": 3, "message": "<action>挠头,表情困惑</action><audio style=\"embarrassed\">我|有|个|问题|...|既然|有人|被|救|了|,|那|被|救|的|人|知道|自己|被|救|了|吗|?</audio><action>眼睛微微睁大,恍然大悟</action><audio style=\"gentle\">他|能|不能|告诉|我们|他|是|谁|?|这样|我们|就|能|知道|谁|是|好人|了|,|对|吧|?</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 4, "message": "仔细|分析|前面|玩家|的|发言|,|尤其|是|1号|和|2号|,|他们|的|发言|模式|很|可疑|。"}}
|
||||||
|
{"type": "speak", "data": {"player": 4, "message": "<action>皱眉,目光锐利</action><audio style=\"serious\">我们|不|知道|谁|被|刀|了|,|被|救|的|人|也|不会|收到|通知|。</audio><action>指向2号</action><audio style=\"angry\">不过|我|对|2号|的|长篇大论|有些|怀疑|,|说|了|那么|多|但|没有|实质|内容|,|这|是|狼人|常用|的|话术|。</audio><action>转向1号</action><audio style=\"disgruntled\">还有|,|1号|的|发言|有意|引导|我们|关注|女巫|,|似乎|想|转移|视线|。</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 5, "message": "作为|女巫|,|我|已经|用了|解药|,|剩下|毒药|需要|谨慎|使用|。|现在|不要|暴露|自己|,|先|同意|4号|对|2号|的|怀疑|,|但|语气|保持|犹豫|。"}}
|
||||||
|
{"type": "speak", "data": {"player": 5, "message": "<action>身体微微后缩,目光游移</action><audio style=\"whispering\">我|...|我|觉得|现在|下|结论|还|太|早|。</audio><action>小心翼翼地看了看4号</action><audio style=\"fearful\">不过|4号|的|分析|有点|道理|,|2号|确实|话|很|多|但|没|说|什么|有用|的|,|预言家|给|一些|线索|吧|。</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 6, "message": "我|已经|确认|2号|是|狼人|,|考虑到|女巫|已经|用了|解药|,|如果|今晚|我|被|杀|就|没有|人|能|救|我|了|。|现在|是|跳出来|的|最佳|时机|,|必须|尽快|告诉|大家|我|的|发现|。"}}
|
||||||
|
{"type": "speak", "data": {"player": 6, "message": "<action>坐直身体,表情严肃</action><audio style=\"newscast\">我|是|预言家|。|昨晚|我|查|了|2号|,|是|狼人|。</audio><action>目光坚定地扫视全场</action><audio style=\"documentary-narration\">我|知道|这样|跳出来|很|危险|,|但|第一晚|有|人|被|救|,|说明|女巫|已经|用了|解药|,|如果|今晚|我|被|杀|就|没人|能|救|了|,|所以|我|出来|说|一下|。</audio>"}}
|
||||||
|
{"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": "<action>举起纸条,表情困惑</action><audio style=\"disgruntled\">等等|,|在|我们|投票|前|,|我|刚刚|捡到|一张|纸条|。</audio><action>念出内容</action><audio style=\"serious\">上面|写着|'四人|独处|时|常|皱眉|,|似|有|心事|难言|'|。|不|知道|这|是|什么|意思|,|但|似乎|有人|想|暗示|我|有|问题|?</audio><action>眼神锐利地扫视全场</action><audio style=\"angry\">我|觉得|这|更|像|是|狼人|的|陷阱|。</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 3, "message": "4号|的|困惑|是|真实|的|,|这|是|我|的|机会|,|可以|继续|扮演|新手|角色|。"}}
|
||||||
|
{"type": "speak", "data": {"player": 3, "message": "<action>尴尬地举手</action><audio style=\"embarrassed\">哎呀|,|抱歉|,|那|是|我|刚才|掉|的|。</audio><action>低头看着自己的手指</action><audio style=\"chat\">我|...|其实|我|收到|了|另|一张|纸条|,|但|上面|的|内容|我|不|确定|是|什么|意思|,|所以|我|就|...|改|了|一下|。</audio><action>抬头,天真</action><audio style=\"gentle\">原来|的|纸条|是|关于|2号|的|,|说|什么|'二人|饭后|常|散步|,|月下|窃窃私语|甚是|亲密|'|。|我|觉得|可能|是|在|暗示|2号|和|谁|是|一伙|的|?</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 2, "message": "小|纸条|暴露|了|信息|,|需要|立即|反击|混淆|局面|。"}}
|
||||||
|
{"type": "speak", "data": {"player": 2, "message": "<action>猛地站起来,表情夸张</action><audio style=\"shouting\">什么|?|这|太|荒谬|了|!</audio><action>指着3号</action><audio style=\"angry\">3号|你|为什么|要|篡改|纸条|内容|?|你|是|想|陷害|4号|吗|?|这种|行为|本身|就|很|可疑|。</audio><action>双手叉腰</action><audio style=\"excited\">而且|你|说|的|原始|内容|明显|是|在|暗示|我|是|狼人|,|配合|6号|的|预言家|身份|,|这|很|可能|是|好人|阵营|的|联合|策略|!</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 1, "message": "必须|乘胜追击|,|引导|大家|怀疑|3号|和|6号|的|联合|。"}}
|
||||||
|
{"type": "speak", "data": {"player": 1, "message": "<action>保持冷静,但语气坚定</action><audio style=\"serious\">我|同意|2号|的|看法|。</audio><action>轻轻指向3号</action><audio style=\"calm\">3号|的|行为|非常|可疑|,|篡改|纸条|是|在|误导|大家|。</audio><action>转向6号,眉头微皱</action><audio style=\"friendly\">再|联系|6号|刚好|查验|了|2号|是|狼人|,|会|不会|这|是|你们|预先|串通|好|的|?</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 6, "message": "狼人|的|联合|反击|很|强势|,|但|小|纸条|内容|恰恰|佐证|了|我|的|预言|结果|,|这|是|个|机会|。"}}
|
||||||
|
{"type": "speak", "data": {"player": 6, "message": "<action>双手平放在桌上,表情镇定</action><audio style=\"calm\">各位|冷静|。</audio><action>用专业的语气分析</action><audio style=\"newscast\">小|纸条|的|内容|确实|与|我|的|预言|结果|一致|,|这|不|是|巧合|。|3号|篡改|纸条|的|行为|虽然|不当|,|但|他|最终|还是|说|出|了|真相|。</audio><action>目光坚定扫视全场</action><audio style=\"documentary-narration\">我们|现在|应该|集中|精力|投|出|2号|这个|确认|的|狼人|,|不要|被|狼人|的|混淆|视听|所|迷惑|。</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 5, "message": "现在|情况|更加|复杂|了|,|但|预言家|和|小|纸条|都|指向|2号|是|狼人|,|这个|信息|很|可能|是|可靠|的|。"}}
|
||||||
|
{"type": "speak", "data": {"player": 5, "message": "<action>身体微微颤抖,声音轻柔</action><audio style=\"whispering\">我|...|我|觉得|6号|说|得|有|道理|。</audio><action>偷瞄了一眼2号又迅速移开视线</action><audio style=\"fearful\">无论|3号|为什么|要|篡改|纸条|,|最|重要|的|是|我们|现在|有|两个|线索|都|指向|2号|有|问题|。</audio>"}}
|
||||||
|
{"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": "<action>声音轻柔,有些颤抖</action><audio style=\"whispering\">我|...|我|捡到|了|一张|纸条|。</audio><action>缓缓展示纸条</action><audio style=\"fearful\">上面|写着|'一人|故作|镇定|,|夜间|却|形迹|可疑|'|。</audio><action>偷偷瞄了一眼1号</action><audio style=\"sad\">我|觉得|这|可能|在|暗示|1号|,|因为|他|总是|表现|得|很|冷静|...</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 1, "message": "预言家|被|杀|,|是|我|自救|的|最佳|时机|。|我|应该|马上|跳|预言家|,|声称|自己|查到|了|3号|是|好人|,|然后|把|怀疑|引向|4号|和|5号|。|同时|我|需要|对抗|小|纸条|带来|的|怀疑|。"}}
|
||||||
|
{"type": "speak", "data": {"player": 1, "message": "<action>从容不迫地站起身,轻轻整理衣领</action><audio style=\"newscast\">其实|我|才|是|预言家|,|但|昨天|没有|立即|跳|是|因为|担心|被|狼人|刀|。</audio><action>竖起一根手指</action><audio style=\"calm\">昨晚|我|查|了|3号|,|是|好人|。|昨天|6号|说|2号|是|狼人|,|结果|证明|他|是|对|的|。</audio><action>目光锐利地扫向4号和5号</action><audio style=\"serious\">所以|剩下|的|狼人|应该|在|4号|和|5号|之间|。</audio><action>嘲讽地扬起纸条</action><audio style=\"disgruntled\">至于|那张|小|纸条|,|明显|是|某人|想要|陷害|我|,|就|像|昨天|3号|试图|篡改|纸条|一样|,|这些|小|纸条|根本|不|可信|!</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 3, "message": "1号|的|行为|非常|可疑|,|真|预言家|已经|跳|了|,|他|为什么|第一天|不|跳|?|很|可能|是|狼人|在|自救|。|加上|小|纸条|的|暗示|,|我|更加|确定|他|有|问题|。"}}
|
||||||
|
{"type": "speak", "data": {"player": 3, "message": "<action>挠头,表情从困惑转为怀疑</action><audio style=\"chat\">等等|,|你|说|你|是|预言家|?</audio><action>歪着头,装作天真</action><audio style=\"embarrassed\">那|为什么|第一天|不|跳|呢|?</audio><action>眼睛忽然变得锐利</action><audio style=\"gentle\">虽然|你|给|我|发|了|金水|,|但|我|对|你|的|身份|依然|存疑|。|两|天|的|小|纸条|指向|都|很|清晰|:|2号|和|1号|是|狼人|搭档|。</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 4, "message": "1号|明显|是|在|伪装|预言家|,|试图|混淆|视听|。|真|预言家|已经|确认|了|2号|是|狼人|,|而|1号|现在|突然|跳|预言家|,|这|是|狼人|惯用|的|自救|手段|。|小|纸条|再次|证实|了|我|的|判断|。"}}
|
||||||
|
{"type": "speak", "data": {"player": 4, "message": "<action>拍桌而起,眼神凌厉</action><audio style=\"angry\">1号|突然|跳|预言家|很|可疑|!</audio><action>用手指着1号</action><audio style=\"disgruntled\">真|预言家|已经|确认|了|2号|是|狼人|,|而|1号|昨天|的|表现|就|很|奇怪|,|现在|又|在|死亡|的|预言家|之后|跳|预言家|。</audio><action>冷笑一声</action><audio style=\"serious\">这|明显|是|狼人|想要|混淆|视听|。|再|加上|今天|的|小|纸条|,|我|认为|1号|就|是|最后|一|只|狼|。</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 5, "message": "局势|变得|更加|复杂|了|,|但|1号|的|行为|确实|很|可疑|,|我|同意|4号|的|看法|,|但|还是|保持|一定|的|谨慎|态度|。|考虑到|我|拿到|了|小|纸条|,|应该|更|坚定|一些|。"}}
|
||||||
|
{"type": "speak", "data": {"player": 5, "message": "<action>小心地向前倾身,声音比平时稍大</action><audio style=\"fearful\">我|...|我|也|觉得|1号|很|可疑|。</audio><action>举起手中的纸条</action><audio style=\"whispering\">小|纸条|的|描述|和|1号|的|行为|太|符合|了|。</audio><action>鼓起勇气直视1号</action><audio style=\"sad\">真|预言家|已经|确认|了|2号|是|狼人|,|而|1号|现在|才|跳|预言家|,|这|很|不|合理|。</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 1, "message": "我|必须|转移|怀疑|,|5号|的|发言|提到|了|小|纸条|,|是|引起|争议|的|关键|。|必须|把|票|投|给|她|,|同时|争取|3号|的|信任|。"}}
|
||||||
|
{"type": "vote", "data": {"player": 1, "target": 5, "message": "<action>镇定自若地指向5号</action><audio style=\"calm\">我|投|5号|。|她|明显|在|利用|小|纸条|操控|局势|,|这|是|狼人|的|惯用|伎俩|。</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 3, "message": "情况|很|复杂|,|1号|说|我|是|好人|,|但|他|可能|是|狼人|在|混淆|视听|...|两|天|的|小|纸条|都|指向|了|狼人|,|我|应该|相信|这些|线索|。"}}
|
||||||
|
{"type": "vote", "data": {"player": 3, "target": 1, "message": "<action>犹豫了一下,最终坚定地指向1号</action><audio style=\"gentle\">我|投|1号|。|这|两|天|的|线索|都|指向|他|和|2号|是|狼人|搭档|。</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 4, "message": "我|非常|确信|1号|是|最后|的|狼人|,|必须|投|他|出局|。"}}
|
||||||
|
{"type": "vote", "data": {"player": 4, "target": 1, "message": "<action>毫不犹豫地指向1号</action><audio style=\"angry\">我|投|1号|!|狼人|的|把戏|已经|被|识破|了|。</audio>"}}
|
||||||
|
{"type": "reasoning", "data": {"player": 5, "message": "1号|的|行为|很|可疑|,|我|应该|投|他|。"}}
|
||||||
|
{"type": "vote", "data": {"player": 5, "target": 1, "message": "<action>战战兢兢地举起手,指向1号</action><audio style=\"whispering\">我|...|我|投|1号|。|小|纸条|不会|说谎|。</audio>"}}
|
||||||
|
{"type": "voteEnd", "data": {"target": 1}}
|
||||||
|
{"type": "playerKilled", "data": {"targets": [1]}}
|
||||||
|
{"type": "gameOver", "data": {"result": "victory/defeat", "winningSide": "village", "reason": "好人阵营获胜,你失败了!"}}
|
||||||
239
index.ts
Normal file
239
index.ts
Normal file
@ -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<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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
25
package.json
Normal file
25
package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user