cjm-transcript-correction-core 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
@@ -0,0 +1,193 @@
1
+ """The CLI driver — the correction core's first (and currently only) frontend. run <decomp-manifest> corrects the committed spine in the decomp graph DB, pointing the graph worker at that shared DB via load-time config, with optional session resume/reopen; review runs the interactive text-correction loop (the cross-transcriber diff is intra-graph since stage 5)."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Dict, List, Optional
8
+
9
+ from cjm_substrate.core.manager import CapabilityManager
10
+ from cjm_substrate.core.queue import JobQueue
11
+ from cjm_transcript_correction_core.models import CorrectionConfig
12
+ from cjm_transcript_correction_core.pipeline import (load_decomp_manifest, resolve_graph_db_path,
13
+ run_correction, run_review)
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def _add_common_run_args(p: argparse.ArgumentParser) -> None: # Shared run/review arguments
19
+ """Attach the capability / session / output arguments shared by `run` and `review`."""
20
+ p.add_argument("manifest", help="Decomp-core run manifest JSON (the committed spine)")
21
+ p.add_argument("--manifests-dir", default=".cjm/manifests", help="Capability manifests directory")
22
+ p.add_argument("--graph-capability", default="cjm-capability-graph-sqlite", help="Graph-storage capability name")
23
+ p.add_argument("--graph-db-path", default=None,
24
+ help="Override graph DB path (default: the decomp manifest's recorded db_path)")
25
+ p.add_argument("--rendition", default=None,
26
+ help="Which AudioRendition spine to correct when a source has more than one "
27
+ "(\"raw\" or a preprocessing substring e.g. \"demucs\"); default: auto-select the "
28
+ "decomposed one (errors if ambiguous)")
29
+ p.add_argument("--sysmon-capability", default=None,
30
+ help="monitor for empirical attribution (CR-7); loaded first; default: none")
31
+ p.add_argument("--session", default=None, help="Resume an existing CorrectionSession id")
32
+ p.add_argument("--reopen", action="store_true", help="Reopen a completed session (with --session)")
33
+ p.add_argument("--actor", default="human", help="Actor recorded on corrections + review markers")
34
+ p.add_argument("--output", default=None, help="Correction-manifest output path (default: runs/<run_id>.json)")
35
+ p.add_argument("-v", "--verbose", action="store_true", help="DEBUG-level logging")
36
+
37
+
38
+ def build_parser() -> argparse.ArgumentParser: # Configured CLI parser
39
+ """Build the CLI parser (subcommands: run, review).
40
+
41
+ Stage 5: --secondary-manifest is RETIRED — the cross-transcriber diff is
42
+ intra-graph now (variant slices on the shared-skeleton segments).
43
+ """
44
+ parser = argparse.ArgumentParser(
45
+ prog="cjm-transcript-correction-core",
46
+ description="Headless transcript correction: non-destructive overlay on a committed source spine.",
47
+ )
48
+ sub = parser.add_subparsers(dest="command", required=True)
49
+
50
+ run = sub.add_parser("run", help="Prune empty segments + surface the worklist (deterministic)")
51
+ _add_common_run_args(run)
52
+ run.add_argument("--no-prune", action="store_true", help="Skip the D14 empty-segment prune")
53
+ run.add_argument("-y", "--yes", action="store_true", help="Auto-accept HITL seams (headless mode)")
54
+
55
+ review = sub.add_parser("review", help="Interactive text-correction review of the flagged worklist")
56
+ _add_common_run_args(review)
57
+ review.add_argument("--review-max", type=int, default=0, help="Max worklist items to review (0 = all)")
58
+ review.add_argument("-y", "--yes", action="store_true", help="Auto-mark every reviewed item (no edits)")
59
+ return parser
60
+
61
+
62
+ def load_capabilities(
63
+ manager: CapabilityManager, # Freshly constructed manager
64
+ instance_ids: List[str], # Capability names to load, in order
65
+ configs: Optional[Dict[str, Dict]] = None, # Per-capability load-time config (e.g. graph db_path)
66
+ ) -> None:
67
+ """Discover manifests + load each capability, passing per-capability config (CR-2 caller-wins)."""
68
+ configs = configs or {}
69
+ manager.discover_manifests()
70
+ discovered = {m.name: m for m in manager.discovered}
71
+ for iid in instance_ids:
72
+ meta = discovered.get(iid)
73
+ if meta is None:
74
+ raise SystemExit(
75
+ f"capability {iid!r} not found in manifests "
76
+ f"(discovered: {sorted(discovered)}) -- run cjm-ctl install-all first"
77
+ )
78
+ if not manager.load_capability(meta, config=configs.get(iid)):
79
+ raise SystemExit(f"failed to load capability {iid!r}")
80
+ logger.info(f"loaded {iid}" + (f" (db_path override)" if iid in configs else ""))
81
+
82
+
83
+ async def run_command(
84
+ args: argparse.Namespace, # Parsed args for the `run` subcommand
85
+ ) -> int: # Process exit code
86
+ """Execute the `run` subcommand: correct a decomp manifest's committed spine."""
87
+ manifest_path = str(Path(args.manifest).resolve())
88
+ if not Path(manifest_path).exists():
89
+ raise SystemExit(f"decomp manifest not found: {manifest_path}")
90
+
91
+ decomp = load_decomp_manifest(manifest_path)
92
+ graph_db_path = resolve_graph_db_path(decomp, args.graph_capability, override=args.graph_db_path)
93
+ if not graph_db_path:
94
+ raise SystemExit("could not resolve graph DB path from manifest; pass --graph-db-path explicitly")
95
+
96
+ cfg = CorrectionConfig(
97
+ graph_capability=args.graph_capability, graph_db_path=graph_db_path,
98
+ actor=args.actor, assume_yes=args.yes, prune_empty=not args.no_prune,
99
+ rendition_selector=args.rendition,
100
+ )
101
+
102
+ manager = CapabilityManager(
103
+ search_paths=[Path(args.manifests_dir)],
104
+ sysmon_capability_name=args.sysmon_capability,
105
+ )
106
+ load_order = ([args.sysmon_capability] if args.sysmon_capability else []) + [cfg.graph_capability]
107
+ # Point the graph worker at the decomp graph DB (the shared spine) via load-time config.
108
+ load_capabilities(manager, load_order, configs={cfg.graph_capability: {"db_path": graph_db_path}})
109
+
110
+ queue = JobQueue(deps=manager, sysmon_capability_name=args.sysmon_capability)
111
+ await queue.start()
112
+ try:
113
+ manifest = await run_correction(
114
+ manager, queue, cfg, manifest_path, graph_db_path,
115
+ session_id=args.session, reopen=args.reopen,
116
+ )
117
+ finally:
118
+ await queue.stop()
119
+ for iid in reversed(load_order):
120
+ try:
121
+ manager.unload_capability(iid)
122
+ except Exception as e: # Best-effort teardown; never mask the run's outcome
123
+ logger.warning(f"unload {iid} failed: {e}")
124
+
125
+ out = Path(args.output) if args.output else Path("runs") / f"{manifest.run_id}.json"
126
+ manifest.save(out)
127
+ n_sources = len(manifest.sources)
128
+ n_pruned = sum(s.get("pruned", 0) for s in manifest.sources)
129
+ n_flagged = sum(s.get("worklist_flagged", 0) for s in manifest.sources)
130
+ print(f"correction manifest: {out}")
131
+ print(f"sources: {n_sources} worklist flagged: {n_flagged} pruned: {n_pruned}")
132
+ print(f"session: {manifest.session_id}")
133
+ return 0
134
+
135
+
136
+ async def review_command(
137
+ args: argparse.Namespace, # Parsed args for the `review` subcommand
138
+ ) -> int: # Process exit code
139
+ """Execute the `review` subcommand: interactive text corrections over the flagged worklist."""
140
+ manifest_path = str(Path(args.manifest).resolve())
141
+ if not Path(manifest_path).exists():
142
+ raise SystemExit(f"decomp manifest not found: {manifest_path}")
143
+ decomp = load_decomp_manifest(manifest_path)
144
+ graph_db_path = resolve_graph_db_path(decomp, args.graph_capability, override=args.graph_db_path)
145
+ if not graph_db_path:
146
+ raise SystemExit("could not resolve graph DB path from manifest; pass --graph-db-path explicitly")
147
+
148
+ cfg = CorrectionConfig(graph_capability=args.graph_capability, graph_db_path=graph_db_path,
149
+ actor=args.actor, assume_yes=args.yes, prune_empty=False,
150
+ rendition_selector=args.rendition)
151
+ manager = CapabilityManager(search_paths=[Path(args.manifests_dir)], sysmon_capability_name=args.sysmon_capability)
152
+ load_order = ([args.sysmon_capability] if args.sysmon_capability else []) + [cfg.graph_capability]
153
+ load_capabilities(manager, load_order, configs={cfg.graph_capability: {"db_path": graph_db_path}})
154
+
155
+ queue = JobQueue(deps=manager, sysmon_capability_name=args.sysmon_capability)
156
+ await queue.start()
157
+ try:
158
+ manifest = await run_review(
159
+ manager, queue, cfg, manifest_path, graph_db_path,
160
+ session_id=args.session, reopen=args.reopen, max_items=args.review_max,
161
+ )
162
+ finally:
163
+ await queue.stop()
164
+ for iid in reversed(load_order):
165
+ try:
166
+ manager.unload_capability(iid)
167
+ except Exception as e: # Best-effort teardown; never mask the run's outcome
168
+ logger.warning(f"unload {iid} failed: {e}")
169
+
170
+ out = Path(args.output) if args.output else Path("runs") / f"{manifest.run_id}.json"
171
+ manifest.save(out)
172
+ n_corr = sum(s.get("corrected", 0) for s in manifest.sources)
173
+ n_active = sum(s.get("active_corrections", 0) for s in manifest.sources)
174
+ print(f"correction manifest: {out}")
175
+ print(f"sources: {len(manifest.sources)} corrected: {n_corr} active corrections: {n_active}")
176
+ print(f"session: {manifest.session_id}")
177
+ return 0
178
+
179
+
180
+ def main(
181
+ argv: Optional[List[str]] = None, # Argument list override (None = sys.argv)
182
+ ) -> int: # Process exit code
183
+ """CLI entry point (console script: `cjm-transcript-correction-core`)."""
184
+ args = build_parser().parse_args(argv)
185
+ logging.basicConfig(
186
+ level=logging.DEBUG if args.verbose else logging.INFO,
187
+ format="%(asctime)s [%(levelname)s] %(name)s :: %(message)s",
188
+ )
189
+ if args.command == "run":
190
+ return asyncio.run(run_command(args))
191
+ if args.command == "review":
192
+ return asyncio.run(review_command(args))
193
+ raise SystemExit(f"unknown command: {args.command}")