参赛项目名称
Lyra · 在听歌的过程中学习一门语言
团队 / 作者
晓风乾(个人参赛者)
我做了什么
市面上所有语言学习应用都在让你「学」,但绝大多数人打开它是靠毅力,不是靠喜欢。
我想做的是反过来:你本来就在听的那首歌,就是课本。
Lyra 是一个网页播放器。你像平时一样放一首日语歌,歌词跟着音乐逐字高亮;
听到不懂的地方,点一下那个词,它会告诉你这个词的读音、词性、词典义,
以及最关键的一层 —— 它在这一句歌词里到底什么意思。
觉得没听清,点「循环这句」,播放器就在这一句上反复循环,直到你听进去。
听过的每首歌会变成夜空里的一个星座,学过的词是星座上的星点,越听越亮。
具体用百炼做了三件事:
-
逐字注音与切分:把整句日语歌词切成词,判断哪些是实词、哪些是助词,
给汉字标假名读音和罗马音。日语的难点在于同一个汉字在不同词里读法完全不同
(「行く」读 iku、「行う」读 okonau),必须结合上下文判断。
-
语境释义:这是产品的核心价值,也是它和「点词弹词典」的分界线。
同样是「沈む」,词典义是「下沉、沉没」,但在「沈むように溶けてゆくように」这句里,
模型给出的是「形容事物如沉入水中般缓慢、不可逆地消失或消融」——
这是针对这一句生成的解释,不是词典搬运。
-
例句生成:用你听过的歌里出现过的词,生成新的例句,让同一个词在另一个语境里再出现一次。
工程上刻意守住的几条纪律(都有测试守门):
- 模型只负责解释,不负责判分。 学习进度、掌握度、复习安排全部由确定性代码和数据库约束决定,
模型永远不写掌握度 —— 因为「你学会了没有」这件事不能靠模型的自信程度来定。
- 每个用户的学习数据物理分库隔离,一人一个数据库文件,跨用户串数据在物理上不可能。
- 不做连胜、不做红叉、不做排行榜。 学语言最大的敌人是焦虑,不是懒。
站上已有 70 多首歌、6700 多条词与语法的理解结果,7 月上线至今持续在线。
上线前后累计有 200+ 位用户参与盲测,其中三位不同画像的用户(日语零基础、N3、只认罗马音)
各深度使用了两天 —— 下面踩坑记录第 4 条就是他们挑出来的问题。
使用的工具
- OpenWork / 百炼 CLI
- 百炼能力 / 模型:qwen-plus(逐字注音切分、语境释义、例句生成)、qwen-max(需要更强推理的分析任务);
经百炼 OpenAI 兼容接口(https://dashscope.aliyuncs.com/compatible-mode/v1)接入,
使用 JSON 结构化输出模式保证返回可直接落库
- Skill 名称:无 —— 直接经百炼 OpenAI 兼容接口调用 qwen 模型,未使用百炼 Skill / 应用框架
- 其他:Python / FastAPI 后端,原生 JavaScript 前端(无框架、无构建步骤),
SQLite 分库存储,MeCab 做日语形态分析给模型输出做接地校验
效果展示
在线体验:https://lyra.niuniu869.com (已开放注册,无需邀请码,建议戴耳机)
体验路径:注册 → 搜「夜に駆ける」→ 点播放 → 歌词逐字亮起 → 点任意一个词 → 看语境释义 → 点「循环这句」→ 看「地图」里的星图
项目链接(可选)
踩坑记录(可选)
1. 音源里混着「看起来正常」的残片,会静默毁掉体验。
有一首歌的音频缓存只有 81KB,但歌曲时长是 303 秒。用户点开能播,播到第 5 秒就静默卡死,
界面没有任何报错 —— 这比直接报错糟糕得多,用户会以为是自己网络的问题,傻等下去。
最初我判断是「下载中断」,加了 Content-Length 校验,结果重新下载还是 81KB,
而且完整通过了校验 —— 说明上游返回的就是个 5 秒试听片段,不是传输中断。
最终按「时长 × 最低码率」做完整性下限校验(303 秒的歌至少该有 1.2MB),
并把这类曲目标记后从推荐和搜索里隐藏,再加一个每天跑的复检任务,
等上游音源恢复了自动放回列表。
2. 歌词源会返回「简体转写版」的日语。
《残酷な天使のテーゼ》48 行歌词里有 10 行是简体字形:「少年よ神话になれ」(应为「神話」)、
「苍い风」(应为「蒼い風」)。对一个日语学习产品来说,这是在教用户写错字。
这里踩了两个坑:一是以为换个 CJK 字体能解决 —— 不能,「话 U+8BDD」和「話 U+8A71」是
两个不同的 Unicode 码位,不是同一个字的两种字形,字体只能改字形不能改码位;
二是想过交给模型去改 —— 但字形映射有唯一正确答案,查表 100% 准确、可单元测试、零延迟,
而模型有幻觉风险,更可能顺手改动字数或断句,那会直接打断逐字高亮赖以对齐的时间轴。
最后用确定性映射表解决,并且只对已判定为日语的歌词生效 ——
第一版我把它放在通用的歌词解析函数里,结果中文歌的歌词也被「日化」了(「真歌词」→「真歌詞」),
全量测试当场挂了 3 个才发现。
3. 定时任务可以「安静地坏」。
复检任务写完、单元测试全绿、部署上线。但我按 cron 的真实方式跑了一次,
日志立刻报 'NoneType' object has no attribute 'headers' —— 我传了个 None 当会话对象。
如果没跑那一次,这个任务会每天准时执行、正常退出、日志看着一切正常,
而复检功能从第一天起就是死的,没有任何人会发现。
根本原因是单元测试把那个函数整个 mock 掉了,永远测不到真实调用契约。
单测绿不等于功能活。
4. 上线前找了三个不同画像的用户各用两天,问题几乎全是我自己想不到的。
一个日语零基础的上班族、一个 N3 水平的学生、一个只认罗马音的动漫迷。
三个人都在同一个地方卡住:注册后弹出的口味问卷里的歌手他们一个都不认识。
零基础用户的原话是:「而且没有 YOASOBI —— 我就是冲它来的。」
查下来发现歌手列表是按「缓存里出现最多」排的,选出来的是系统内部的高频项,
而不是用户认得的名字。N3 用户则一眼看出片假名被逐字注了平假名读音(レ(れ)モ(も)ン(ん)),
指出这是纯视觉噪音 —— 会读片假名的人本来就会读平假名。
这些都不是能靠自己盯着代码看出来的。
参赛项目名称
Lyra · 在听歌的过程中学习一门语言
团队 / 作者
晓风乾(个人参赛者)
我做了什么
市面上所有语言学习应用都在让你「学」,但绝大多数人打开它是靠毅力,不是靠喜欢。
我想做的是反过来:你本来就在听的那首歌,就是课本。
Lyra 是一个网页播放器。你像平时一样放一首日语歌,歌词跟着音乐逐字高亮;
听到不懂的地方,点一下那个词,它会告诉你这个词的读音、词性、词典义,
以及最关键的一层 —— 它在这一句歌词里到底什么意思。
觉得没听清,点「循环这句」,播放器就在这一句上反复循环,直到你听进去。
听过的每首歌会变成夜空里的一个星座,学过的词是星座上的星点,越听越亮。
具体用百炼做了三件事:
逐字注音与切分:把整句日语歌词切成词,判断哪些是实词、哪些是助词,
给汉字标假名读音和罗马音。日语的难点在于同一个汉字在不同词里读法完全不同
(「行く」读 iku、「行う」读 okonau),必须结合上下文判断。
语境释义:这是产品的核心价值,也是它和「点词弹词典」的分界线。
同样是「沈む」,词典义是「下沉、沉没」,但在「沈むように溶けてゆくように」这句里,
模型给出的是「形容事物如沉入水中般缓慢、不可逆地消失或消融」——
这是针对这一句生成的解释,不是词典搬运。
例句生成:用你听过的歌里出现过的词,生成新的例句,让同一个词在另一个语境里再出现一次。
工程上刻意守住的几条纪律(都有测试守门):
模型永远不写掌握度 —— 因为「你学会了没有」这件事不能靠模型的自信程度来定。
站上已有 70 多首歌、6700 多条词与语法的理解结果,7 月上线至今持续在线。
上线前后累计有 200+ 位用户参与盲测,其中三位不同画像的用户(日语零基础、N3、只认罗马音)
各深度使用了两天 —— 下面踩坑记录第 4 条就是他们挑出来的问题。
使用的工具
经百炼 OpenAI 兼容接口(https://dashscope.aliyuncs.com/compatible-mode/v1)接入,
使用 JSON 结构化输出模式保证返回可直接落库
SQLite 分库存储,MeCab 做日语形态分析给模型输出做接地校验
效果展示
在线体验:https://lyra.niuniu869.com (已开放注册,无需邀请码,建议戴耳机)
体验路径:注册 → 搜「夜に駆ける」→ 点播放 → 歌词逐字亮起 → 点任意一个词 → 看语境释义 → 点「循环这句」→ 看「地图」里的星图
项目链接(可选)
踩坑记录(可选)
1. 音源里混着「看起来正常」的残片,会静默毁掉体验。
有一首歌的音频缓存只有 81KB,但歌曲时长是 303 秒。用户点开能播,播到第 5 秒就静默卡死,
界面没有任何报错 —— 这比直接报错糟糕得多,用户会以为是自己网络的问题,傻等下去。
最初我判断是「下载中断」,加了
Content-Length校验,结果重新下载还是 81KB,而且完整通过了校验 —— 说明上游返回的就是个 5 秒试听片段,不是传输中断。
最终按「时长 × 最低码率」做完整性下限校验(303 秒的歌至少该有 1.2MB),
并把这类曲目标记后从推荐和搜索里隐藏,再加一个每天跑的复检任务,
等上游音源恢复了自动放回列表。
2. 歌词源会返回「简体转写版」的日语。
《残酷な天使のテーゼ》48 行歌词里有 10 行是简体字形:「少年よ神话になれ」(应为「神話」)、
「苍い风」(应为「蒼い風」)。对一个日语学习产品来说,这是在教用户写错字。
这里踩了两个坑:一是以为换个 CJK 字体能解决 —— 不能,「话 U+8BDD」和「話 U+8A71」是
两个不同的 Unicode 码位,不是同一个字的两种字形,字体只能改字形不能改码位;
二是想过交给模型去改 —— 但字形映射有唯一正确答案,查表 100% 准确、可单元测试、零延迟,
而模型有幻觉风险,更可能顺手改动字数或断句,那会直接打断逐字高亮赖以对齐的时间轴。
最后用确定性映射表解决,并且只对已判定为日语的歌词生效 ——
第一版我把它放在通用的歌词解析函数里,结果中文歌的歌词也被「日化」了(「真歌词」→「真歌詞」),
全量测试当场挂了 3 个才发现。
3. 定时任务可以「安静地坏」。
复检任务写完、单元测试全绿、部署上线。但我按 cron 的真实方式跑了一次,
日志立刻报
'NoneType' object has no attribute 'headers'—— 我传了个None当会话对象。如果没跑那一次,这个任务会每天准时执行、正常退出、日志看着一切正常,
而复检功能从第一天起就是死的,没有任何人会发现。
根本原因是单元测试把那个函数整个 mock 掉了,永远测不到真实调用契约。
单测绿不等于功能活。
4. 上线前找了三个不同画像的用户各用两天,问题几乎全是我自己想不到的。
一个日语零基础的上班族、一个 N3 水平的学生、一个只认罗马音的动漫迷。
三个人都在同一个地方卡住:注册后弹出的口味问卷里的歌手他们一个都不认识。
零基础用户的原话是:「而且没有 YOASOBI —— 我就是冲它来的。」
查下来发现歌手列表是按「缓存里出现最多」排的,选出来的是系统内部的高频项,
而不是用户认得的名字。N3 用户则一眼看出片假名被逐字注了平假名读音(
レ(れ)モ(も)ン(ん)),指出这是纯视觉噪音 —— 会读片假名的人本来就会读平假名。
这些都不是能靠自己盯着代码看出来的。