Tag: 日记

2 Posts

折腾无名杀:为了玩上“武陆逊”顺便用 GitHub Pages 绕过服务器带宽瓶颈
最近搞了个特别有情怀的开发小尝试:做一个三国杀里“武陆逊”的单武将对战模拟器。 熟玩三国杀的玩家应该知道,十周年的“武陆逊”这个武将强度极高、机制设计得很有趣,台词也特别带感。可惜官方定价实在是高攀不起,平时手痒想玩,还得跑去租号平台租号,非常不爽。 这让我突然回想起了读初高中那会儿经常用手机玩的开源三国杀项目——**无名杀(No Name)**。这是一个纯前端(HTML5)写成的开源项目,承载了许多三国杀玩家的折腾回忆。既然它是开源的,那我能不能把它精简一下,自己复刻一个专门拿来爽玩武陆逊的单机模拟器呢? 说干就干,不过实际折腾起来才发现这玩意儿的体量远比想象中要“重”: 1. **基础逻辑剪不断理还乱**:我本想通过 Agent 去裁减不必要的功能,只保留武陆逊对战最核心的代码。但发现无名杀作为一个迭代多年的系统,卡牌判定逻辑、人机对战 AI、底层 UI 框架早已经深度耦合。最后把乱七八糟的武将包和花哨功能砍完,整个静态项目依然有 100 多 MB(塞满了素材和音效),运行起来还要吃浏览器七八百兆的内存,简直跟个正规游戏没差。 2. **带宽成为致命瓶颈**:100 多 MB 的静态项目,如果直接丢在我这台带宽只有 3M 的腾讯云轻量服务器上,光是加载首屏资源就能把人急死。 为了解决这 3M 带宽的“小水管”问题,我开始尝试各种白嫖 CDN 加速的偏门方案: * **首案失败(jsDelivr)**:一开始我尝试用开源的 jsDelivr 做静态资源的中转代理,搭一个免费的“Fake CDN”。但实际折腾下来发现,这玩意儿在某些网络环境下的解析和加载速度并不理想,经常断流。 * **终极方案(GitHub Pages)**:既然无名杀是纯前端项目,压根不需要任何后端逻辑,那我为什么不直接嫖 GitHub 官方提供的免费 CDN 服务呢?于是我干脆把打包好的静态代码库推到了 GitHub 上,直接用 **GitHub Pages** 托管。 这个改动效果显著!虽然因为首次需要下载大量的图片和音效包,开局加载依然需要缓冲一会儿,但 GitHub Pages 的全球 CDN 链路非常稳定,且自带浏览器强缓存机制。只要第一次加载完毕,后面再开局就能瞬间秒开。 用着自己亲手复刻出来的“无名杀·武陆逊模拟器”,脑海里一下就闪回了初高中在课间偷偷折腾手机包的时光。虽然现在能写更复杂的代码、能部署更精密的系统,但像这样为了一个喜欢的武将而去阅读开源代码、动手解决静态资源托管问题的小折腾,依然能带给我最纯粹的快乐。
把 Gemini Live 实时语音接进个人网站,以及顺手解决的音字同步问题
最近把 Google 的 Gemini Multimodal Live API 接到了个人网站上。 本来是冲着 Gemini 那个能实时语音、甚至能实时传视频流的酷炫效果去的,跟它官方 App 的体验差不多。不过做着做着就发现,理想很丰满,现实的带宽和安全墙很骨感: 1. **安全第一,Key 不能裸奔**:如果完全在前端调用它的 WebSocket,那 API Key 就不得不暴露在浏览器里,这肯定不能忍。再加上国内服务器访问 Google 接口得挂一层代理,所以必须在后端起个网关做一层 WebSocket 转发。 2. **带宽刺客**:如果要开摄像头实时传视频,视频帧需要以极高的频率进行数据传输(要么走 WebRTC,要么在 WebSocket 里狂塞图片)。而我这腾讯云轻量服务器只有区区 3M 带宽,一旦转发视频,服务器的带宽瞬间就会被吃满,卡成 PPT。所以最后我索性砍掉了视频传输,只做纯语音和文字的实时对话。 虽然没有摄像头,但做出来的普通对话体验依然很赞,而且在折腾的过程中,顺手解决了一个很经典的问题——**字幕与声音的同步**。 以前做这种语音对话,大模型的文本流(Transcription)和音频流(Audio)通常是异步发过来的。如果是文本先跑出来,声音还在慢吞吞地读,体验就非常诡异,像是在看“剧透字幕”;如果自己用复杂的定时器或者每一帧去算字数 pacing,又容易因为网络抖动产生累计误差。 后面折腾了半天,让 AI 帮我一起研究,发现 Gemini 返回的字段结构其实很丰富。我就想了一个比较取巧且优雅的方案:**音字分组机制**。 在 Node.js 中转层里,我定义了一个 `group` 缓冲区。一旦接收到上游发来的文本片段,就标志着一个新分组的开始,同时把在这之后到达的所有音频切片(Audio Chunks)通通塞进这个分组里,并把它们作为一个整体的 `chunk_group` 包打包发给前端。 在前端,播放逻辑的核心思路是: * 依次把分组内的音频 buffer 调度到 Web Audio API 的时间线上连续播放。 * 重点来了:**把这组文本的显示时机,直接绑定到该组最后一个音频切片的 `ended` 事件上**。 这样一来,前端根本不需要用任何复杂的时间轴计算或定时器,字幕会在声音刚好播完的瞬间自然跳出来,声音和文字完美咬合,绝对不会出现“字比音快”或者累计漂移的情况。 前端的核心逻辑大概长这样: ```javascript // 前端音频播放与字幕同步的核心逻辑 let groupDuration = 0; let lastSrc = null; for (let i = 0; i < buffers.length; i++) { const buf = buffers[i]; const src = audioCtx.createBufferSource(); src.buffer = buf; src.connect(audioCtx.destination); // 在时间线上精准排队播放 const startAt = Math.max(nextTime, audioCtx.currentTime); src.start(startAt); nextTime = startAt + buf.duration; groupDuration += buf.duration; lastSrc = src; } // 只有当这一组的最后一个音频切片播放完毕,才把对应的字幕文本渲染到屏幕上 if (group.text && lastSrc) { const textToReveal = group.text; lastSrc.addEventListener('ended', () => { revealSubtitle(textToReveal); }); } ``` 现在这个实时聊天小工具已经在后台跑起来了。虽然只是个没有视频的“阉割版”,但实时对话的响应速度和这个音字同步的字幕效果,用起来已经相当令人满意了。