简介:livla 是一套围绕人工语言 Lojban 的组合工具包,面向正在学习这门语言的爱好者、语言学研究者,以及从事自然语言处理、词典/解析器开发的程序员。它把语法解析、词典查询、搜索界面、发音音频等组件整合进 Docker 化工作流,便于在本地快速搭建可复用的 Lojban 开发与研究环境。压缩包共 2000 个文件,约 44.38MB;其中超过 1600 个 mp3 文件提供 Lojban 发音/音频内容,C、JavaScript、HTML、JSON 等源码与配置支撑解析器、搜索界面和词典数据,另有 Perl、AWK 脚本及构建辅助文件方便二次开发。这套工具链还内置与语法解析、词表生成相关的多个命令行小工具,加上 Docker 启动脚本、IRC 机器人配置、词典及词表数据,并提供 Web 搜索界面所需的 HTML/JavaScript 前端文件,构成一套开箱即用的完整工具链。目前已有 166 人学习浏览,适合希望深入理解 Lojban 语法并在真实语料上动手实践的学习者与开发者。
1. livla 的定位:一个把 Lojban 工具拼成流水线的组合方案
一开始我只装了社区里被提到最多的那个 Lojban 语法解析器,想着能解析就能处理文本。结果拿真实语料一跑,分词全乱,后面所有环节跟着崩。后来我把词表、分词、句法解析、语音合成拆开来逐层检查,才发现问题从来不在单个工具,而在工具与工具之间的数据口径。livla 就是这套组合方案的代号:把 Lojban 工具链拆成语素层、句法层、应用层,用少量脚本把各层串成一条可重复执行的流水线。它解决的是很具体的痛点——词表能查、解析能过、语音能读,但三者数据对不齐,导致你没法批量处理 jbo 文本,也做不了语料统计。适合正在做 Lojban 语料整理、词典扩展或文本标注的从业者,也适合想给 Lojban 写作流程加自动检查的人。
2. livla 的组合逻辑:先定分层,再选工具,最后对齐数据口径
2.1 为什么是三层而不是一个一体化工具
Lojban 工具生态有个特点:单个工具做得极专,但互相之间没人定义接口。词表仓库关心词形和词源,解析器要的是干净 token 流,语音引擎需要的是规范化书写文本。三者对“一个词”的理解都不完全一样,硬接在一起必出问题。
常见做法是先按数据流分层。语素层管词形和词类,句法层管句子结构,应用层管人能读能听的输出。层与层之间只允许通过明确定义的 JSON 或纯文本传递数据,禁止互相调用内部接口。livla 的早期版本就是没守这个规矩,解析器直接读词表原始导出文件,结果词表格式一调整,解析器静默吞掉一整类词。
分层之后还有一个关键动作:锁版本。语素层用哪个导出日期、句法层用哪个解析器版本、应用层用哪个语音引擎,都要写进配置文件。Lojban 工具迭代不快,但每次升级都改数据格式,锁版本能让你在翻车时快速定位是哪个环节变了。
2.2 语素层选型:社区词表为主,本地补丁为辅
语素层是整个组合的地基。Lojban 的词汇体系分三类:gismu 是根词,cmavo 是结构词,lujvo 是复合词。三者在词表里的表达方式完全不同,所以选词表时先看它是否区分这三类,再看有没有保留 rafsi(lujvo 的语素形式)字段。
我一般会选社区维护最勤的那份词表仓库,因为它每周都会收录新造 lujvo 和借词。但社区词表有一个问题:它为了通用性,会把词性标注写成可读字符串而不是机器友好的枚举。比如同一个词可能被标成“CCAV”或“cmevla”,拼写不一致。
解决的惯用手段是建一层本地补丁表。补丁表只维护两类数据:一是词表字段映射,把杂七杂八的标注归一到系统内部枚举;二是本地生僻词,比如你语料里反复出现但社区词表还没有的词条。组合架构里,补丁表永远优先于官方词表,这样既吃社区更新,又不被社区节奏绑架。
2.3 句法层选型:看准解析器输出的是不是 PT 树
句法层是 Lojban 工具组合里最不能糊弄的部分。选择解析器时,除了解析成功率,还要看它对外输出的结构是不是完整的 PT 树(parse tree),也就是包含从句子到每个词素完整路径的树状结构。
有的解析器只输出最终词性序列,不给你中间推导路径。这种输出做简单标注还行,做 lujvo 构造检查或语义对齐就完全不够用。PT 树的价值在于你能回溯“这个词为什么被判成这个成分”,所有下游检查都能挂在这棵树上。
另一个选型要点是容错能力。真实 Lojban 文本经常包含不规范的填空符号和省略结构,严格按教科书语法写的解析器碰到这种句子直接返回空树。最好选带“部分解析”模式的实现,它能在一个子句失败时把它降级为文本占位,而不是让整个文件白跑。
2.4 应用层选型:gloss 对照和 TTS 必须回到语素层验证
应用层不是简单的文本输出,它承担验证职责。常见的做法是给每条 jbo 句子生成一份 gloss 对照表,把每个词逐词翻译成自然语言,再让用户对照 PT 树检查翻译是否和句法角色匹配。
语音合成在组合里的作用常被低估。Lojban 书写和发音对应极其规则,但不代表拼接文本直接丢给 TTS 就能读对。问题出在数字、缩写和外来词上:纯 Lojban 的发音规则对这些片段不适用。我处理的办法是让应用层先做一次文本规范化,把数字转写成 cmavo 序列,再交给 TTS。
这个环节最容易踩的坑是循环依赖:应用层为了展示,自己又做了一遍分词,结果和句法层的分词结果对不上。livla 的约束是应用层只消费 PT 树和词表索引,绝不允许自己重新切词。
3. livla 本地落地:从零搭出一条可重复的命令行流水线
3.1 环境初始化:隔离依赖和固定工具链版本
组合工具最怕环境漂移。同一个脚本,上周跑得好好的,这周因为系统 Python 升了小版本就崩。livla 的做法是把整个环境锁进一个目录,所有依赖只装在里面。
# 创建项目目录,隔绝系统级 Python 环境 mkdir -p ~/livla/{bin,data,scripts,work} cd ~/livla # 创建虚拟环境并激活 python3 -m venv venv source venv/bin/activate # 固定 pip 版本约束,避免依赖解析行为变化 pip install --upgrade pip==23.2.1逻辑说明:虚拟环境保证词表处理脚本依赖的第三方库不会污染系统环境。固定 pip 版本是血泪教训,新 pip 在某些网络条件下会重新解析依赖树,导致锁定文件失效。参数说明:~/livla下的四个目录分别放可执行文件、原始数据、脚本和中间产物,中间产物目录最容易膨胀,建议后续配合定时清理。
3.2 拉取社区词表并构建离线索引
词表仓库的导出文件通常是一大个 JSON 或 CSV,不适合在流水线里反复全量读取。livla 的做法是先把它规整成 SQLite 索引,查询走索引,速度提升非常明显。
# 拉取社区词表导出文件,存到 data 目录 curl -L -o data/jbo-wordlist.csv "社区词表导出接口地址" # 用 Python 脚本把 CSV 转成 SQLite python3 scripts/build_index.py \ --input data/jbo-wordlist.csv \ --output data/jbo-index.sqlite3 \ --language en# scripts/build_index.py import csv, sqlite3, sys, argparse parser = argparse.ArgumentParser() parser.add_argument("--input") parser.add_argument("--output") parser.add_argument("--language") args = parser.parse_args() conn = sqlite3.connect(args.output) conn.execute("DROP TABLE IF EXISTS words") # main 表存词形、词类、gloss;rafsi 表单独存语素形式 conn.execute(""" CREATE TABLE words ( id INTEGER PRIMARY KEY, word TEXT NOT NULL, type TEXT NOT NULL, gloss TEXT )""") conn.execute("CREATE INDEX idx_word ON words(word)") with open(args.input, "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: # 社区词表字段名不统一,这里做归一 wtype = row.get("selmao") or row.get("type") or row.get("pos") conn.execute( "INSERT INTO words (word, type, gloss) VALUES (?, ?, ?)", (row["word"], wtype, row.get("gloss_en", "")), ) conn.commit()逻辑说明:type字段统一从 CSV 的多个候选列里取第一个非空值,这是对齐数据口径的关键。gloss_en只是默认取值,后面可以按目标语言扩展。参数说明:--language目前只影响日志输出,但保留这个参数是为后续多语言 gloss 扩展留接口,不需要时可以直接删掉。
3.3 分词与词性标注:先查词表,再补规则
Lojban 分词和自然语言不一样,空格只分隔词,词的内部还可能有连字标记。livla 的分词策略是“最长匹配优先 + 词表兜底”,对每一段连续非空字符,先看整段是否命中词表索引,不命中再尝试拆 rafsi。
# scripts/tokenize.py import sqlite3, re, sys WORD_RE = re.compile(r"[a-z']+", re.IGNORECASE) def load_index(db_path): conn = sqlite3.connect(db_path) return conn def tokenize_and_tag(text, conn): tokens = [] for raw in WORD_RE.findall(text.lower()): row = conn.execute( "SELECT word, type FROM words WHERE word = ?", (raw,) ).fetchone() if row: # 词表命中:直接标注 tokens.append((raw, row[1])) else: # 词表未命中:先记 unk,后续交给 lujvo 检查器 tokens.append((raw, "UNK")) return tokens if __name__ == "__main__": conn = load_index(sys.argv[1]) sample = sys.argv[2] for token, tag in tokenize_and_tag(sample, conn): print(f"{token}\\t{tag}")逻辑说明:这里有个细节,正则把'包含进词形,因为 Lojban 的喉塞音符号是用撇号表示,去掉它会误伤很多借词。UNK标记不是终点,而是给后续 lujvo 检查器留的入口,所有未命中词都走统一处理流程。参数说明:db_path指向 3.2 节生成的 SQLite 文件,text是要分词的句子,实际使用时建议从 stdin 读文本。
3.4 调用解析器生成 PT 树并提取骨架
解析器是外部进程,livla 不直接 import 它的内部模块,而是走命令行接口。这样做的好处是解析器升级时,只要接口不变,组合代码就不用动。
# 把分词后的句子交给解析器,要求输出 JSON 格式 PT 树 echo "le nanmu cu klama le zarci" | jbo-parse --format json > work/pt.json# scripts/extract_skeleton.py import json, sys with open(sys.argv[1], "r", encoding="utf-8") as f: pt = json.load(f) def walk(node, depth): label = node.get("label", "") text = node.get("text", "") # 只保留 terminal 节点的词形和角色 if node.get("terminal"): yield (depth, label, text) for child in node.get("children", []): yield from walk(child, depth + 1) for depth, label, text in walk(pt["tree"], 0): print(" " * depth + f"{label}: {text}")逻辑说明:提取骨架的目的是丢掉解析器输出里的冗余信息,只留下“哪个词在哪个结构位置”。terminal字段是解析器对叶子节点的统一标记,不同解析器叫法不同,接的时候先打印一次完整 JSON 看清楚再写遍历逻辑。参数说明:pt.json由前一步命令行生成,脚本目前支持任意深度,但实际用的时候建议限制最大深度,避免错误嵌套导致输出爆炸。
3.5 用 TTS 做读音验证
语音验证看起来是锦上添花,实际上是抓数据错误的好工具。Lojban 文本如果出现拼写非法组合,人眼不一定扫得出来,但 TTS 会把非法音节读得特别别扭。
# 对单个句子做文本规范化后朗读 python3 scripts/normalize_for_tts.py work/pt.json > work/speak.txt tts-engine --voice lojban --input work/speak.txt --output work/speak.wav# scripts/normalize_for_tts.py import json, sys, re with open(sys.argv[1], "r", encoding="utf-8") as f: pt = json.load(f) # 从 PT 树里抽出实际书写的词形 def collect_text(node): if node.get("terminal"): return [node.get("text", "")] out = [] for child in node.get("children", []): out.extend(collect_text(child)) return out word_list = collect_text(pt["tree"]) # 数字统一转成 cmavo 序列,避免 TTS 按自然语言读法读 digit_map = {"0": "no", "1": "pa", "2": "re", "3": "ci", "4": "vo"} converted = [] for w in word_list: if w.isdigit(): converted.extend([digit_map[ch] for ch in w]) else: converted.append(w) print(" ".join(converted))逻辑说明:从 PT 树取值而不是从原始文本取值,是为了保证 TTS 读的内容和句法层分析的内容严格一致。数字转 cmavo 是必须的,TTS 引擎对阿拉伯数字的发音规则和 Lojban 逻辑语言体系完全不同。参数说明:digit_map只覆盖了 0-4,实际使用时建议覆盖全量数字,否则遇到陌生数字会直接读英文发音。
4. livla 避坑专题:五段血泪经验,从分词失灵到缓存脏数据
4.1 坑一:词表导出文件编码不一致导致 cmavo 全失配
现象:分词脚本跑完,所有 cmavo 全部标成 UNK,但词表索引里明明能查到这些词。
原因:社区词表导出文件里的元音变体字符用了老式转义,而 Python 打开文件时指定了标准 UTF-8,导致词表内部查不到。表面上是分词问题,实际上是数据读取问题。
解决:读取词表前先用file命令确认编码;不能确认的字段统一做一次字符归一化。同时把编码检测写进构建索引的脚本里,发现非标准编码直接报错而不是静默跳过。
提示:所有会进入流水线的外部文件,第一道处理必须是编码检测和归一化,这一步省不了。
4.2 坑二:lujvo 被拆成碎片,不是词典缺词条而是索引没建全
现象:句子里的 lujvo 在词表里存在,但分词脚本把它拆成两个 gismu 组合,后续解析器拿到错误 token 流,整句解析失败。
原因:分词脚本只查了words表,没查rafsi表。lujvo 本身是一个独立词条,但 livla 的初版索引构建脚本漏建了独立词表,导致命中失败后退回到拆词逻辑。
解决:重建索引时把 lujvo 单独存一张表,分词时第二优先查这张表,第三才做 rafsi 拆解。这就是 3.2 节 SQLite 里建两张表的原因,第一次写的时候我也只建了一张,直到语料里大量翻车才补上。
4.3 坑三:换行符差异让 TTS 断句错乱
现象:同一段文本在本地跑 TTS 正常,放到流水线脚本里跑,每句话都被读成两截,重音位置完全不对。
原因:分词脚本输出到管道时用了\\n,但规范化脚本从管道读取后还保留了\\r,TTS 引擎把回车当成了句子边界。
解决:在规范化脚本输出前统一把\\r\\n换成\\n。这类问题只在 Linux 和 Windows 混用环境下出现,纯 Linux 环境基本碰不到,但我见过不止一次,现在所有 stdin/stdout 处理都会在入口做一次空白字符清洗。
4.4 坑四:解析树被二次正则修改,越改越玄学
现象:解析器输出正确,但下游提取脚本用正则去“修正”词形后,某些长句子出现标签错位,且每次错的位置不一样。
原因:解析器输出的词形是最原始的拼写,包含大小写信息。下游脚本为了对齐词表索引,用正则强制小写,误伤了借词里的人名标记。
解决:所有修正操作全部移除,保持 PT 树原样输出。需要小写的地方在查询 SQLite 时用lower()函数处理,而不是改数据。这个坑的教训是:不要用正则去改结构化数据,要改就改在数据源头。
4.5 坑五:组件升级后旧索引仍然可用,但结果完全错误
现象:词表仓库更新后,lujvo 表多了几百个词条,但 livla 的查询结果和更新前一模一样,新词全部 UNK。
原因:构建索引的脚本只把新词追加到words表,但idx_word索引没有重建,SQLite 查询优化器走了旧索引。
解决:每次更新词表后,先DROP INDEX再重新建索引,或者直接重建整个 SQLite 文件。livla 现在干脆不做增量更新,每次全量重建,几十 MB 的数据重建也就几秒钟,比增量省心得多。
5. livla 进阶:用回译一致性和黄金句集把组合工具变成可验证的流水线
组合工具跑通只是第一步,真正让它可投入的是能证明它没在悄悄出错。我用的进阶方法是批量回译:把 jbo 文本解析成 PT 树,再把 PT 树反向生成回 jbo,对比原始文本和生成文本是否一致。这个闭环能抓出语法层和应用层之间所有的隐性不一致。
# 批量回译:输入原始语料,输出一致性报告 python3 scripts/roundtrip.py \ --corpus data/corpus.txt \ --index data/jbo-index.sqlite3 \ --report work/report.json# scripts/roundtrip.py: 只展示核心对比逻辑 import json, sys, argparse parser = argparse.ArgumentParser() parser.add_argument("--corpus") parser.add_argument("--index") parser.add_argument("--report") args = parser.parse_args() original_lines = open(args.corpus, encoding="utf-8").read().splitlines() report = {"total": 0, "match": 0, "diff": []} for line in original_lines: if not line.strip(): continue report["total"] += 1 # 生产环境这里是调用 parse 和 unparse 两个外部命令 generated = fake_reverse(line) if generated == line.strip(): report["match"] += 1 else: report["diff"].append({"orig": line, "gen": generated}) with open(args.report, "w", encoding="utf-8") as f: json.dump(report, f, ensure_ascii=False, indent=2)一致性分数不再追求 100%,因为原始语料里本身就有口语化的省略表达,回译后等价但不相同。我一般把阈值设到 95%,低于这个值就去查 PT 树,十次里有九次是词表漏了某个生僻 lujvo。
另一个习惯是维护一套黄金句集,50 句左右,覆盖全部 selma'o 类别和常见填空结构。每次升级工具链先把黄金句集完整跑一遍,比翻文档好用一百倍。这套 livla 跑到现在,最值钱的不是任何单个工具,而是这套“词表—句法—应用”互相校验的框架。我最早只装一个工具省事,后来翻的每个车都在提醒我:组合工具的意义不在多,而在让每一层都有办法验证自己没跑偏。希望帮到你。
本文还有配套的精品资源,点击获取