-
Notifications
You must be signed in to change notification settings - Fork 40
Expand file tree
/
Copy pathcore.py
More file actions
129 lines (109 loc) · 5.1 KB
/
Copy pathcore.py
File metadata and controls
129 lines (109 loc) · 5.1 KB
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
"""Shared conversion API — used by both CLI and GUI."""
from pathlib import Path
import logging
from cleaner import load_config, load_prompt, create_ai_backend, process_file, SCRIPT_DIR
_STATUS_MAP = {
"ok": "ok",
"dry_run": "dry_run", # preserved so CLI --summary is not a breaking change
"no_content": "skipped",
"write_error": "error",
"error": "error",
}
class _ErrorCapture(logging.Handler):
"""Captures warning-and-above messages from conversion-related loggers."""
def __init__(self):
super().__init__(logging.WARNING)
self.messages = []
def emit(self, record):
self.messages.append(record.getMessage())
def _build_env(ai="none", config_path=None):
"""Load config + create backend + load prompt. Shared setup for GUI and batch calls."""
if config_path is None:
config_path = str(SCRIPT_DIR / "config.json")
config = load_config(config_path)
ai_backend = create_ai_backend(ai, config)
prompt = load_prompt(config, config_path=config_path) if ai_backend else None
return config, ai_backend, prompt
def _run_one(filepath, ai_backend, prompt, config, output_dir, output_format="md", dry_run=False):
"""
Call process_file and return a GUI-shaped result dict.
Attaches to doc-cleaner, parsers, and classifiers loggers to capture
warning-and-above messages emitted during conversion.
"""
capture = _ErrorCapture()
_watched = [
logging.getLogger("doc-cleaner"),
logging.getLogger("parsers"),
logging.getLogger("classifiers"),
]
for _log in _watched:
_log.addHandler(capture)
try:
status_raw, out_path = process_file(
filepath, ai_backend, prompt, config, output_dir, output_format=output_format, dry_run=dry_run
)
finally:
for _log in _watched:
_log.removeHandler(capture)
status = _STATUS_MAP.get(status_raw, "error")
if status == "error":
error_msg = capture.messages[-1] if capture.messages else "轉換失敗"
elif status == "skipped":
# surface an actionable hint when any parser logged one — e.g. a modern
# iWork file whose content can't be extracted and should be exported to
# PDF. Scan all captured messages, not just the last (process_file also
# logs a generic "No content extracted" line after the parser's hint).
if any("export" in m.lower() or "匯出" in m for m in capture.messages):
error_msg = "此檔無可擷取的內嵌內容;請用原 App 匯出 PDF 後再轉換"
else:
error_msg = "未擷取到文字內容"
else:
error_msg = None
output = str(Path(out_path).resolve()) if out_path else None
# Preview target: only Markdown can be previewed. For md/both the primary
# path process_file returns IS the written .md (epub-only never writes one),
# so this derives locally with no process_file interface change.
preview = output if (status == "ok" and output_format in ("md", "both")) else None
return {
"file": Path(filepath).name,
"input": str(Path(filepath).resolve()),
"output": output,
"preview": preview,
"status": status,
"error": error_msg,
}
def convert_file(input_path, output_dir=None, ai="none", output_format="md"):
"""
Convert a single file. Loads config, builds backend, runs extraction.
Returns: {file, input, output, status, error}
status: "ok" | "skipped" | "error"
output_dir=None writes the result beside the source file.
"""
input_path = str(Path(input_path).resolve())
if output_dir is None:
output_dir = str(Path(input_path).parent)
config, ai_backend, prompt = _build_env(ai=ai)
return _run_one(input_path, ai_backend, prompt, config, output_dir, output_format=output_format)
def convert_files(paths, output_resolver=None, ai="none", output_format="md", config=None, config_path=None, dry_run=False):
"""
Batch conversion. Builds config + backend + prompt once, reuses across all files.
Continues past failing files — one error never aborts the batch.
paths: iterable of file path strings
output_resolver: callable(abspath) -> output_dir string; None → sibling of each source
ai: AI backend name ("none" for pure extraction)
config: pre-processed config dict (optional; None loads default config.json)
config_path: path to config file (for prompt resolution when config is provided)
dry_run: skip writing files, preview only
Returns: list of {file, input, output, status, error} dicts.
"""
if config is None:
config, ai_backend, prompt = _build_env(ai=ai, config_path=config_path)
else:
ai_backend = create_ai_backend(ai, config)
prompt = load_prompt(config, config_path=config_path) if ai_backend else None
results = []
for path in paths:
path = str(Path(path).resolve())
output_dir = output_resolver(path) if output_resolver else str(Path(path).parent)
results.append(_run_one(path, ai_backend, prompt, config, output_dir, output_format=output_format, dry_run=dry_run))
return results