专名本土化(信达雅):冻结译名表 + 渲染层,并修掉校验器的正则盲点
- 新增 dfannals/glossary.py(译名表)与 dfannals/localize.py(渲染层) 人名=音译+绰号意译(Ral Fastenhatchets→拉尔·缚斧、朗古德·商熊「剖胎之寒」), 地名整体意译(Fordedwinds→涉风渡、The Volcano Of Ravens→渡鸦火山) - 生成链路本土化:素材渲染、人物表、扩写白名单全部走译名;已有产物做确定性替换 - 修复 factcheck 的 \b 盲点:Unicode 下汉字也算词字符,紧贴汉字的英文名不被检查 (曾因此漏掉编造专名)。修好后抓出并定点修掉长版第 1 集的 2 处编造 - 修正 title_form 大小写(YETI→Yeti),此前 Yeti/Goblin 一直漏译 - 新增 tests/test_localize.py(13 项回归)
This commit is contained in:
+3
-2
@@ -15,7 +15,7 @@ import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from dfannals import factcheck
|
||||
from dfannals import factcheck, localize
|
||||
from dfannals.config import MAX_TOKENS_STRUCTURED, Gateway
|
||||
from dfannals.episodes import Episode
|
||||
from dfannals.legends import Event, World
|
||||
@@ -221,7 +221,8 @@ def build_cast(
|
||||
"",
|
||||
]
|
||||
|
||||
return "\n".join(lines) + "\n", rows, suspicions
|
||||
# 专名本土化:表格与小传都输出中文译名
|
||||
return localize.for_world(world).render("\n".join(lines)) + "\n", rows, suspicions
|
||||
|
||||
|
||||
def write_cast(path: Path, text: str) -> Path:
|
||||
|
||||
+6
-4
@@ -18,7 +18,7 @@ from collections import Counter
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
from dfannals import deslop, volume
|
||||
from dfannals import deslop, localize, volume
|
||||
from dfannals import episodes as ep_mod
|
||||
from dfannals.chronicle import cjk_length
|
||||
from dfannals.config import (
|
||||
@@ -100,14 +100,16 @@ def allowed_names(world: World, thread: Thread, episode: ep_mod.Episode) -> list
|
||||
names.add(name)
|
||||
figure = world.figures.get(fid)
|
||||
if figure and figure.race:
|
||||
names.add(figure.race)
|
||||
names.add(_title_form(figure.race))
|
||||
# 种族值也要给中文:模型引到雪人、鹰人这类词时不能写成新角色
|
||||
names.add(localize.translate_race(figure.race))
|
||||
for event in episode.events:
|
||||
for ref in world.render_refs(event):
|
||||
value = ref.partition("=")[2].strip()
|
||||
if value:
|
||||
names.add(value)
|
||||
return sorted(n for n in names if n)
|
||||
# 全部过一遍译名表:白名单里必须是正文里真正会出现的写法
|
||||
translator = localize.for_world(world)
|
||||
return sorted({tr for tr in (translator.render(n).strip() for n in names) if tr})
|
||||
|
||||
|
||||
def chunk_messages(
|
||||
|
||||
@@ -35,7 +35,10 @@ COMMON_WORDS = {
|
||||
}
|
||||
|
||||
# 拉丁专名序列:首字母大写的词,可含 ' - 与数字
|
||||
NAME_RE = re.compile(r"\b[A-Z][A-Za-z'\-]*(?:\s+[A-Z][A-Za-z'\-]*)*\b")
|
||||
# 英文专名的边界不能用 \b:Unicode 下汉字也算「词字符」,于是 `在Mistbreath的`
|
||||
# 这种紧贴汉字的英文名根本不会被匹配——实测正是这个盲点让一个编造的地名混过了校验。
|
||||
# 改成前后看「是不是英文字母」,与汉字相邻也能抓到。
|
||||
NAME_RE = re.compile(r"(?<![A-Za-z])[A-Z][A-Za-z'\-]*(?:\s+[A-Z][A-Za-z'\-]*)*(?![A-Za-z])")
|
||||
_CJK = re.compile(r"[\u4e00-\u9fff]")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
"""专名译名表(冻结)。
|
||||
|
||||
为什么要有这张表:正文里满屏 `Ral Fastenhatchets`、`Fordedwinds` 对中文读者很难读。
|
||||
但如果交给模型每次现译,同一个角色会在第 3 集变成另一个名字,所以译名必须冻结在这里。
|
||||
|
||||
翻译策略(信达雅):
|
||||
|
||||
1. **人名 = 名字音译 + 姓氏/绰号意译**,用「·」连接。
|
||||
这与《魔戒》把 Thorin Oakenshield 译作「索林·橡木盾」是同一套惯例,
|
||||
读起来像中国人名,又保住了原名的意思。
|
||||
2. **地名整体意译**。DF 地名同样是词表拼接(Fordedwinds = forded + winds),
|
||||
直译成「涉风渡」比音译成「福德温兹」更像地名。
|
||||
3. **称号(the X of Y)单独译**,DF 给传奇人物的长称号自带画面感,直译最好。
|
||||
|
||||
DF 的名字由词表拼接而成,所以同一批词根会在不同世界反复出现:
|
||||
这张表按「整词」而非「词根」收词,避免把 Fateddie 硬拆成"命定+死"这类生硬堆叠。
|
||||
遇到表里没有的名字,localize 会原样保留并登记进待译清单,而不是硬造一个。
|
||||
"""
|
||||
|
||||
# ------------------------------------------------------------------ 名字词干(音译)
|
||||
STEMS: dict[str, str] = {
|
||||
"alath": "阿拉斯",
|
||||
"amxu": "阿姆苏",
|
||||
"ana": "安娜",
|
||||
"artuk": "阿图克",
|
||||
"asmel": "阿斯梅尔",
|
||||
"astri": "阿斯特里",
|
||||
"domas": "多马斯",
|
||||
"dostngosp": "多斯特恩戈斯普",
|
||||
"ducim": "杜西姆",
|
||||
"espu": "埃斯普",
|
||||
"evala": "埃瓦拉",
|
||||
"guspu": "古斯普",
|
||||
"id": "伊德",
|
||||
"ineth": "伊内斯",
|
||||
"ingish": "英吉什",
|
||||
"langgud": "朗古德",
|
||||
"lolor": "洛洛尔",
|
||||
"meng": "蒙",
|
||||
"moldath": "莫尔达斯",
|
||||
"momuz": "莫穆兹",
|
||||
"mosus": "莫苏斯",
|
||||
"mudi": "穆迪",
|
||||
"ngokang": "恩戈康",
|
||||
"ngom": "恩戈姆",
|
||||
"niri": "尼里",
|
||||
"nithe": "尼瑟",
|
||||
"ongu": "翁古",
|
||||
"onul": "奥努尔",
|
||||
"pari": "帕里",
|
||||
"perom": "佩罗姆",
|
||||
"ral": "拉尔",
|
||||
"rigoth": "里戈斯",
|
||||
"sathrel": "萨瑟雷尔",
|
||||
"sayrsnis": "赛尔斯尼斯",
|
||||
"shosa": "绍萨",
|
||||
"sina": "西娜",
|
||||
"someg": "索梅格",
|
||||
"song": "松",
|
||||
"stinthad": "斯廷哈德",
|
||||
"tekkud": "泰库德",
|
||||
"thrun": "斯伦",
|
||||
"tirist": "提里斯特",
|
||||
"tulon": "图隆",
|
||||
"uknol": "乌克诺尔",
|
||||
"usel": "乌塞尔",
|
||||
"utes": "乌特斯",
|
||||
"zuntir": "尊提尔",
|
||||
"zutthan": "祖坦",
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ 姓氏/绰号(意译)
|
||||
# 键是姓名后半段的拼接结果(小写、无空格)
|
||||
SURNAMES: dict[str, str] = {
|
||||
"baldedattic": "秃阁",
|
||||
"basementrampart": "地垒",
|
||||
"blazedbrushes": "焰刷",
|
||||
"blottedmine": "污矿",
|
||||
"bonestands": "骨立",
|
||||
"bronzescorched": "铜焦",
|
||||
"churchbrass": "堂铜",
|
||||
"clappedbrass": "拍铜",
|
||||
"crazeowned": "狂据",
|
||||
"decentpage": "端页",
|
||||
"dyesong": "染歌",
|
||||
"fastenhatchets": "缚斧",
|
||||
"fateddie": "命终",
|
||||
"fencefinders": "篱寻",
|
||||
"fisherfair": "渔市",
|
||||
"flayerline": "剥血",
|
||||
"frillyknots": "褶结",
|
||||
"glimmeredhelps": "光援",
|
||||
"glowroofs": "光顶",
|
||||
"gleamcircled": "光环",
|
||||
"growllie": "吼卧",
|
||||
"hammershake": "锤摇",
|
||||
"huggedfastened": "抱固",
|
||||
"incenselyrics": "香诗",
|
||||
"jackalbrims": "豺帽",
|
||||
"matchedmenaces": "匹凶",
|
||||
"menacedslinks": "危潜",
|
||||
"mirroredfloors": "镜层",
|
||||
"oilshove": "油推",
|
||||
"palmsaved": "掌救",
|
||||
"playfills": "戏填",
|
||||
"ruinedhorrors": "毁怖",
|
||||
"searcheddated": "寻期",
|
||||
"squirtlocks": "喷锁",
|
||||
"steelgrouped": "钢聚",
|
||||
"stilledearthen": "静土",
|
||||
"stolendipped": "窃浸",
|
||||
"strikepaints": "击彩",
|
||||
"talonhollows": "爪穴",
|
||||
"theatercrested": "剧冠",
|
||||
"thornticks": "棘蜱",
|
||||
"tongswall": "钳墙",
|
||||
"tradebears": "商熊",
|
||||
"walkclasped": "行扣",
|
||||
"watchedleads": "守铅",
|
||||
"wheeleddrinks": "轮饮",
|
||||
"wisdomskins": "智皮",
|
||||
"yellplank": "啸板",
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ 地名(整体意译)
|
||||
PLACES: dict[str, str] = {
|
||||
"clampokes": "钳戳",
|
||||
"fordedwinds": "涉风渡",
|
||||
"fuchsiarumor": "紫谣",
|
||||
"gloryholes": "荣耀穴",
|
||||
"lashheroes": "鞭雄",
|
||||
"leapdrinks": "跃饮",
|
||||
"liecats": "卧猫",
|
||||
"mortalpears": "殇梨",
|
||||
"paperhushed": "纸寂",
|
||||
"rainseduced": "雨诱",
|
||||
"tomessoldiers": "典兵",
|
||||
"toolfainted": "器昏",
|
||||
"walkedbirds": "行鸟",
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ 称号(the X of Y)
|
||||
EPITHETS: dict[str, str] = {
|
||||
"the eviscerated chill of pregnancies": "剖胎之寒",
|
||||
"the trickery of mornings": "晨之诡计",
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ 势力/组织
|
||||
GROUPS: dict[str, str] = {
|
||||
"the volcano of ravens": "渡鸦火山",
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ 种族
|
||||
RACES: dict[str, str] = {
|
||||
"AARDVARK_MAN": "土豚人",
|
||||
"ALLIGATOR": "鳄鱼",
|
||||
"ALLIGATOR_MAN": "鳄人",
|
||||
"BAT_GIANT": "巨蝠",
|
||||
"BEAR_BLACK": "黑熊",
|
||||
"BEAR_GRIZZLY": "灰熊",
|
||||
"BEAR_GRIZZLY_MAN": "灰熊人",
|
||||
"BEAR_POLAR": "白熊",
|
||||
"BIRD_OWL_GREAT_HORNED": "大雕鸮",
|
||||
"BIRD_ROC": "大鹏",
|
||||
"BIRD_WREN": "鹩鹪",
|
||||
"BLIND_CAVE_BEAR": "盲穴熊",
|
||||
"BLIND_CAVE_OGRE": "盲穴食人魔",
|
||||
"BOBCAT": "山猫",
|
||||
"CAMEL_1_HUMP_MAN": "单峰驼人",
|
||||
"CARDINAL_MAN": "红衣主教雀人",
|
||||
"CAVE_DRAGON": "穴龙",
|
||||
"CHEETAH": "猎豹",
|
||||
"CHINCHILLA_MAN": "绒鼠人",
|
||||
"COLOSSUS_BRONZE": "青铜巨像",
|
||||
"COUGAR": "美洲狮",
|
||||
"CYCLOPS": "独眼巨人",
|
||||
"DOG": "犬",
|
||||
"DWARF": "矮人",
|
||||
"EAGLE_MAN": "鹰人",
|
||||
"ELEPHANT": "象",
|
||||
"ELF": "精灵",
|
||||
"FALCON": "隼族",
|
||||
"GOAT_MOUNTAIN_MAN": "山羊山人",
|
||||
"GOBLIN": "哥布林",
|
||||
"HUMAN": "人类",
|
||||
"HYDRA": "九头蛇",
|
||||
"JAGUAR": "美洲豹",
|
||||
"KOBOLD": "狗头人",
|
||||
"LEOPARD": "豹",
|
||||
"LION": "狮",
|
||||
"TIGER": "虎",
|
||||
"TROLL": "巨魔",
|
||||
"WOLF": "狼",
|
||||
"YETI": "雪人",
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ 地点类型
|
||||
SITE_TYPES: dict[str, str] = {
|
||||
"cave": "洞穴",
|
||||
"dark fortress": "黑堡",
|
||||
"dark pits": "幽坑",
|
||||
"forest retreat": "林居",
|
||||
"fortress": "要塞",
|
||||
"hamlet": "村落",
|
||||
"hillocks": "丘屯",
|
||||
"tomb": "陵墓",
|
||||
"town": "城镇",
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
"""专名本土化:把史料里的英文专名翻成中文(只作用于显示层)。
|
||||
|
||||
铁律:**只改显示,不改史料**。World 里的英文原名、事件字段、事实校验依据全部保持原样,
|
||||
译文只在这一层生成,所以:
|
||||
|
||||
- 事实校验仍以英文原名为准(译文通过 `Translator.accepted()` 参与校验,不会被当成编造);
|
||||
- 译名不写回 data/export,不改变 legends 的解析结果,去掉这层也不影响任何已产出的史料。
|
||||
|
||||
表里查不到的名字**原样保留**并登记进 `pending`,绝不硬造一个音译——
|
||||
一个难读的英文名远好过一个每次都不一样的乱译名。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from dfannals import glossary
|
||||
from dfannals.legends import World
|
||||
|
||||
_WORD_SPLIT = re.compile(r"\s+")
|
||||
_CJK = r"\u4e00-\u9fff\u3000-\u303f\uff00-\uffef"
|
||||
|
||||
|
||||
def split_epithet(name: str) -> tuple[str, str]:
|
||||
"""拆出 DF 的长称号:`Xxx Yyy the Zzz Of Www` → ('Xxx Yyy', 'the zzz of www')。"""
|
||||
lowered = name.strip().lower()
|
||||
marker = " the "
|
||||
if marker in lowered:
|
||||
main, epithet = lowered.split(marker, 1)
|
||||
return main, "the " + epithet
|
||||
return lowered, ""
|
||||
|
||||
|
||||
def translate_person(name: str) -> str | None:
|
||||
"""人名:名字音译 + 姓氏意译。查不全就返回 None(宁可保留英文)。"""
|
||||
if not name:
|
||||
return None
|
||||
main, epithet = split_epithet(name)
|
||||
parts = _WORD_SPLIT.split(main)
|
||||
stem = glossary.STEMS.get(parts[0])
|
||||
if stem is None:
|
||||
return None
|
||||
surname = ""
|
||||
if len(parts) > 1:
|
||||
surname = glossary.SURNAMES.get("".join(parts[1:]), "")
|
||||
if not surname:
|
||||
return None
|
||||
out = stem if not surname else f"{stem}·{surname}"
|
||||
if epithet:
|
||||
out += "「" + glossary.EPITHETS.get(epithet, "") + "」"
|
||||
if not glossary.EPITHETS.get(epithet):
|
||||
return None
|
||||
return out
|
||||
|
||||
|
||||
def translate_place(name: str) -> str | None:
|
||||
"""地名:整体意译;带称号的写成「主名(称号)」。"""
|
||||
if not name:
|
||||
return None
|
||||
main, epithet = split_epithet(name)
|
||||
base = glossary.PLACES.get(main.replace(" ", ""))
|
||||
if base is None:
|
||||
return None
|
||||
if epithet:
|
||||
extra = glossary.EPITHETS.get(epithet)
|
||||
return f"{base}({extra})" if extra else None
|
||||
return base
|
||||
|
||||
|
||||
def translate_group(name: str) -> str | None:
|
||||
if not name:
|
||||
return None
|
||||
return glossary.GROUPS.get(name.strip().lower())
|
||||
|
||||
|
||||
def translate_race(value: str) -> str:
|
||||
"""种族值(DWARF、EAGLE_MAN)→ 中文;查不到原样返回。"""
|
||||
if not value:
|
||||
return value
|
||||
return glossary.RACES.get(value.strip().upper(), value)
|
||||
|
||||
|
||||
def translate_site_type(value: str) -> str:
|
||||
if not value:
|
||||
return value
|
||||
return glossary.SITE_TYPES.get(value.strip().lower(), value)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Translator:
|
||||
"""把一整个世界的专名译好,供渲染与校验共用。"""
|
||||
|
||||
people: dict[str, str] = field(default_factory=dict) # 英文原名 → 中文
|
||||
places: dict[str, str] = field(default_factory=dict)
|
||||
groups: dict[str, str] = field(default_factory=dict)
|
||||
stems: dict[str, str] = field(default_factory=dict) # 单用名(正文里绝大多数是简称)
|
||||
surnames: dict[str, str] = field(default_factory=dict) # 单用姓(对白里互相叫姓)
|
||||
races: dict[str, str] = field(default_factory=dict) # 含全大写与首字母大写两种写法
|
||||
pending: list[str] = field(default_factory=list) # 表里没有、原样保留的名字
|
||||
|
||||
def person(self, name: str) -> str:
|
||||
return self.people.get(name, name)
|
||||
|
||||
def place(self, name: str) -> str:
|
||||
return self.places.get(name, name)
|
||||
|
||||
def group(self, name: str) -> str:
|
||||
return self.groups.get(name, name)
|
||||
|
||||
def _pairs(self) -> list[tuple[str, str]]:
|
||||
"""按英文原名长度降序:先替换长的,避免 `Ral` 抢在 `Ral Fastenhatchets` 前面。
|
||||
|
||||
顺序很重要:全名 → 单独的姓 → 单独的名。换掉全名后正文里已无拉丁字母,
|
||||
后面两步不会再动它。
|
||||
"""
|
||||
full = list(self.people.items()) + list(self.places.items()) + list(self.groups.items())
|
||||
full.sort(key=lambda kv: -len(kv[0]))
|
||||
tail = list(self.surnames.items()) + list(self.stems.items())
|
||||
tail.sort(key=lambda kv: -len(kv[0]))
|
||||
return full + tail
|
||||
|
||||
def render(self, text: str) -> str:
|
||||
"""把已有正文里的英文专名换成中文,并收拾替换后多余的空格。"""
|
||||
for source, target in self._pairs():
|
||||
text = re.sub(rf"(?<![A-Za-z]){re.escape(source)}(?![A-Za-z])", target, text)
|
||||
# 种族:既要处理枚举值 EAGLE_MAN,也要处理正文里的 Yeti/Eagle_man
|
||||
for source, target in sorted(self.races.items(), key=lambda kv: -len(kv[0])):
|
||||
text = re.sub(rf"(?<![A-Za-z_]){re.escape(source)}(?![A-Za-z_])", target, text)
|
||||
# 地点类型是小写短语(forest retreat、dark pits)
|
||||
for source, target in sorted(glossary.SITE_TYPES.items(), key=lambda kv: -len(kv[0])):
|
||||
text = re.sub(rf"(?<![A-Za-z]){re.escape(source)}(?![A-Za-z])", target, text,
|
||||
flags=re.IGNORECASE)
|
||||
return tidy_spaces(text)
|
||||
|
||||
def accepted(self) -> set[str]:
|
||||
"""译名集合:事实校验要把它们当已知名字,否则会把译文当成凭空编造。"""
|
||||
out: set[str] = set()
|
||||
for target in [*self.people.values(), *self.places.values(), *self.groups.values()]:
|
||||
out.add(target)
|
||||
out.update(re.split(r"[·()「」,、]", target))
|
||||
return {name for name in out if name}
|
||||
|
||||
def coverage(self) -> str:
|
||||
rows = [
|
||||
(
|
||||
f"译名覆盖:人物 {len(self.people)} 个|地点 {len(self.places)} 个|"
|
||||
f"势力 {len(self.groups)} 个"
|
||||
),
|
||||
]
|
||||
if self.pending:
|
||||
rows.append(f"待译(原样保留){len(self.pending)} 个:" + "、".join(self.pending[:12]))
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
def tidy_spaces(text: str) -> str:
|
||||
"""去掉中日韩字符之间被替换后留下的空格:`拉尔 说` → `拉尔说`。
|
||||
|
||||
英文专名之间、以及英文与汉字之间的空格保留(`Ral Fastenhatchets` 用得上)。
|
||||
"""
|
||||
text = re.sub(rf"(?<=[{_CJK}])[ \t]+(?=[{_CJK}])", "", text)
|
||||
text = re.sub(rf"(?<=[{_CJK}])[ \t]+(?=[,。、;:!?)】」])", "", text)
|
||||
text = re.sub(rf"(?<=[(【「])[ \t]+(?=[{_CJK}])", "", text)
|
||||
return text
|
||||
|
||||
|
||||
def title_form(value: str) -> str:
|
||||
"""YETI → Yeti、EAGLE_MAN → Eagle_man、fastenhatchets → Fastenhatchets。
|
||||
|
||||
两个坑都踩过:
|
||||
- 只改首字母不转小写,`YETI` 会得到 `YETI`,永远匹配不上正文里的 `Yeti`;
|
||||
- 按下划线逐段首字母大写,`EAGLE_MAN` 会得到 `Eagle_Man`,
|
||||
而史料渲染惯例是 `Eagle_man`(见 legends.pretty)。
|
||||
"""
|
||||
lowered = value.lower()
|
||||
return lowered[:1].upper() + lowered[1:]
|
||||
|
||||
|
||||
def build(world: World) -> Translator:
|
||||
"""按世界里的实际名字建翻译器。"""
|
||||
translator = Translator()
|
||||
# 名与姓都单独收一份:正文里大量使用简称("Ral")和对白里只呼姓("Fastenhatchets")
|
||||
translator.stems = {title_form(en): zh for en, zh in glossary.STEMS.items()}
|
||||
translator.surnames = {title_form(en): zh for en, zh in glossary.SURNAMES.items()}
|
||||
translator.races = {}
|
||||
for en, zh in glossary.RACES.items():
|
||||
translator.races[en] = zh
|
||||
translator.races[title_form(en)] = zh
|
||||
# 逐段大写的写法(Eagle_Man)也登记:模型两种都可能写
|
||||
translator.races["_".join(p[:1].upper() + p[1:].lower() for p in en.split("_"))] = zh
|
||||
for figure in world.figures.values():
|
||||
raw = figure.name
|
||||
if not raw:
|
||||
continue
|
||||
translated = translate_person(raw)
|
||||
shown = _display(raw)
|
||||
if translated:
|
||||
translator.people[shown] = translated
|
||||
translator.people[raw] = translated
|
||||
elif _WORD_SPLIT.split(shown.lower())[0] in glossary.STEMS:
|
||||
# 名字能读、姓氏没词条:这是真正需要补的缺口,登记下来
|
||||
translator.pending.append(shown)
|
||||
for site in world.sites.values():
|
||||
raw = site.name
|
||||
if not raw:
|
||||
continue
|
||||
translated = translate_place(raw)
|
||||
shown = _display(raw)
|
||||
if translated:
|
||||
translator.places[shown] = translated
|
||||
translator.places[raw] = translated
|
||||
for entity in world.entities.values():
|
||||
raw = entity.name
|
||||
if not raw:
|
||||
continue
|
||||
translated = translate_group(raw)
|
||||
shown = _display(raw)
|
||||
if translated:
|
||||
translator.groups[shown] = translated
|
||||
translator.groups[raw] = translated
|
||||
return translator
|
||||
|
||||
|
||||
_CACHE: dict[int, tuple[World, Translator]] = {}
|
||||
|
||||
|
||||
def for_world(world: World) -> Translator:
|
||||
"""带缓存的建立器:一次运行里反复渲染素材,不能每次都重建 4 万个人名。"""
|
||||
hit = _CACHE.get(id(world))
|
||||
if hit is not None and hit[0] is world:
|
||||
return hit[1]
|
||||
translator = build(world)
|
||||
_CACHE[id(world)] = (world, translator)
|
||||
return translator
|
||||
|
||||
|
||||
def _display(raw: str) -> str:
|
||||
"""史料的程序化名全小写,渲染层会做首字母大写;译名要按展示形式建索引。"""
|
||||
from dfannals.legends import pretty
|
||||
|
||||
return pretty(raw)
|
||||
+4
-3
@@ -12,7 +12,7 @@ from dataclasses import dataclass, replace
|
||||
from pathlib import Path
|
||||
|
||||
from dfannals import episodes as ep_mod
|
||||
from dfannals import gate
|
||||
from dfannals import gate, localize
|
||||
from dfannals.cast import collect_rows
|
||||
from dfannals.chronicle import cjk_length
|
||||
from dfannals.config import (
|
||||
@@ -194,12 +194,13 @@ def render_material(
|
||||
"",
|
||||
"### 写作要求",
|
||||
"",
|
||||
f"- 本集 {CHAPTER_MIN_CHARS}–{CHAPTER_MAX_CHARS} 字,中文正文,专名保留英文。",
|
||||
f"- 本集 {CHAPTER_MIN_CHARS}–{CHAPTER_MAX_CHARS} 字,中文正文,人名地名用上面给出的中文译名。",
|
||||
"- 贴着主角写:他们的目标、算计、得失做主语;对手要是个具体的人。",
|
||||
"- 性别按上面卡片写,不要默认“他”;史料未载性别时用名字或身份称呼。",
|
||||
"- 史料之外的世界大事不要写;只有影响到主角时才提一句。",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
# 专名本土化:素材里的英文原名换成中文译名,让模型直接用中文写
|
||||
return localize.for_world(world).render("\n".join(lines))
|
||||
|
||||
|
||||
def build_messages(
|
||||
|
||||
Reference in New Issue
Block a user