Coverage for src\genesis\qa\validator.py: 100%
27 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 14:20 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 14:20 +0800
1"""QA 校验:章节级/文档级评估(Phase 5)。"""
2from __future__ import annotations
4from genesis.eval.scorer import ChapterScorer, EvalReport, ChapterArtifact
5from genesis.qa.report import QAReport
6from genesis.writer.models import ChapterContent
9class QAValidator:
10 def __init__(self, scorer: ChapterScorer | None = None) -> None:
11 self.scorer = scorer or ChapterScorer()
13 def _to_artifact(self, content: ChapterContent, expected_language: str = "") -> ChapterArtifact:
14 """把 ChapterContent 转换为评分器所需的 ChapterArtifact(聚合正文与来源 URI)。
16 expected_language:期望输出语言("zh"/"ja";空=不可验证,维度记满分)。
17 blocks 保留 (type, text) 供语言维度排除 heading/table。
18 """
19 text = "".join(b.text or "" for b in content.blocks)
20 source_uris: list[str] = []
21 for b in content.blocks:
22 source_uris.extend(b.source_uris)
23 return ChapterArtifact(
24 chapter_id=content.chapter_id,
25 text=text,
26 source_uris=source_uris,
27 template_sections_expected=[],
28 expected_language=expected_language,
29 blocks=[(b.type, b.text or "", b.caption or "") for b in content.blocks],
30 )
32 def validate_chapter(
33 self, content: ChapterContent, structured_source, expected_language: str = ""
34 ) -> tuple[bool, EvalReport]:
35 report = self.scorer.score([self._to_artifact(content, expected_language)], structured_source)
36 passed = content.chapter_id not in report.failed_chapters
37 return passed, report
39 def validate_document(
40 self, contents: list[ChapterContent], structured_source, expected_language: str = ""
41 ) -> list[tuple[bool, EvalReport]]:
42 return [self.validate_chapter(c, structured_source, expected_language) for c in contents]
44 def validate_doc(
45 self, contents: list[ChapterContent], structured_source, expected_language: str = ""
46 ) -> QAReport:
47 results = self.validate_document(contents, structured_source, expected_language)
48 passed_flags = [ok for ok, _ in results]
49 reports = [rep for _, rep in results]
50 failed = [c.chapter_id for c, ok in zip(contents, passed_flags) if not ok]
51 overall = sum(r.total_score for r in reports) / len(reports) if reports else 0.0
52 summary = "全部章节通过" if all(passed_flags) else f"{len(failed)} 章未通过: {failed}"
53 return QAReport(
54 passed=all(passed_flags),
55 overall_score=round(overall, 4),
56 per_chapter=reports,
57 failed_chapters=failed,
58 summary=summary,
59 )