MB-X Bilibili Pipeline
6 days ago dda2d9f4270f7670d2746522cf11a4ce9f72567c
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
"""Convert nonferrous raw reports for an approved batch.
 
This script is intentionally narrow: it reads a raw inventory CSV, converts
PDF files with pypdf, extracts basic text from Office zip containers when
possible, and writes conversion status rows. It does not create conclusions.
"""
 
from __future__ import annotations
 
import argparse
import csv
import re
import zipfile
from pathlib import Path
 
from pypdf import PdfReader
 
 
def clean_text(text: str) -> str:
    text = text.replace("\x00", " ")
    text = re.sub(r"[ \t]+", " ", text)
    text = re.sub(r"\n{3,}", "\n\n", text)
    return text.strip()
 
 
def convert_pdf(path: Path) -> tuple[str, int, str]:
    reader = PdfReader(str(path))
    parts: list[str] = []
    for idx, page in enumerate(reader.pages, start=1):
        page_text = page.extract_text() or ""
        parts.append(f"\n\n[[PAGE {idx}]]\n{page_text}")
    text = clean_text("\n".join(parts))
    return text, len(reader.pages), ""
 
 
def convert_office_zip(path: Path) -> tuple[str, int, str]:
    parts: list[str] = []
    with zipfile.ZipFile(path) as zf:
        names = zf.namelist()
        for name in names:
            lower = name.lower()
            if not (
                lower.endswith(".xml")
                and (
                    lower.startswith("word/")
                    or lower.startswith("ppt/")
                    or lower.startswith("xl/sharedstrings")
                )
            ):
                continue
            raw = zf.read(name).decode("utf-8", errors="ignore")
            raw = re.sub(r"<[^>]+>", " ", raw)
            raw = re.sub(r"\s+", " ", raw)
            if raw.strip():
                parts.append(f"[[{name}]]\n{raw.strip()}")
    if not parts:
        return "", 0, "no readable Office XML text found"
    return clean_text("\n\n".join(parts)), 0, ""
 
 
def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--project-root", default=".")
    parser.add_argument("--inventory", required=True)
    parser.add_argument("--converted-dir", required=True)
    parser.add_argument("--status-out", required=True)
    parser.add_argument("--case-id", required=True)
    parser.add_argument("--batch-id", required=True)
    parser.add_argument("--run-id", required=True)
    parser.add_argument("--only-unregistered", action="store_true")
    args = parser.parse_args()
 
    root = Path(args.project_root).resolve()
    inventory = root / args.inventory
    converted_dir = root / args.converted_dir
    converted_dir.mkdir(parents=True, exist_ok=True)
    status_out = root / args.status_out
    status_out.parent.mkdir(parents=True, exist_ok=True)
 
    with inventory.open("r", encoding="utf-8-sig", newline="") as f:
        rows = list(csv.DictReader(f))
 
    status_rows: list[dict[str, str | int]] = []
    for idx, row in enumerate(rows, start=1):
        if args.only_unregistered and row.get("already_in_source_document") == "YES":
            continue
        raw_path = root / row["raw_file_path"]
        stem = raw_path.name
        out_name = f"{stem}.txt" if not stem.lower().endswith(".txt") else stem
        out_path = converted_dir / out_name
        error = ""
        page_count = 0
        char_count = 0
        status = "FAILED"
        try:
            detected = row.get("detected_type", "")
            if detected == "PDF":
                text, page_count, error = convert_pdf(raw_path)
            elif detected == "ZIP_OR_OFFICE":
                text, page_count, error = convert_office_zip(raw_path)
            else:
                text, page_count, error = "", 0, f"unsupported detected_type={detected}"
            if text and not error:
                out_path.write_text(text, encoding="utf-8")
                char_count = len(text)
                status = "TEXT_CONVERTED"
            elif not error:
                error = "empty converted text"
        except Exception as exc:  # keep batch running and record the failure
            error = f"{type(exc).__name__}: {exc}"
 
        status_rows.append(
            {
                "conversion_id": f"YS-B3-CONV-{idx:04d}",
                "source_doc_id": row.get("inventory_id", ""),
                "case_id": args.case_id,
                "batch_id": args.batch_id,
                "run_id": args.run_id,
                "raw_file_path": row.get("raw_file_path", ""),
                "raw_sha256": row.get("sha256", ""),
                "detected_type": row.get("detected_type", ""),
                "conversion_tool": "pypdf_or_office_zip_xml",
                "converted_text_path": out_path.relative_to(root).as_posix() if status == "TEXT_CONVERTED" else "",
                "page_count": page_count,
                "char_count": char_count,
                "conversion_status": status,
                "error_message": error,
            }
        )
 
    if status_rows:
        with status_out.open("w", encoding="utf-8-sig", newline="") as f:
            writer = csv.DictWriter(f, fieldnames=list(status_rows[0].keys()))
            writer.writeheader()
            writer.writerows(status_rows)
    print(f"status_rows={len(status_rows)}")
    print(status_out.relative_to(root))
    return 0
 
 
if __name__ == "__main__":
    raise SystemExit(main())