Cai
2026-08-16 7285798f1a8033d6987ed7d3f6bda7c1ecc469b2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import argparse
import csv
from collections import Counter, defaultdict
from pathlib import Path
 
 
def read_csv(path):
    with path.open("r", encoding="utf-8-sig", newline="") as handle:
        return list(csv.DictReader(handle))
 
 
def write_csv(path, fieldnames, rows):
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8-sig", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(rows)
 
 
def norm_path(value):
    return (value or "").replace("\\", "/").strip()
 
 
def rel_to_project(path, project_root):
    p = Path(path)
    if p.is_absolute():
        try:
            return p.relative_to(project_root).as_posix()
        except ValueError:
            return p.as_posix()
    return p.as_posix()
 
 
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--project-root", default=".")
    parser.add_argument("--status", required=True)
    parser.add_argument("--source-document", required=True)
    parser.add_argument("--manifest-dir", required=True)
    parser.add_argument("--outputs-dir", required=True)
    args = parser.parse_args()
 
    project_root = Path(args.project_root).resolve()
    status_path = project_root / args.status
    source_path = project_root / args.source_document
    manifest_dir = project_root / args.manifest_dir
    outputs_dir = project_root / args.outputs_dir
 
    status_rows = read_csv(status_path)
    source_rows = read_csv(source_path)
 
    source_by_path = {norm_path(row.get("raw_file_path")): row for row in source_rows}
    source_by_hash = {row.get("file_sha256", ""): row for row in source_rows if row.get("file_sha256")}
 
    enriched = []
    for row in status_rows:
        raw_path = norm_path(row.get("raw_file_path"))
        source = source_by_path.get(raw_path) or source_by_hash.get(row.get("raw_sha256", ""))
        enriched_row = dict(row)
        if source:
            enriched_row["source_doc_id"] = source.get("doc_id", "")
            enriched_row["source_title"] = source.get("title", "")
            enriched_row["source_processing_status"] = source.get("processing_status", "")
        else:
            enriched_row["source_doc_id"] = row.get("source_doc_id", "")
            enriched_row["source_title"] = ""
            enriched_row["source_processing_status"] = ""
        enriched.append(enriched_row)
 
    enriched_fields = list(status_rows[0].keys()) if status_rows else []
    for extra in ["source_title", "source_processing_status"]:
        if extra not in enriched_fields:
            enriched_fields.append(extra)
    enriched_path = manifest_dir / "conversion_status_batch003_enriched.csv"
    write_csv(enriched_path, enriched_fields, enriched)
 
    by_sub = defaultdict(list)
    for row in enriched:
        by_sub[row.get("sub_batch_id", "")].append(row)
 
    summary_rows = []
    for sub_batch_id in sorted(by_sub):
        rows = by_sub[sub_batch_id]
        status_counts = Counter(row.get("conversion_status", "") for row in rows)
        detected_counts = Counter(row.get("detected_type", "") for row in rows)
        indexes = []
        for row in rows:
            conv_id = row.get("conversion_id", "")
            try:
                indexes.append(int(conv_id.rsplit("-", 1)[-1]))
            except ValueError:
                pass
        summary_rows.append(
            {
                "case_id": rows[0].get("case_id", ""),
                "batch_id": rows[0].get("batch_id", ""),
                "sub_batch_id": sub_batch_id,
                "run_id": rows[0].get("run_id", ""),
                "input_count": len(rows),
                "raw_inventory_index_min": min(indexes) if indexes else "",
                "raw_inventory_index_max": max(indexes) if indexes else "",
                "text_converted_count": status_counts.get("TEXT_CONVERTED", 0),
                "failed_count": status_counts.get("FAILED", 0),
                "zip_or_office_count": detected_counts.get("ZIP_OR_OFFICE", 0),
                "pdf_count": detected_counts.get("PDF", 0),
                "review_status": "DRAFT_FOR_REVIEW",
            }
        )
 
        sub_path = manifest_dir / f"conversion_status_batch003_{sub_batch_id}.csv"
        write_csv(sub_path, enriched_fields, rows)
 
    summary_fields = [
        "case_id",
        "batch_id",
        "sub_batch_id",
        "run_id",
        "input_count",
        "raw_inventory_index_min",
        "raw_inventory_index_max",
        "text_converted_count",
        "failed_count",
        "zip_or_office_count",
        "pdf_count",
        "review_status",
    ]
    sub_manifest_path = manifest_dir / "sub_batch_manifest_batch003.csv"
    write_csv(sub_manifest_path, summary_fields, summary_rows)
 
    failed_rows = [row for row in enriched if row.get("conversion_status") != "TEXT_CONVERTED"]
    gap_fields = [
        "case_id",
        "batch_id",
        "sub_batch_id",
        "run_id",
        "source_doc_id",
        "raw_file_path",
        "raw_sha256",
        "detected_type",
        "conversion_status",
        "error_message",
        "review_status",
    ]
    gap_path = manifest_dir / "conversion_gap_batch003.csv"
    write_csv(gap_path, gap_fields, [{field: row.get(field, "") for field in gap_fields} for row in failed_rows])
 
    total_counts = Counter(row.get("conversion_status", "") for row in enriched)
    type_counts = Counter(row.get("detected_type", "") for row in enriched)
    md_lines = [
        "# BATCH-003 转换摘要",
        "",
        "状态:DRAFT_FOR_REVIEW",
        "",
        "## 总览",
        "",
        f"- 输入记录:{len(enriched)}",
        f"- TEXT_CONVERTED:{total_counts.get('TEXT_CONVERTED', 0)}",
        f"- FAILED:{total_counts.get('FAILED', 0)}",
        f"- PDF:{type_counts.get('PDF', 0)}",
        f"- ZIP_OR_OFFICE:{type_counts.get('ZIP_OR_OFFICE', 0)}",
        "",
        "## sub_batch 汇总",
        "",
        "| sub_batch_id | 输入 | 成功 | 失败 | PDF | ZIP_OR_OFFICE | raw_inventory 行号范围 |",
        "|---|---:|---:|---:|---:|---:|---|",
    ]
    for row in summary_rows:
        md_lines.append(
            f"| {row['sub_batch_id']} | {row['input_count']} | {row['text_converted_count']} | "
            f"{row['failed_count']} | {row['pdf_count']} | {row['zip_or_office_count']} | "
            f"{row['raw_inventory_index_min']}-{row['raw_inventory_index_max']} |"
        )
    md_lines.extend(
        [
            "",
            "说明:raw_inventory 行号范围来自 `conversion_id`,中间可能跳号;跳号通常是因为原始盘点中已有登记文件被本轮未登记转换流程跳过。",
            "",
            "## 输出文件",
            "",
            f"- `{rel_to_project(enriched_path, project_root)}`",
            f"- `{rel_to_project(sub_manifest_path, project_root)}`",
            f"- `{rel_to_project(gap_path, project_root)}`",
            "- `ana-data/cases/有色案例/manifest/conversion_status_batch003_SB*.csv`",
            "",
            "## 限制",
            "",
            "本摘要只证明 raw 到文本的转换状态,不构成正式行业结论。失败文件进入转换缺口清单,后续需要重试、替代解析或人工复核。",
        ]
    )
    summary_path = outputs_dir / "batch003_conversion_summary.md"
    summary_path.parent.mkdir(parents=True, exist_ok=True)
    summary_path.write_text("\n".join(md_lines) + "\n", encoding="utf-8")
 
    print(f"enriched={rel_to_project(enriched_path, project_root)}")
    print(f"sub_manifest={rel_to_project(sub_manifest_path, project_root)}")
    print(f"gap={rel_to_project(gap_path, project_root)}")
    print(f"summary={rel_to_project(summary_path, project_root)}")
 
 
if __name__ == "__main__":
    main()