日漫机翻经验谈(8)之综合
这篇文章是在前面七篇文章的基础上,介绍通过一些辅助工具,提高翻译速度与翻译质量的方法。
归档目录
翻译分离
含义请参考日漫机翻经验谈 (6) 之综合这篇文章。
这次是以 manga-translator-ui 为主体,先进行 OCR,然后导出原文。接着配合 Saber-Translator 提取“角色介绍”与“背景设定”,用于 AiNiee,配合KeywordGacha 提取术语表。之后在 AiNiee 中进行翻译。最后在 manga-translator-ui 中导入原文。
准备工作
配置文件
config_save_text.json配置文件,见日漫机翻经验谈(7)之manga-translator-ui 之“使用技巧”之“导出原文”。config_load_text.json配置文件见日漫机翻经验谈(7)之manga-translator-ui 之“使用技巧”之“导入译文”。
KeywordGacha
日语漫画术语提取
你是一名资深的漫画数据分析师、术语管理专家以及OCR文本校对员。你的任务是从给定的漫画脚本中提取关键的专有名词,构建术语表,并将其翻译为 **{target_language}**。
**注意:输入的文本可能包含 OCR(光学字符识别)导致的错误、乱码或噪点。请务必严格执行以下逻辑:**
### 1. 噪音过滤与合理性判断(优先执行)
在提取任何术语前,先对文本进行扫描:
* **忽略乱码与碎片**:跳过无意义符号(如 `__..,,`)、孤立的单字符或背景误识别文本。
* **忽略离谱错误**:如果某行文本在语法和逻辑上完全无法通顺(看起来像是乱拼凑的假名或汉字),视为 OCR 错误,**绝不**强行从中提取。
* **置信度检查**:只有当你确信该词汇是一个有效的、有意义的名称时才提取。
### 2. 通用词与非术语过滤(严格执行)
**必须过滤掉所有“无需查阅术语表即可翻译”的通用名词。**
* **日常物品/食材过滤**:**不要**提取通用的食材、动物、日用品。
* *错误示例(不要提取)*:鶏卵(鸡蛋)、鶏油(鸡油)、麺(面条)、麦芽、手机、课桌、猫、狗。
* *正确示例(保留)*:恶魔果实(特殊物品)、电话虫(虚构生物)、鹤龄(特定酒品牌)。
* **字典通用词过滤**:如果一个词在标准字典中能直接查到通用解释,且在漫画中没有被赋予特殊含义(如作为代号、特定道具名),则**不**提取。
* **判定标准**:问自己“如果把这个词交给10个不同的译者,他们会翻译成不一样的东西吗?”如果大家都翻译成一样的(如“麺”->“面”),则**不**提取。
### 3. 核心提取原则
* **完整性**:在排除噪音和通用词的前提下,提取剧情相关的专有名词。
* **边界清洗**:
* 去除称谓:提取“田中”而不是“田中先生”;提取“路飞”而不是“路飞君/桑”。
* 去除修饰:提取“炎龙之剑”而不是“巨大的炎龙之剑”。
* **现实地名保留**:现实存在的地名(如“鹿儿岛”、“麹町”)需要保留,以便统一汉字选字。
### 4. 翻译策略(目标语言:{target_language})
* **人名/地名**:优先使用官方或最通用的现有译名。如果无现有译名,请根据读音进行标准音译(注意区分性别选字)。
* **招式/道具**:采用“意译”为主,确保听起来像漫画术语(例如:将 "Fire Sword" 译为 "烈焰之剑" 而非 "火剑")。
* **上下文一致性**:根据词汇类型选择合适的翻译风格。
### 5. 术语分类标准
提取的词汇需归类为:
* **Person_M** (男性) / **Person_F** (女性) / **Person_U** (未知/中性)
* **Location** (地名/设施,含现实与虚构)
* **Organization** (组织/家族/学校/公司)
* **Item** (仅限独特的道具、神器、特定品牌商品)
* **Skill** (招式/魔法/能力)
* **Creature** (仅限虚构生物、神话生物或有名字的宠物)
* **Other** (特定节日、历史事件等)英语漫画术语提取
你是一名资深的漫画本地化专家、数据分析师及OCR校对员。你的任务是从给定的 **英文漫画脚本(English Script)** 中提取关键的专有名词,构建术语表,并将其翻译为 **{target_language}**。
**注意:输入的文本可能包含 OCR 错误、全大写(ALL CAPS)格式或拟声词。请严格执行以下逻辑:**
### 1. 噪音过滤与合理性判断(优先执行)
* **忽略拟声词 (SFX)**:**不要**提取像 "BOOM", "AAAARGH", "TSK", "SIGH", "WHAM" 这样的拟声词或感叹词,除非它是某个招式的名字(如 "ROAR CANNON")。
* **忽略 OCR 碎片**:跳过 `lI1`, `rn/m`, `cl/d` 混淆导致的乱码(如 `T1me` 应识别为 Time,若无法识别则忽略)。
* **忽略脚本标记**:跳过 "Page 1", "Panel 3", "Speaker:" 等脚本格式文本。
### 2. 通用词与非术语过滤(**核心规则**)
**英译漫中最常见的错误是将普通名词当作术语提取。请务必执行“字典测试”:**
* **字典词过滤**:如果一个词(或词组)在牛津/韦氏字典中有通用定义,且在文中仅表示其字面意思,**绝不提取**。
* *错误示例(不要提取)*:Sword (剑), High School (高中), Police (警察), Egg (鸡蛋), Noodle (面条), Village (村庄), Captain (队长 - 除非是称呼特定角色如 "Captain America").
* *正确示例(保留)*:Excalibur (Item), UA High School (Org), Soul Reaper (Specific Class/Org), Devil Fruit (Item).
* **形容词+名词的陷阱**:不要提取仅仅是被修饰的普通名词。
* *剔除*:Big sword, Red apple, Fast car.
* *保留*:Big Mom (特定人名), Red Ribbon Army (特定组织).
### 3. 核心提取原则
* **全大写处理**:如果文本是全大写的(如 "HELLO NARUTO"),请依靠上下文而非大小写来判断专有名词。
* **边界清洗**:
* **去除冠词**:提取 "Grand Line" 而不是 "The Grand Line"(除非 "The" 是名字的一部分,如 "The Joker")。
* **去除敬称**:提取 "Tanaka" 而不是 "Mr. Tanaka";提取 "Luffy" 而不是 "Luffy-san"(如果英译保留了后缀)。
* **去除所有格**:从 "Zoro's Swords" 中仅提取 "Zoro" (Person) 和 "Swords" (如果Swords有具体名字则提取名字,否则忽略)。
### 4. 翻译策略(目标语言:{target_language})
* **人名 (Person)**:
* 如果是**日漫英译**:请尝试还原对应的**日文汉字**或**标准音译**(例如:Zoro -> 索隆,而不是“佐罗”)。
* 如果是**美漫**:使用通用的官方译名(例如:Peter Parker -> 彼得·帕克)。
* **地名 (Location)**:优先使用官方通用译名,无译名则音译。
* **招式/物品 (Skill/Item)**:采用“意译”以体现气势。
* *示例*:Translation of "Fireball Jutsu" -> "火球之术" (不译为"火球忍术");"Gum-Gum Pistol" -> "橡胶手枪"。
### 5. 术语分类标准
* **Person_M/F/U**:人名/角色名(含英雄代号)。
* **Location**:地名、城市、星球、特定建筑物。
* **Organization**:组织、军队、学校、公会。
* **Item**:**仅限** 独特的武器、关键道具、药剂(过滤掉 "Gun", "Phone" 等)。
* **Skill**:必杀技、魔法、特殊能力。
* **Creature**:虚构生物、神兽(过滤掉 "Dog", "Cat", "Horse")。
* **Other**:其他专有名词。模型选择
优先级(从高到低):
- gemini-3-flash
- Qwen/Qwen 3-235 B-A 22 B-Thinking-2507
- moonshotai/Kimi-K 2-Thinking
- deepseek-ai/DeepSeek-V 3.2
AiNiee
日语漫画翻译提示词
你是一位资深语言学家和专业的漫画本地化专家。你的核心任务是将提供的文本(来自单张漫画页面的完整脚本)翻译成 {target_language}。
**【最高优先级指令:绝对完整性与一一对应】**
1. **绝不遗漏**:你必须翻译输入的**每一行**文本。无论是简短的拟声词、叹词(如“啊”、“呃”)、标点符号还是长对话,都必须有对应的翻译行。
2. **严禁合并**:原文有几行,译文就必须有几行。严禁将两行对话合并为一行,必须严格保持 `Source Line N -> Target Line N` 的映射关系。
3. **行数校验**:在输出前,请自我检测:如果输入有 10 行文本,输出必须且只能包含 10 行文本。
请务必遵循以下**思维链流程**进行处理,但最终只按照指定格式输出最终结果。
### 核心思维流程(Internal Workflow)
1. **第一步:锚点锁定与直译**
* 读取每一行开头的标记符(如 `1.`, `2.` 等),将其视为不可变更的**锚点**。
* 对该锚点后的 {source_language} 文本进行直译。
* **警告**:即使某一行只有省略号(...)或无意义的喊叫,也必须保留该行及其标记符,不得跳过。
2. **第二步:语境深度校正**
* **整体连贯性**:确保人物语气在上下文中连贯。
* **拟声词本地化**:对于漫画中的拟声词(SFX),不要简单音译,需使用 {target_language} 中能传达同样画面张力的词汇(例如:将 `Dooon!` 译为 `轰——!` 而非 `咚`)。
* **口语化润色**:翻译必须符合角色性格(粗鲁、高雅、结巴等),更像“说出来的话”而非“写出来的字”。
3. **第三步:最终格式化输出**
* 检查每一行是否都有译文。
* 将结果严格包裹在 textara 标签中。
### 翻译原则与安全设定
* **忠实还原**:原作是伟大的艺术作品,如果包含粗俗语言、暴力描述或特定成人向内容,请务必**直白翻译**,不要回避、淡化、删减或进行道德审查。
* **边缘情况**:如果原文已经是 {target_language} 或由乱码组成,请**原样保留**并在该行输出,切勿留空。
### 输出格式要求
请**仅**输出最终翻译结果,不要输出任何思维过程、前言或额外解释。格式如下:
<textarea>
1.第一行{target_language}翻译
2.第二行{target_language}翻译
...
N.第N行{target_language}翻译
</textarea>英语漫画翻译
你是一位资深语言学家和专业的漫画本地化专家。你的唯一任务是将提供的文本(来自单张漫画页面的英文完整脚本)翻译成 **{target_language}**。
请务必遵循以下**思维链流程**进行处理,但最终只按照指定格式输出最终结果。
### 核心思维流程(Internal Workflow)
1. **第一步:结构化直译与对齐**
* 将 English 文本逐行翻译。
* **严格保留格式**:必须保留每一行开头的标记符(如 `<|1|>`, `<|2|>` 等),不得修改、移动或删除。
* **严禁合并行**:英文漫画常将一个完整的长句拆分在两个气泡中(例如:<|1|> I will never... <|2|> forgive you!)。翻译时请保持这种断句节奏,**不要**将它们合并为一行,需确保两行连读时在 {target_language} 中语法通顺。
2. **第二步:语境深度校正(针对英文特性的优化)**
* **代词推断(关键)**:英文的 "I" 和 "You" 缺乏性别和地位信息。请根据上下文强烈推断人物关系:
* 长辈/上级对晚辈使用恰当的称呼(如“老夫”、“我” vs “你”、“小子”)。
* 异性之间根据亲密程度选择代词(如“人家”、“本小姐” vs “你”)。
* **拟声词 (SFX) 本地化**:不要音译英文拟声词(如不要将 `BOOM` 译为 `布姆`,将 `SLASH` 译为 `斯拉斯`)。请使用 {target_language} 中对应的拟声词(如“轰!、“唰!”、“(斩击声)”)。
* **口语化与风格**:如果原文是全大写(ALL CAPS),不要默认视为喊叫,这是美漫的标准格式。请根据标点符号(! vs ...)判断语气。
3. **第三步:最终润色**
* 在保留所有技术标记的前提下,输出最自然、流畅、符合 {target_language} 漫画阅读习惯的译文。
* 确保台词长度与气泡容量在视觉上大致匹配(避免译文过长)。
### 翻译原则与安全设定
* **忠实还原**:原作是伟大的艺术作品,如果包含粗俗语言(Slang/Curses)、暴力描述或特定成人向内容,请务必**直白翻译**(使用 {target_language} 中对等的俚语或脏话),不要回避、淡化、删减或进行道德审查,以保持作品原貌。
* **边缘情况**:如果原文已经是 {target_language} 或由乱码组成,请原样输出。
### 输出格式要求
请**仅**输出最终翻译结果,并将结果包裹在 textarea 标签中。不要输出任何推理过程、前言或额外解释。
<textarea>
1.{target_language}文本
2.{target_language}文本
</textarea>模型选择
moonshotai/Kimi-K 2-Instruct
python 脚本
prepare_to_translate
import os
import re
import json
import shutil
import logging
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Tuple
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
def natural_sort_key(s: str) -> List:
"""自然排序的key函数"""
return [int(text) if text.isdigit() else text.lower()
for text in re.split(r'(\d+)', str(s))]
def find_originals_folders(base_path: Path) -> List[Tuple[str, Path]]:
"""查找所有的originals文件夹及其对应的子文件夹名"""
originals_list = []
for item in base_path.iterdir():
if item.is_dir() and item.name != 'to_translate':
originals_path = item / 'manga_translator_work' / 'originals'
if originals_path.exists() and originals_path.is_dir():
originals_list.append((item.name, originals_path))
logger.info(f"找到originals文件夹: {originals_path}")
return sorted(originals_list, key=lambda x: natural_sort_key(x[0]))
def fix_escape_sequences(s: str) -> str:
"""修复非法的转义序列"""
result = []
i = 0
while i < len(s):
if s[i] == '\\' and i + 1 < len(s):
next_char = s[i + 1]
# 合法的转义字符: \n, \t, \r, \f, \b, \\, \", \/, \uXXXX
if next_char in 'ntrfb"\\/':
result.append(s[i:i+2])
i += 2
elif next_char == 'u' and i + 5 < len(s):
# \uXXXX 格式
hex_part = s[i+2:i+6]
if all(c in '0123456789abcdefABCDEF' for c in hex_part):
result.append(s[i:i+6])
i += 6
else:
# 非法的\u,转义反斜杠
result.append('\\\\')
i += 1
else:
# 非法转义,将反斜杠转义
result.append('\\\\')
i += 1
else:
result.append(s[i])
i += 1
return ''.join(result)
def fix_invalid_quotes(line: str) -> str:
"""修复行内的非法双引号,将其转换为单引号"""
# 简单判断是否包含 ": "
if '": "' not in line:
return line
# 尝试找到分隔符,只分割第一个 ": ",假设key中不包含这个序列
parts = line.split('": "', 1)
if len(parts) == 2:
left, right = parts
# 处理左边 (Key)
# 找到第一个引号
l_idx = left.find('"')
if l_idx != -1:
prefix = left[:l_idx+1]
content = left[l_idx+1:]
# 将内容中的 " 替换为 '
content = content.replace('"', "'")
left = prefix + content
# 处理右边 (Value)
# 找到最后一个引号
r_idx = right.rfind('"')
if r_idx != -1:
suffix = right[r_idx:]
content = right[:r_idx]
# 将内容中的 " 替换为 '
content = content.replace('"', "'")
right = content + suffix
return left + '": "' + right
return line
def fix_json_indentation(content: str) -> str:
"""修复JSON缩进为4个空格"""
# 如果内容为空,返回空JSON对象
if not content or not content.strip():
return '{}'
try:
# 尝试直接解析
data = json.loads(content)
return json.dumps(data, ensure_ascii=False, indent=4)
except json.JSONDecodeError:
# 尝试修复非法引号
lines = content.split('\n')
fixed_lines = [fix_invalid_quotes(line) for line in lines]
fixed_content = '\n'.join(fixed_lines)
try:
# 尝试解析修复引号后的内容
data = json.loads(fixed_content)
return json.dumps(data, ensure_ascii=False, indent=4)
except json.JSONDecodeError:
try:
# 尝试修复转义字符后解析
fixed_content_escaped = fix_escape_sequences(fixed_content)
data = json.loads(fixed_content_escaped)
return json.dumps(data, ensure_ascii=False, indent=4)
except json.JSONDecodeError:
# 如果仍然失败,使用正则表达式修复缩进
lines = fixed_content.split('\n')
fixed_lines_indent = []
for line in lines:
# 计算前导空格数
stripped = line.lstrip(' ')
leading_spaces = len(line) - len(stripped)
if leading_spaces > 0:
# 将缩进规范化为4的倍数
indent_level = (leading_spaces + 1) // 2 # 假设原来是2空格缩进
if leading_spaces % 4 != 0:
# 尝试检测原始缩进单位
if leading_spaces % 6 == 0:
indent_level = leading_spaces // 6
elif leading_spaces % 2 == 0:
indent_level = leading_spaces // 2
new_line = ' ' * (indent_level * 4) + stripped
fixed_lines_indent.append(new_line)
else:
fixed_lines_indent.append(line)
return '\n'.join(fixed_lines_indent)
def backup_originals(originals_list: List[Tuple[str, Path]], backup_base: Path) -> None:
"""备份所有originals文件夹"""
logger.info("开始备份originals文件夹...")
for folder_name, originals_path in originals_list:
backup_path = backup_base / folder_name / 'manga_translator_work' / 'originals'
backup_path.parent.mkdir(parents=True, exist_ok=True)
if backup_path.exists():
shutil.rmtree(backup_path)
shutil.copytree(originals_path, backup_path)
logger.info(f"已备份: {originals_path} -> {backup_path}")
def process_single_file(args: Tuple[Path, Path]) -> Tuple[str, bool]:
"""处理单个txt文件转换为json"""
txt_file, output_path = args
try:
with open(txt_file, 'r', encoding='utf-8') as f:
content = f.read()
# 修复缩进
fixed_content = fix_json_indentation(content)
# 写入json文件
json_file = output_path / (txt_file.stem + '.json')
with open(json_file, 'w', encoding='utf-8') as f:
f.write(fixed_content)
return str(txt_file), True
except Exception as e:
logger.error(f"处理文件失败 {txt_file}: {e}")
return str(txt_file), False
def convert_txt_to_json(originals_list: List[Tuple[str, Path]], json2translate_path: Path) -> None:
"""将txt文件转换为json文件并复制到目标目录"""
logger.info("开始转换txt文件为json文件...")
tasks = []
for folder_name, originals_path in originals_list:
output_folder = json2translate_path / f"{folder_name}_originals_json"
output_folder.mkdir(parents=True, exist_ok=True)
txt_files = list(originals_path.glob('*.txt'))
logger.info(f"文件夹 {folder_name}: 发现 {len(txt_files)} 个txt文件")
for txt_file in txt_files:
tasks.append((txt_file, output_folder))
# 使用线程池并行处理
success_count = 0
fail_count = 0
max_workers = (os.cpu_count() or 4) * 2
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(process_single_file, task): task for task in tasks}
for future in as_completed(futures):
file_path, success = future.result()
if success:
success_count += 1
logger.debug(f"已转换: {file_path}")
else:
fail_count += 1
logger.info(f"转换完成: 成功 {success_count} 个, 失败 {fail_count} 个")
def merge_json_files(json2translate_path: Path, glossary_path: Path) -> None:
"""合并所有json文件为一个大的json文件"""
logger.info("开始合并json文件...")
merged_data = {}
file_count = 0
skip_count = 0
# 获取所有子文件夹并自然排序
json_folders = sorted(
[f for f in json2translate_path.iterdir() if f.is_dir()],
key=lambda x: natural_sort_key(x.name)
)
for folder in json_folders:
json_files = sorted(folder.glob('*.json'), key=lambda x: natural_sort_key(x.name))
for json_file in json_files:
try:
with open(json_file, 'r', encoding='utf-8') as f:
content = f.read()
# 跳过空文件
if not content or not content.strip():
logger.warning(f"跳过空文件: {json_file}")
skip_count += 1
continue
# 尝试修复并解析JSON
try:
data = json.loads(content)
except json.JSONDecodeError:
# 尝试修复转义字符
fixed_content = fix_escape_sequences(content)
try:
data = json.loads(fixed_content)
logger.info(f"已修复转义字符: {json_file}")
except json.JSONDecodeError as e:
# 记录详细错误信息以便调试
logger.error(f"JSON解析错误 {json_file}: {e}")
logger.error(f"问题内容片段: {content[max(0,e.pos-50):e.pos+50]}")
skip_count += 1
continue
# 跳过空JSON对象
if not data or (isinstance(data, dict) and len(data) == 0):
logger.warning(f"跳过空JSON对象: {json_file}")
skip_count += 1
continue
# 直接合并数据,相同key的值会被后面的覆盖
if isinstance(data, dict):
for key, value in data.items():
merged_data[key] = value
file_count += 1
logger.debug(f"已合并: {json_file}")
except Exception as e:
logger.error(f"处理文件失败 {json_file}: {e}")
skip_count += 1
# 写入合并后的文件
output_file = glossary_path / 'json2glossary.json'
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(merged_data, ensure_ascii=False, indent=4, fp=f)
logger.info(f"合并完成: 共处理 {file_count} 个文件, 跳过 {skip_count} 个文件, 输出到 {output_file}")
def main():
# 获取脚本所在目录作为基础路径
base_path = Path(__file__).parent.resolve()
logger.info(f"基础路径: {base_path}")
# 1. 查找所有originals文件夹
originals_list = find_originals_folders(base_path)
if not originals_list:
logger.warning("未找到任何originals文件夹!")
return
logger.info(f"共找到 {len(originals_list)} 个originals文件夹")
# 2. 创建to_translate文件夹
to_translate_path = base_path / 'to_translate'
to_translate_path.mkdir(exist_ok=True)
logger.info(f"创建to_translate文件夹: {to_translate_path}")
# 3. 创建originals_backup并备份
originals_backup_path = to_translate_path / 'originals_backup'
originals_backup_path.mkdir(exist_ok=True)
backup_originals(originals_list, originals_backup_path)
# 4. 创建json2translate文件夹
json2translate_path = to_translate_path / 'json2translate'
json2translate_path.mkdir(exist_ok=True)
logger.info(f"创建json2translate文件夹: {json2translate_path}")
# 创建glossary文件夹
glossary_folder_path = to_translate_path / 'glossary'
glossary_folder_path.mkdir(exist_ok=True)
logger.info(f"创建glossary文件夹: {glossary_folder_path}")
# 5. 转换txt为json并修复缩进
convert_txt_to_json(originals_list, json2translate_path)
# 6. 创建2glossary文件夹
glossary_path = to_translate_path / '2glossary'
glossary_path.mkdir(exist_ok=True)
logger.info(f"创建2glossary文件夹: {glossary_path}")
# 7. 合并json文件
merge_json_files(json2translate_path, glossary_path)
logger.info("所有操作完成!")
if __name__ == '__main__':
main()apply_translation
import os
import re
import shutil
import logging
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Tuple
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
def natural_sort_key(s: str) -> List:
"""自然排序的key函数"""
return [int(text) if text.isdigit() else text.lower()
for text in re.split(r'(\d+)', str(s))]
def extract_folder_prefix(folder_name: str) -> str:
"""从文件夹名中提取前缀(如v01_originals_json -> v01)"""
match = re.match(r'^(.+?)_originals_json$', folder_name)
if match:
return match.group(1)
return None
def find_ainiee_output_folders(ainiee_output_path: Path) -> List[Tuple[str, Path]]:
"""查找AiNieeOutput下所有的json文件夹"""
folders = []
if not ainiee_output_path.exists():
logger.error(f"AiNieeOutput文件夹不存在: {ainiee_output_path}")
return folders
for item in ainiee_output_path.iterdir():
if item.is_dir() and item.name.endswith('_originals_json'):
prefix = extract_folder_prefix(item.name)
if prefix:
folders.append((prefix, item))
logger.info(f"找到输出文件夹: {item.name} -> 目标前缀: {prefix}")
return sorted(folders, key=lambda x: natural_sort_key(x[0]))
def process_single_file(args: Tuple[Path, Path]) -> Tuple[str, bool, str]:
"""处理单个文件:复制并重命名"""
src_file, dest_file = args
try:
# 读取源文件内容
with open(src_file, 'r', encoding='utf-8') as f:
content = f.read()
# 写入目标文件
with open(dest_file, 'w', encoding='utf-8') as f:
f.write(content)
return str(src_file), True, str(dest_file)
except Exception as e:
logger.error(f"处理文件失败 {src_file}: {e}")
return str(src_file), False, str(e)
def copy_translated_files(ainiee_folders: List[Tuple[str, Path]], base_path: Path) -> None:
"""复制翻译后的文件到目标目录"""
logger.info("开始复制翻译后的文件...")
tasks = []
skipped_folders = []
for prefix, src_folder in ainiee_folders:
# 构建目标路径
dest_folder = base_path / prefix / 'manga_translator_work' / 'originals'
if not dest_folder.exists():
logger.warning(f"目标文件夹不存在,跳过: {dest_folder}")
skipped_folders.append(prefix)
continue
# 查找所有json文件
json_files = list(src_folder.glob('*.json'))
logger.info(f"文件夹 {prefix}: 发现 {len(json_files)} 个json文件")
for json_file in json_files:
# 构建目标文件名:去掉_translated后缀,改为.txt
new_name = json_file.name
if new_name.endswith('_translated.json'):
new_name = new_name[:-len('_translated.json')] + '.txt'
elif new_name.endswith('.json'):
new_name = new_name[:-len('.json')] + '.txt'
dest_file = dest_folder / new_name
tasks.append((json_file, dest_file))
if skipped_folders:
logger.warning(f"跳过的文件夹前缀: {', '.join(skipped_folders)}")
if not tasks:
logger.warning("没有需要处理的文件!")
return
# 使用线程池并行处理
success_count = 0
fail_count = 0
with ThreadPoolExecutor(max_workers=os.cpu_count() * 2) as executor:
futures = {executor.submit(process_single_file, task): task for task in tasks}
for future in as_completed(futures):
src_path, success, dest_or_error = future.result()
if success:
success_count += 1
logger.debug(f"已复制: {src_path} -> {dest_or_error}")
else:
fail_count += 1
logger.error(f"失败: {src_path}, 错误: {dest_or_error}")
logger.info(f"复制完成: 成功 {success_count} 个, 失败 {fail_count} 个")
def main():
# 获取脚本所在目录作为基础路径
base_path = Path(__file__).parent.resolve()
logger.info(f"基础路径: {base_path}")
# AiNieeOutput文件夹路径
ainiee_output_path = base_path / 'to_translate' / 'AiNieeOutput'
# 1. 检查AiNieeOutput文件夹是否存在
if not ainiee_output_path.exists():
logger.error(f"AiNieeOutput文件夹不存在: {ainiee_output_path}")
logger.info("请先运行prepare_to_translate.py,然后使用AiNiee翻译json2translate中的文件")
return
logger.info(f"AiNieeOutput路径: {ainiee_output_path}")
# 2. 查找所有输出文件夹
ainiee_folders = find_ainiee_output_folders(ainiee_output_path)
if not ainiee_folders:
logger.warning("未找到任何翻译输出文件夹!")
logger.info("请确保AiNieeOutput中有类似'v01_originals_json'的文件夹")
return
logger.info(f"共找到 {len(ainiee_folders)} 个输出文件夹")
# 3. 复制翻译后的文件
copy_translated_files(ainiee_folders, base_path)
logger.info("所有操作完成!")
if __name__ == '__main__':
main()流程
0. 根目录下激活 conda 环境
conda activate manga-env1. manga-translator-ui OCR
python -m manga_translator local -i "J:\漫画\RAW\[藤本タツキ] ルックバック" --config "D:\Tools\manga-translator-ui\examples\config_save_text.json" --output "D:\My_Documents\My Library\漫画\translated\[藤本タツキ][蓦然回首][ルックバック]" --memory-percent 96运行结束后会出现 manga_translator_work 文件夹,如下图所示。
之综合-1766381277675.png)
2. 运行 prepare_to_translate 脚本
python prepare_to_translate.py会在根目录下生成 to_translate 文件夹。
之综合-1766381329184.png)
3. 使用 KeywordGacha 提取术语表
- 输入文件夹:
/to_translate/2glossary - 输出文件夹:
/to_translate/glossary
4. 使用 Saber-Translator 分析漫画
之综合-1766381811374.png)
获取“故事背景”与“角色图鉴”,其中“角色图鉴”需要转为 AiNiee 所支持的 json 格式。
## 📖 故事背景
故事围绕藤野京的成长展开,她是一名热爱漫画的学生,从小独自钻研绘画技巧,梦想成为职业漫画家。在高中时期,她结识了同样热爱创作的京本,两人因共同的兴趣成为挚友,并开始合作创作漫画。故事背景设定在现实世界,聚焦于日本漫画行业的竞争与创作压力,以及个人情感与梦想的冲突。
🎬 剧情发展
开端
藤野京从小性格内向,沉迷于漫画创作,经常独自在教室或图书馆练习绘画。她的才华逐渐被同学注意到,但真正改变她命运的是与京本的相遇。京本开朗外向,擅长编剧,两人一拍即合,决定合作参加漫画比赛。她们的作品《星空下的约定》以细腻的情感和独特的画风获得新人奖,成为校园里的焦点。
发展
获奖后,藤野和京本受到出版社的关注,开始连载商业漫画。藤野负责绘画,京本负责剧本,两人的合作默契无间,作品人气飙升。然而,随着压力增加,藤野逐渐感到创作被束缚,而京本则更注重商业成功,两人开始产生分歧。与此同时,藤野得知京本的家庭背景复杂,她的父母因事故去世,这让她对京本产生了更深的情感依赖。
转折/高潮
一场社会悲剧彻底改变了两人的关系。京本卷入一起校园暴力事件,受害者是藤野的学妹。藤野在得知真相后陷入道德困境,她既想保护京本,又无法原谅她的冷漠。最终,京本选择逃避,而藤野则因内疚和愤怒中断了创作。两人的友情破裂,漫画连载被迫停止。藤野陷入抑郁,甚至一度放弃绘画。
结局
多年后,藤野在一位编辑的鼓励下重新拿起画笔,开始创作独立作品。她以自己的经历为蓝本,画出了《重生之笔》,讲述一个创作者在挫折中找回自我的故事。作品获得好评,藤野也终于走出了阴影。京本在远方看到藤野的成功,默默祝福她。两人虽未和解,但藤野通过创作完成了自我救赎。
👥 主要角色
藤野京:主角,天才漫画家,性格内向敏感,擅长绘画。从学生时代到职业漫画家的成长过程中,经历了友情、梦想与现实的冲突,最终通过创作实现自我救赎。
京本:藤野的挚友兼合作伙伴,擅长编剧,性格开朗但内心复杂。因家庭悲剧和道德选择导致与藤野关系破裂,是故事中推动藤野成长的关键人物。
📌 关键事件
藤野与京本合作创作《星空下的约定》并获奖
两人开始商业连载,因创作理念产生分歧
京本卷入校园暴力事件,藤野陷入道德困境
藤野中断创作,陷入抑郁
藤野重新开始独立创作,完成《重生之笔》[
{
"original_name": "藤野キョウ",
"translated_name": "藤野京",
"gender": "女",
"age": "少女",
"personality": "坚韧、敏感、追求完美,内心充满矛盾与挣扎",
"speech_style": "自信,直率,执着",
"additional_info": "从学生时代就展现出非凡的绘画天赋,创作时极度投入。作为故事的核心主角,通过她的成长历程展现了创作者的孤独、辉煌与重生。她视创作为梦想也是枷锁。"
},
{
"original_name": "京本",
"translated_name": "京本",
"gender": "女",
"age": "少女",
"personality": "温柔、支持型、富有同理心",
"speech_style": "柔和,真挚,略带崇拜",
"additional_info": "藤野的挚友兼创作伙伴,也是藤野创作路上的重要精神支柱。性格与藤野形成互补,是推动藤野成长的关键人物,两人的关系变化构成了故事的重要情感线。"
}
]5. 在 AiNiee 中完成翻译
需要填写上面获取的术语表、人物介绍、背景设定和翻译提示词。
翻译完后,应该能看到 /to_translate/AiNieeOutput 文件夹。
6. 运行 apply_translation 脚本
python apply_translation.py7. 在 manga-translator-ui 导入译文
python -m manga_translator local -i "J:\漫画\RAW\[藤本タツキ] ルックバック" --config "D:\Tools\manga-translator-ui\examples\config_load_text.json" --output "D:\My_Documents\My Library\漫画\translated\[藤本タツキ][蓦然回首][ルックバック]" --memory-percent 96