天空 Blond - AI
type: channel
link: https://skyblond.info/tag/AI/
items: 【歪门邪道】使用LLM翻译超时空辉夜姬轻小说
items: 【代码札记】基于Chrome内置游戏Dino的基因算法实现

【歪门邪道】使用LLM翻译超时空辉夜姬轻小说
type: item
date: 2026-04-01T09:59:00+08:00
description: 上个月月初看了超时空辉夜姬，非常喜欢，喜欢到每天都在b站上看各种解析和二创。听闻官方还有轻小说，于是便想办法来看。可是我一句日本话也看不懂，这怎么办呢？没事，已经2026年了，我们有LLM。前言也许是受到电影的鼓舞，我3月6号周五晚上看完的电影，紧接着那个周末就把小说给翻译了。以前我从未想过，一句日文都看不懂的我甚至还能以一种比较有参与感的方式翻译一本小说。尽管一开始我想到了LLM会让翻译工作轻松不少，但我确实没有期待什么质量。但真正做完了才知道，虽然远不比彩叶，但我也能做到我曾经没想过或者不敢想的事情。我可太喜欢这部电影了。买书所谓万事开头难，由于小说是日文的，并且目前只在日本发行，我又等不及了，所以搜了全网售卖日文电子书的网站。找到了，但全都是日文，我看不懂。最后我想起来乐天kobo，于是就跑到上面搜了一下。好消息：能买坏消息：日区是独立管理买之前网页上信誓旦旦地说提供Adobe DRM保护的EPUB，这种通常是提供一个acsm文件，然后内容服务器会根据你阅读器的密钥生成一个加密的EPUB，理论上只能在你导入acsm文件的阅读器上使用，因为只有它有密钥。但实际上Calibre的插件就可以轻而易举地破解——Adobe只提供加密服务，但并不控制最终设备，因此只要能够通过API模拟出设备的注册过程，那用我们自己持有的密钥解密自己购买的电子书，那不是轻而易举？只可惜我买之后才发现，整个kobo日区都不提供下载服务，要么直接从电子书里下载，要么下载app到手机上看。好在天无绝人之路，Kobo PC版还可以下载。虽然下载的是kobo加密的格式（Kepub），但Calibre照样有破解的插件。第一次解密发现怎么解出来全是乱码，我还以为新书用了新的加密方法，插件落后了。后来搜索了一番才只要，要把Kobo PC关掉才行。总之，我们得到了心心念念的drm-free epub文件。I have a plan!一开始我打算在OpenCode中操作。简单来说OpenCode是一个利用LLM进行编程的工具，它为LLM提供了一个环境，其中包含各种与文件和终端互动的工具。最近我司在推行这个工具，也被迫用了一段时间，感觉还行，但还是离不开人的审核。我原本打算直接让它把文件中的日语内容翻译成中文。后来觉得有三个问题：编程用的Opus 4.6或者codex 5.3都是为了编写代码而优化的，他们也许能在OpenCode中如鱼得水，但或许并不擅长翻译这类具有文学性的工作我希望大部分更改是reproducible的，也就是说我搞砸了，可以重新运行代码恢复到某一个步骤。由于LLM的随机性，我不可能做到完全的可重复，但能做一点是一点嘛Opus和Codex太贵了，虽然有公司给的key，但这毕竟是个人项目，不能用公司的资源。我尝试了GLM-5，整体算是堪用，写不出kotlin native的代码风格，更像是用kotlin写Java，整体不是很糟糕，但也没有让我很想用基于以上理由，我选择在IDEA中新建项目，将大部分操作编写成可重复运行的代码。这样不光实现了可重复性，还变相增加了稳定性。本文的代码全都是我自己手写的，但写完之后有让GLM-5检查。由于版权原因，本文尽可能不提供对小说内容的直接引用。因为我没有处理DMCA投诉的经验，所以多一事不如少一事，我希望我的博客能长存八千年。拆EPUB首先是拆解EPUB。众所周知，EPUB其实一个具有特殊格式的zip压缩包。理论上来说，想要解包的话直接把epub的后缀改成zip就可以用随便什么压缩工具解压了。但我还是想用代码处理：fun unpackEpub(epubFile: File, outputFolder: File) { require(epubFile.exists()) { "EPUB file not found: ${epubFile.path}" } if (!outputFolder.exists()) { require(outputFolder.mkdirs()) { "Cannot create output folder: ${outputFolder.path}" } } ZipInputStream(epubFile.inputStream()).use { zis -> var entry = zis.nextEntry while (entry != null) { val newFile = File(outputFolder, entry.name) // zip slip detection, to prevent entry name contains something like ../../../etc/something val canonicalDestPath = newFile.…

【代码札记】基于Chrome内置游戏Dino的基因算法实现
type: item
date: 2019-05-18T17:53:00+08:00
description: 本项目受启发于Code Bullet的Youtube视频「AI学习如何玩Chrome内置的游戏Dino || 你能打败它吗？？」（代码参见视频下方简介）。本项目Github地址：https://github.com/hurui200320/Genetic-Algorithm-with-Chrome-dino 。简介最近刷Youtube看到了Code Bullet的一系列关于AI学习某一动作的视频，了解到了Genetic algorithm，我感觉它的翻译方式有很多种，我就按照字面意思取了，无伤大雅。关于基因算法，Code Bullet还做过教学视频(Youtube)，其第二期还使用了一个实例展现对应的代码该如何编写。我觉得这个视频挺简单易懂的，感兴趣的各位欢迎移步去看看。这个项目是看到了如开头说的视频而受到了启发。视频中的代码我也下载下来看过了，感觉有些地方比较复杂（我太菜了没能理解他那样实现的目的），因此我在这些地方做出了一些简化（就是单纯的懒以及玄学上的感觉这样的简化是可行的）。实现游戏我按照Chrome原版的Dino（Chrome用户可以通过访问 chrome://dino 来尝试游戏。按空格可以开始游戏以及操作游戏角色跳跃，方向键下键可以控制游戏角色下蹲，游戏的目标是躲避障碍物）使用Java8和Processing3复刻了一个（我感觉）差不多的游戏，只是游戏比较简陋，截图如下：我参照Code Bullet的实现选定了一些参数：例如角色的高度、宽度，角色在下蹲时的宽度、高度，以及各类障碍物的尺寸及位置。其余参数则是通过经验和多次尝试得到的。这些参数并不意味着完美，只是我觉得差不多了。物理系统我这里用的是高中物理，不考虑空气阻力，重力加速度在各高度上保持不变。每一帧代表的时间间隔由Game类的deltaT决定，地面和障碍物的行进速度一致，由基速（Game类的groundBaseSpeed字段）和补正（Game类的speedFix）相加得到。每一帧行进的距离由前述二者相乘得到。速度补正在每一帧结束后增加0.002，上限是1.3倍基速。游戏角色的跳跃是通过赋予初速度实现的，大小跳跃分别赋予角色不同的初速度即可。两个初速度在游戏过程中保持不变。游戏机制游戏角色游戏角色直接使用黑色方块代替，我懒得整贴图了。游戏角色由Dino类定义，每个Dino对象可以通过调用bigJump()、smallJump()和setDown(status)（status为布尔型，true表示下蹲，false表示不下蹲）三个函数来操作其动作。Dino对象的move()方法将在游戏的每一帧被调用以使角色的位置被更新。障碍物障碍物分两种：仙人掌（Obstacle类）和鸟（Bird类）。这里我使用红色方块代替，原因同前述。其move()方法将被游戏的每一帧被调用，用以更新障碍物的位置。而collided(dino)方法会在被调用时判断该障碍物是否与给定的dino碰撞，若碰撞则返回true，否则返回false。判断碰撞的算法借鉴了Code Bullet的算法。基因算法玩家玩家由Player类定义，每一个玩家持有一个Dino类，代表他们所操作的游戏角色。同时还有一个Brain类对象，当玩家需要决策时，其think(obstacles)方法被调用，brain对象将根据当前的障碍物做出计算，随后玩家依据此进行操作。每次玩家位置更新后将对玩家与障碍物判断是否碰撞，若碰撞则判定当前玩家的游戏结束，后面不再计算该玩家的更新。神经网络（Brain类）神经网络是含有一个隐藏层的MLP（多层感知器）。10个输入（第一个要遇到的障碍物的横纵坐标位置、宽高；游戏速度；游戏角色的高度；第二个要遇到的障碍物得横纵坐标、宽高） + 1个偏置，3个输出（是否跳跃；大跳或小跳；是否下蹲），隐藏层有五个神经元（无偏置）。所有神经元（Node类）的激活函数都是Sigmoid。在第一世代时需要通过调用randomWeight()方法随机化权重，其中调用的nextDouble()可以提供$\pm10$以内的双精度浮点随机数。需要决策时，player先调用brain.lookAround(obstacles, dino)方法，获取玩家与屏幕中障碍物的状态并作为神经网络的输入，随后调用brain.computeAction()计算当前的操作。神经网络的输出以大于0.8为真，小于0.8为假，返回对应输出的布尔型以供player做出操作决策。世代迭代当所有玩家都判定为游戏结束时（game.isGameOver()返回true），该世代结束。程序先调用game.naturalSelection()进行自然选择，产生下一代。程序将首先找到分数最高的玩家并计算所有玩家的分数总和。其中最高分的玩家直接克隆至下一世代，各…