新增学习资料生成器模块(learner),从权威库生成多维日语学习资料: - 拼音/假名/汉字/熟字训四类索引,带拼音↔假名↔汉字交叉跳转 - 逐字音训分类(KANJIDIC2 精确查表 + 启发式回退 + 排序键) - 音变标注体系:浊化(連濁)、半浊化、促音变(促音便)、连声(れんじょう) 独立配色 + 合并逻辑 + 音变规律说明 - 显式标注表:rendaku_marks(连用形连浊)、renjou_marks(连声) - 每索引独立例词数配置(jlearn.toml + --config) - HTML 单页应用 + 静态 HTML + PDF(playwright) 清洗工具增强: - 拼音校验器(pinyin_checker)集成到 jclean - 多音字拼音校正、ます形サ変動詞转原型 数据: - 权威库补充连声词(反応/天皇/陰陽/観音/因縁/三位/輪廻/安穏) - KANJIDIC2 音训分类表、拼音校正字典 整理 .gitignore:忽略生成产物(output/study_materials)、词典数据库、 任务运行日志、备份文件
212 lines
7.4 KiB
Python
212 lines
7.4 KiB
Python
"""
|
||
测试新的两段式待确认文件 + done 信号机制。
|
||
"""
|
||
import pytest
|
||
import tempfile
|
||
import shutil
|
||
from pathlib import Path
|
||
from pl_japanese.cleaner import TaskManager
|
||
from pl_japanese.cleaner.workflow import CleanerWorkflow
|
||
|
||
|
||
@pytest.fixture
|
||
def temp_dir():
|
||
"""每个测试独立的临时目录"""
|
||
tmp = Path(tempfile.mkdtemp())
|
||
yield tmp
|
||
shutil.rmtree(tmp, ignore_errors=True)
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def reset_pinyin_dict():
|
||
"""每个测试前重置拼音字典单例"""
|
||
from pl_japanese.cleaner.pinyin_overrides import DEFAULT_DICT
|
||
original_path = DEFAULT_DICT.source_path
|
||
original_word = DEFAULT_DICT.word_override.copy()
|
||
original_kana = DEFAULT_DICT.kana_hint.copy()
|
||
|
||
yield
|
||
|
||
DEFAULT_DICT.source_path = original_path
|
||
DEFAULT_DICT.word_override = original_word
|
||
DEFAULT_DICT.kana_hint = original_kana
|
||
|
||
|
||
def test_two_section_file_generation(temp_dir):
|
||
"""测试两段式待确认文件生成"""
|
||
tmp = temp_dir
|
||
tm = TaskManager(tasks_root=str(tmp / 'tasks'))
|
||
src = tmp / 'src.txt'
|
||
src.write_text(
|
||
'議会:ぎかい:\n' # 多音字 → 待确认
|
||
'社会:しゃかい:\n' # 多音字 → 待确认
|
||
'飲料:いんりょう:\n' # 自动通过
|
||
'IT:アイティー:\n', # 无汉字 → 建议skip
|
||
encoding='utf-8')
|
||
|
||
task = tm.create_task('t1', source=str(src))
|
||
main_path = str(tmp/'voc.txt')
|
||
skipped_path = str(tmp/'skip.txt')
|
||
wf = CleanerWorkflow(task, main=main_path, skipped=skipped_path)
|
||
|
||
result = wf.run()
|
||
assert result['status'] == 'reviewing'
|
||
assert result['process']['auto_pass'] == 1
|
||
assert result['process']['need_review'] == 3 # 2多音字 + 1skip建议
|
||
|
||
# 检查文件结构
|
||
review_file = Path(task.review_file)
|
||
content = review_file.read_text(encoding='utf-8')
|
||
lines = content.splitlines()
|
||
|
||
# 第一段标记
|
||
assert '自动通过' in lines[0]
|
||
# 飲料在第一段
|
||
assert '飲|料:いん|りょう:yin|liao' in content
|
||
# 第二段标记
|
||
assert '待确认' in content
|
||
# 待确认条目
|
||
assert '議|会:ぎ|かい:yi|hui' in content
|
||
assert '# 多音字' in content
|
||
# skip建议(无汉字条目拼音为空)
|
||
assert 'skip IT:アイティー:' in content
|
||
|
||
|
||
def test_apply_review_with_modifications(temp_dir):
|
||
"""测试人工review后应用修改"""
|
||
tmp = temp_dir
|
||
tm = TaskManager(tasks_root=str(tmp / 'tasks'))
|
||
src = tmp / 'src.txt'
|
||
src.write_text('議会:ぎかい:\n社会:しゃかい:\n飲料:いんりょう:\n', encoding='utf-8')
|
||
|
||
task = tm.create_task('t1', source=str(src))
|
||
main_path = str(tmp/'voc.txt')
|
||
skipped_path = str(tmp/'skip.txt')
|
||
wf = CleanerWorkflow(task, main=main_path, skipped=skipped_path)
|
||
wf.run()
|
||
|
||
# 模拟人工review:修改第一条拼音,第二条加skip
|
||
review_file = Path(task.review_file)
|
||
content = review_file.read_text(encoding='utf-8')
|
||
# 修改拼音
|
||
content = content.replace('議|会:ぎ|かい:yi|hui', '議|会:ぎ|かい:yi|kuai')
|
||
# 加skip标记
|
||
content = content.replace('社|会:しゃ|かい:she|hui', 'skip 社|会:しゃ|かい:she|hui')
|
||
content += 'done\n'
|
||
review_file.write_text(content, encoding='utf-8')
|
||
|
||
# 应用
|
||
result = wf.run()
|
||
assert result['status'] == 'ready'
|
||
assert result['apply_result']['accepted'] == 2 # 飲料 + 議会
|
||
assert result['apply_result']['skipped'] == 1 # 社会
|
||
|
||
# 检查落地文件
|
||
proc = wf.processor
|
||
auto = proc._read_clean_lines(proc.auto_done)
|
||
assert len(auto) == 2
|
||
assert '飲|料:いん|りょう:yin|liao' in auto
|
||
assert '議|会:ぎ|かい:yi|kuai' in auto # 修正后的拼音
|
||
|
||
skip = proc._read_clean_lines(proc.skip)
|
||
assert len(skip) == 1
|
||
assert '社|会:しゃ|かい:she|hui' in skip
|
||
|
||
|
||
def test_format_error_rollback_to_third_section(temp_dir):
|
||
"""测试格式错误回退到第三段"""
|
||
tmp = temp_dir
|
||
tm = TaskManager(tasks_root=str(tmp / 'tasks'))
|
||
src = tmp / 'src.txt'
|
||
src.write_text('議会:ぎかい:\n社会:しゃかい:\n', encoding='utf-8')
|
||
|
||
task = tm.create_task('t1', source=str(src))
|
||
main_path = str(tmp/'voc.txt')
|
||
skipped_path = str(tmp/'skip.txt')
|
||
wf = CleanerWorkflow(task, main=main_path, skipped=skipped_path)
|
||
wf.run()
|
||
|
||
# 模拟人工错误修改(误删分隔符)
|
||
review_file = Path(task.review_file)
|
||
content = review_file.read_text(encoding='utf-8')
|
||
content = content.replace('議|会:ぎ|かい:yi|hui', '議|会ぎ|かいyi|hui')
|
||
content += 'done\n'
|
||
review_file.write_text(content, encoding='utf-8')
|
||
|
||
# 尝试应用 → 格式错误
|
||
result = wf.run()
|
||
assert result['action'] == 'need_human'
|
||
assert '格式错误' in result['message']
|
||
|
||
# 检查回退结果:三段结构
|
||
content_after = review_file.read_text(encoding='utf-8')
|
||
lines = content_after.splitlines()
|
||
|
||
# 第一段:自动通过(无)
|
||
assert '自动通过' in content_after
|
||
# 第二段:待确认(正确的)
|
||
assert '待确认' in content_after
|
||
assert '社|会:しゃ|かい:she|hui' in content_after
|
||
# 第三段:格式错误
|
||
assert '格式错误,请修正' in content_after
|
||
assert '議|会ぎ|かいyi|hui # 格式错误:' in content_after
|
||
# done 被删除
|
||
assert 'done' not in content_after
|
||
|
||
|
||
def test_skip_suggestion_can_be_accepted_or_modified(temp_dir):
|
||
"""测试skip建议可以接受或修改"""
|
||
tmp = temp_dir
|
||
tm = TaskManager(tasks_root=str(tmp / 'tasks'))
|
||
src = tmp / 'src.txt'
|
||
src.write_text('IT:アイティー:\nCS:シーエス:\n', encoding='utf-8')
|
||
|
||
task = tm.create_task('t1', source=str(src))
|
||
main_path = str(tmp/'voc.txt')
|
||
skipped_path = str(tmp/'skip.txt')
|
||
wf = CleanerWorkflow(task, main=main_path, skipped=skipped_path)
|
||
wf.run()
|
||
|
||
review_file = Path(task.review_file)
|
||
content = review_file.read_text(encoding='utf-8')
|
||
|
||
# IT保持skip,CS删掉skip前缀(改为保留)
|
||
# 注意:无汉字条目的拼音段为空,格式是 CS:シーエス:
|
||
content = content.replace('skip CS:シーエス:', 'CS:シーエス:')
|
||
content += 'done\n'
|
||
review_file.write_text(content, encoding='utf-8')
|
||
|
||
result = wf.run()
|
||
assert result['status'] == 'ready'
|
||
|
||
proc = wf.processor
|
||
auto = proc._read_clean_lines(proc.auto_done)
|
||
skip = proc._read_clean_lines(proc.skip)
|
||
|
||
assert 'CS:シーエス:' in auto # 删skip后进auto
|
||
assert 'IT:アイティー:' in skip # 保持skip
|
||
|
||
|
||
def test_tilde_preserved_in_output(temp_dir):
|
||
"""测试~被保留在输出中(占一段,拼音留空)"""
|
||
tmp = temp_dir
|
||
tm = TaskManager(tasks_root=str(tmp / 'tasks'))
|
||
src = tmp / 'src.txt'
|
||
src.write_text('~人:アメリカじん:\n~員:かいいん:\n', encoding='utf-8')
|
||
|
||
task = tm.create_task('t1', source=str(src))
|
||
main_path = str(tmp/'voc.txt')
|
||
skipped_path = str(tmp/'skip.txt')
|
||
wf = CleanerWorkflow(task, main=main_path, skipped=skipped_path)
|
||
result = wf.run()
|
||
|
||
# ~ 条目全部自动通过,直接进 auto_done(不经过待确认)
|
||
assert result['status'] == 'ready'
|
||
|
||
proc = wf.processor
|
||
auto = proc._read_clean_lines(proc.auto_done)
|
||
|
||
# ~ 占一段,拼音留空
|
||
assert '~|人:アメリカ|じん:|ren' in auto
|
||
assert '~|員:かい|いん:|yuan' in auto
|