cjm-transcript-decomp-core 0.0.9__tar.gz → 0.0.10__tar.gz
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.
- {cjm_transcript_decomp_core-0.0.9/cjm_transcript_decomp_core.egg-info → cjm_transcript_decomp_core-0.0.10}/PKG-INFO +6 -3
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/README.md +5 -2
- cjm_transcript_decomp_core-0.0.10/cjm_transcript_decomp_core/__init__.py +1 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/cli.py +103 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/graph.py +57 -2
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10/cjm_transcript_decomp_core.egg-info}/PKG-INFO +6 -3
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_graph.py +32 -0
- cjm_transcript_decomp_core-0.0.9/cjm_transcript_decomp_core/__init__.py +0 -1
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/LICENSE +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/MANIFEST.in +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/alignment.py +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/discovery.py +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/launch.py +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/models.py +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/pipeline.py +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/runs.py +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/segments.py +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core/state.py +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core.egg-info/SOURCES.txt +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core.egg-info/dependency_links.txt +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core.egg-info/entry_points.txt +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core.egg-info/requires.txt +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/cjm_transcript_decomp_core.egg-info/top_level.txt +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/pyproject.toml +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/setup.cfg +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_alignment.py +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_batch_logic.py +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_cli.py +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_models.py +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_pipeline.py +0 -0
- {cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_segment_logic.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cjm-transcript-decomp-core
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
4
4
|
Summary: A frontend-agnostic core for the transcript decomposition workflow — composes isolated capability workers (forced alignment, VAD, graph storage) into a headless pipeline that decomposes transcription run manifests into a VAD-aligned context-graph spine with traceable provenance, and a CLI as its first driver.
|
|
5
5
|
Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -26,7 +26,7 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
|
|
|
26
26
|
|
|
27
27
|
## Modules
|
|
28
28
|
|
|
29
|
-
- **`cjm_transcript_decomp_core`**
|
|
29
|
+
- **`cjm_transcript_decomp_core.__init__`**
|
|
30
30
|
- **`cjm_transcript_decomp_core.alignment`** — Pure forced-alignment logic (no capability calls): map FA words back to character spans in the original text, assign words to VAD chunks by timestamp, and build one text segment per VAD chunk. Extracted from the page-centric ForcedAlignmentService (Tier-1 logic).
|
|
31
31
|
- **`cjm_transcript_decomp_core.cli`** — The CLI driver — the decomposition core's first (and currently only) frontend.
|
|
32
32
|
- **`cjm_transcript_decomp_core.discovery`** — Capability-role discovery by manifest surface match — the journaling-by-
|
|
@@ -53,6 +53,7 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
|
|
|
53
53
|
|
|
54
54
|
### `cjm_transcript_decomp_core.cli`
|
|
55
55
|
|
|
56
|
+
- `backfill_provenance_command` _function_ — Execute `backfill-provenance`: mint the Segment -> Transcript DERIVED_FROM
|
|
56
57
|
- `build_parser` _function_ — Build the CLI parser (subcommands: run).
|
|
57
58
|
- `load_capabilities` _function_ — Discover manifests + load each requested capability (default instance).
|
|
58
59
|
- `main` _function_ — CLI entry point (console script: `cjm-transcript-decomp-core`).
|
|
@@ -67,7 +68,9 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
|
|
|
67
68
|
|
|
68
69
|
- `SourceVerification` _class_ — Skeptical-lens verification of one Source's fine-spine extension under a
|
|
69
70
|
- `build_extension_payload` _function_ — Build the fine-spine EXTENSION payload (pure; no capability calls).
|
|
71
|
+
- `provenance_edges_from_segment_wire` _function_ — Derive a Segment's Transcript provenance edges from the node itself:
|
|
70
72
|
- `resolve_root_ids` _function_ — Recompute the transcription-emitted root node ids from manifest data.
|
|
73
|
+
- `segment_provenance_edges` _function_ — Text provenance as EDGES (finding 89b16be6, the references-must-be-edges
|
|
71
74
|
- `verify_source` _function_ — Verify a Source's committed extension via server-side AGGREGATES (D13/D19).
|
|
72
75
|
|
|
73
76
|
### `cjm_transcript_decomp_core.launch`
|
|
@@ -141,4 +144,4 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
|
|
|
141
144
|
## Dependencies
|
|
142
145
|
|
|
143
146
|
**Depends on:** `cjm-capability-primitives`, `cjm-context-graph-layer`, `cjm-context-graph-primitives`, `cjm-substrate`, `cjm-transcript-graph-schema`, `pyyaml`
|
|
144
|
-
**Used by:** `cjm-transcript-decomp-qt
|
|
147
|
+
**Used by:** `cjm-transcript-decomp-qt`
|
|
@@ -6,7 +6,7 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
|
|
|
6
6
|
|
|
7
7
|
## Modules
|
|
8
8
|
|
|
9
|
-
- **`cjm_transcript_decomp_core`**
|
|
9
|
+
- **`cjm_transcript_decomp_core.__init__`**
|
|
10
10
|
- **`cjm_transcript_decomp_core.alignment`** — Pure forced-alignment logic (no capability calls): map FA words back to character spans in the original text, assign words to VAD chunks by timestamp, and build one text segment per VAD chunk. Extracted from the page-centric ForcedAlignmentService (Tier-1 logic).
|
|
11
11
|
- **`cjm_transcript_decomp_core.cli`** — The CLI driver — the decomposition core's first (and currently only) frontend.
|
|
12
12
|
- **`cjm_transcript_decomp_core.discovery`** — Capability-role discovery by manifest surface match — the journaling-by-
|
|
@@ -33,6 +33,7 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
|
|
|
33
33
|
|
|
34
34
|
### `cjm_transcript_decomp_core.cli`
|
|
35
35
|
|
|
36
|
+
- `backfill_provenance_command` _function_ — Execute `backfill-provenance`: mint the Segment -> Transcript DERIVED_FROM
|
|
36
37
|
- `build_parser` _function_ — Build the CLI parser (subcommands: run).
|
|
37
38
|
- `load_capabilities` _function_ — Discover manifests + load each requested capability (default instance).
|
|
38
39
|
- `main` _function_ — CLI entry point (console script: `cjm-transcript-decomp-core`).
|
|
@@ -47,7 +48,9 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
|
|
|
47
48
|
|
|
48
49
|
- `SourceVerification` _class_ — Skeptical-lens verification of one Source's fine-spine extension under a
|
|
49
50
|
- `build_extension_payload` _function_ — Build the fine-spine EXTENSION payload (pure; no capability calls).
|
|
51
|
+
- `provenance_edges_from_segment_wire` _function_ — Derive a Segment's Transcript provenance edges from the node itself:
|
|
50
52
|
- `resolve_root_ids` _function_ — Recompute the transcription-emitted root node ids from manifest data.
|
|
53
|
+
- `segment_provenance_edges` _function_ — Text provenance as EDGES (finding 89b16be6, the references-must-be-edges
|
|
51
54
|
- `verify_source` _function_ — Verify a Source's committed extension via server-side AGGREGATES (D13/D19).
|
|
52
55
|
|
|
53
56
|
### `cjm_transcript_decomp_core.launch`
|
|
@@ -121,4 +124,4 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
|
|
|
121
124
|
## Dependencies
|
|
122
125
|
|
|
123
126
|
**Depends on:** `cjm-capability-primitives`, `cjm-context-graph-layer`, `cjm-context-graph-primitives`, `cjm-substrate`, `cjm-transcript-graph-schema`, `pyyaml`
|
|
124
|
-
**Used by:** `cjm-transcript-decomp-qt
|
|
127
|
+
**Used by:** `cjm-transcript-decomp-qt`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.10"
|
|
@@ -27,9 +27,13 @@ import os
|
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
from typing import Any, Dict, List, Optional
|
|
29
29
|
|
|
30
|
+
from cjm_context_graph_layer.journal import journal_extend, sidecar_journal_path
|
|
31
|
+
from cjm_context_graph_layer.ops import graph_task
|
|
32
|
+
from cjm_context_graph_primitives.query import NodeQuery
|
|
30
33
|
from cjm_substrate.core.manager import CapabilityManager
|
|
31
34
|
from cjm_substrate.core.queue import JobQueue
|
|
32
35
|
from cjm_substrate.core.workspace import resolve_workspace
|
|
36
|
+
from cjm_transcript_decomp_core.graph import provenance_edges_from_segment_wire
|
|
33
37
|
from cjm_transcript_decomp_core.models import DecompConfig
|
|
34
38
|
from cjm_transcript_decomp_core.pipeline import load_source_manifest, run_decomp
|
|
35
39
|
|
|
@@ -107,6 +111,29 @@ def build_parser() -> argparse.ArgumentParser: # Configured CLI parser
|
|
|
107
111
|
run.add_argument("--actor", default=None,
|
|
108
112
|
help="Journal attribution for who/what initiated this run (default: cli:<username>)")
|
|
109
113
|
run.add_argument("-v", "--verbose", action="store_true", help="DEBUG-level logging")
|
|
114
|
+
|
|
115
|
+
# ---- backfill-provenance: Segment -> Transcript DERIVED_FROM edges (finding 89b16be6) ----
|
|
116
|
+
bp = sub.add_parser(
|
|
117
|
+
"backfill-provenance",
|
|
118
|
+
help="Mint Segment -> Transcript DERIVED_FROM edges for Segments already on the graph "
|
|
119
|
+
"(text provenance as an edge, not only text_from metadata); journaled per source "
|
|
120
|
+
"under spine-extension, idempotent on re-run")
|
|
121
|
+
bp.add_argument("--source", default=None,
|
|
122
|
+
help="Restrict to one Source node id (prefix ok; default: every Segment)")
|
|
123
|
+
bp.add_argument("--page-size", type=int, default=2000,
|
|
124
|
+
help="Segments per query page (default 2000)")
|
|
125
|
+
bp.add_argument("--manifests-dir", default=".cjm/manifests", help="Capability manifests directory")
|
|
126
|
+
bp.add_argument("--graph-capability", default="cjm-capability-graph-sqlite",
|
|
127
|
+
help="Graph-storage capability name")
|
|
128
|
+
bp.add_argument("--graph-db-path", default=None,
|
|
129
|
+
help="Graph db path (default: the workspace capability config)")
|
|
130
|
+
bp.add_argument("--workspace", default=None,
|
|
131
|
+
help="Workspace root (default: CJM_WORKSPACE env, else upward walk from cwd)")
|
|
132
|
+
bp.add_argument("--actor", default=None,
|
|
133
|
+
help="Journal attribution (default: cli:<username>)")
|
|
134
|
+
bp.add_argument("--dry-run", action="store_true",
|
|
135
|
+
help="Count the edges per source; touch neither graph nor journal")
|
|
136
|
+
bp.add_argument("-v", "--verbose", action="store_true", help="DEBUG-level logging")
|
|
110
137
|
return parser
|
|
111
138
|
|
|
112
139
|
|
|
@@ -263,4 +290,80 @@ def main(
|
|
|
263
290
|
)
|
|
264
291
|
if args.command == "run":
|
|
265
292
|
return asyncio.run(run_command(args))
|
|
293
|
+
if args.command == "backfill-provenance":
|
|
294
|
+
return asyncio.run(backfill_provenance_command(args))
|
|
266
295
|
raise SystemExit(f"unknown command: {args.command}")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
async def backfill_provenance_command(
|
|
299
|
+
args: argparse.Namespace, # Parsed CLI arguments for the `backfill-provenance` subcommand
|
|
300
|
+
) -> int: # Process exit code
|
|
301
|
+
"""Execute `backfill-provenance`: mint the Segment -> Transcript DERIVED_FROM
|
|
302
|
+
edges (finding 89b16be6) for Segments already on the graph, deriving each
|
|
303
|
+
segment's edges from its own refs (`provenance_edges_from_segment_wire`,
|
|
304
|
+
the same derivation a fresh decomposition uses). Journaled per SOURCE under
|
|
305
|
+
the existing `spine-extension` verb (edges-only wires; the replay handler
|
|
306
|
+
is already registered, and `journal_extend`'s added-delta trim keeps a
|
|
307
|
+
re-run silent), args carry act=provenance-backfill so the ops read as what
|
|
308
|
+
they are. Pages the Segment table so a 100k-segment graph never sits in one
|
|
309
|
+
query; --source narrows to one Source id (prefix ok); --dry-run counts."""
|
|
310
|
+
ws = resolve_workspace(explicit=getattr(args, "workspace", None))
|
|
311
|
+
if ws is not None:
|
|
312
|
+
os.environ["CJM_WORKSPACE"] = str(ws.root)
|
|
313
|
+
manager = CapabilityManager(search_paths=[Path(args.manifests_dir)])
|
|
314
|
+
configs = ({args.graph_capability: {"db_path": args.graph_db_path}}
|
|
315
|
+
if args.graph_db_path else None)
|
|
316
|
+
load_capabilities(manager, [args.graph_capability], configs=configs)
|
|
317
|
+
effective = args.graph_db_path or (
|
|
318
|
+
(manager.instances[args.graph_capability].config or {}).get("db_path"))
|
|
319
|
+
if not effective:
|
|
320
|
+
raise SystemExit("no graph db path: pass --graph-db-path, or persist one on the "
|
|
321
|
+
f"{args.graph_capability} instance in the active workspace's config store")
|
|
322
|
+
journal_path = sidecar_journal_path(str(effective))
|
|
323
|
+
actor = args.actor or f"cli:{getpass.getuser()}"
|
|
324
|
+
queue = JobQueue(deps=manager)
|
|
325
|
+
await queue.start()
|
|
326
|
+
try:
|
|
327
|
+
per_source: Dict[str, List[Dict[str, Any]]] = {}
|
|
328
|
+
seg_counts: Dict[str, int] = {}
|
|
329
|
+
offset = 0
|
|
330
|
+
while True:
|
|
331
|
+
nq = NodeQuery(label="Segment", project=["text_from", "source_id", "sources"],
|
|
332
|
+
limit=args.page_size, offset=offset)
|
|
333
|
+
res = await graph_task(queue, args.graph_capability, "query_nodes", query=nq.to_dict())
|
|
334
|
+
rows = list(res.rows or [])
|
|
335
|
+
for row in rows:
|
|
336
|
+
sid = row.get("source_id") or "?"
|
|
337
|
+
if args.source and not sid.startswith(args.source):
|
|
338
|
+
continue
|
|
339
|
+
seg_counts[sid] = seg_counts.get(sid, 0) + 1
|
|
340
|
+
per_source.setdefault(sid, []).extend(provenance_edges_from_segment_wire(row))
|
|
341
|
+
if len(rows) < args.page_size:
|
|
342
|
+
break
|
|
343
|
+
offset += len(rows)
|
|
344
|
+
total_edges = sum(len(v) for v in per_source.values())
|
|
345
|
+
if args.dry_run:
|
|
346
|
+
for sid in sorted(per_source):
|
|
347
|
+
print(f"{sid[:8]} segments={seg_counts[sid]} edges={len(per_source[sid])}")
|
|
348
|
+
print(f"dry run: {len(per_source)} sources, {sum(seg_counts.values())} segments, "
|
|
349
|
+
f"{total_edges} DERIVED_FROM edges (nothing written)")
|
|
350
|
+
return 0
|
|
351
|
+
added = 0
|
|
352
|
+
for sid in sorted(per_source):
|
|
353
|
+
edges = per_source[sid]
|
|
354
|
+
if not edges:
|
|
355
|
+
continue
|
|
356
|
+
r = await journal_extend(queue, args.graph_capability, [], edges,
|
|
357
|
+
journal_path=journal_path, verb="spine-extension",
|
|
358
|
+
actor=actor,
|
|
359
|
+
args={"act": "provenance-backfill", "source_id": sid,
|
|
360
|
+
"segments": seg_counts[sid], "edges": len(edges)})
|
|
361
|
+
added += r.edges_added
|
|
362
|
+
print(f"{sid[:8]} segments={seg_counts[sid]} edges={len(edges)} added={r.edges_added}")
|
|
363
|
+
finally:
|
|
364
|
+
await queue.stop()
|
|
365
|
+
manager.unload_capability(args.graph_capability)
|
|
366
|
+
print(f"backfilled DERIVED_FROM edges: {added} added of {total_edges} derived "
|
|
367
|
+
f"across {len(per_source)} sources (actor {actor})")
|
|
368
|
+
print(f"journal: {journal_path}")
|
|
369
|
+
return 0
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from typing import Any, Dict, List, Optional, Tuple
|
|
5
5
|
|
|
6
|
-
from cjm_context_graph_layer.grammar import grouped_spine_edges,
|
|
6
|
+
from cjm_context_graph_layer.grammar import (grouped_spine_edges, make_edge, OverlayRelations,
|
|
7
|
+
SpineRelations)
|
|
7
8
|
from cjm_context_graph_layer.ops import graph_task
|
|
8
9
|
from cjm_context_graph_primitives.graph import GraphNode
|
|
9
10
|
from cjm_context_graph_primitives.provenance import SourceRef
|
|
@@ -80,7 +81,9 @@ def build_extension_payload(
|
|
|
80
81
|
transcriber variant; `text_from` is recorded per segment as provenance
|
|
81
82
|
(which Transcript the layer-0 text came from), never as global config. Edges
|
|
82
83
|
come from `grouped_spine_edges`: PART_OF to the OWNING AudioRendition,
|
|
83
|
-
STARTS_WITH per rendition, NEXT chained source-wide across coarse boundaries
|
|
84
|
+
STARTS_WITH per rendition, NEXT chained source-wide across coarse boundaries —
|
|
85
|
+
plus one DERIVED_FROM per sliced Transcript (text provenance as an edge,
|
|
86
|
+
finding 89b16be6; role=text_from marks the authoritative one).
|
|
84
87
|
"""
|
|
85
88
|
roots = resolve_root_ids(source_entry, capabilities_info)
|
|
86
89
|
source_id = roots["source"]
|
|
@@ -114,6 +117,13 @@ def build_extension_payload(
|
|
|
114
117
|
|
|
115
118
|
# The fine spine hangs under each segment's RENDITION (PART_OF rendition).
|
|
116
119
|
edges = grouped_spine_edges([(asegs[i]["rendition"], groups[i]) for i in sorted(groups)])
|
|
120
|
+
# Text provenance is an EDGE, not only a property (finding 89b16be6): each
|
|
121
|
+
# Segment DERIVED_FROM every Transcript it slices (role=text_from on the
|
|
122
|
+
# authoritative one), so Segments are reachable from their Transcript by
|
|
123
|
+
# traversal the way they already were from their AudioRendition. Derived
|
|
124
|
+
# from the node's own refs — the same derivation the backfill verb uses.
|
|
125
|
+
for node in nodes:
|
|
126
|
+
edges.extend(provenance_edges_from_segment_wire(node))
|
|
117
127
|
ids = {"source": source_id, "segments": seg_ids,
|
|
118
128
|
"audio_segments": [a["audio_segment"] for a in asegs],
|
|
119
129
|
"renditions": [a["rendition"] for a in asegs],
|
|
@@ -273,3 +283,48 @@ async def verify_source(
|
|
|
273
283
|
all_have_sources=missing_sources == 0,
|
|
274
284
|
source_locators=sorted(locators),
|
|
275
285
|
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def segment_provenance_edges(
|
|
289
|
+
segment_id: str, # The Segment node id
|
|
290
|
+
transcript_ids: List[str], # Transcript node ids the segment slices text from (any order; de-duplicated)
|
|
291
|
+
text_from: Optional[str] = None, # The authoritative Transcript id (None when the segment carries no text)
|
|
292
|
+
) -> List[Dict[str, Any]]: # DERIVED_FROM edge wires, Segment -> Transcript, one per distinct transcript
|
|
293
|
+
"""Text provenance as EDGES (finding 89b16be6, the references-must-be-edges
|
|
294
|
+
instance): a Segment's text comes from Transcript nodes, and until now that
|
|
295
|
+
link lived only in `text_from` metadata + the CharSlice source refs, so a
|
|
296
|
+
Segment was unreachable from its Transcript by traversal (the only
|
|
297
|
+
traceability edge went to the AudioRendition). One DERIVED_FROM per sliced
|
|
298
|
+
Transcript; the authoritative one carries role=text_from, the rest
|
|
299
|
+
role=variant. Ids are deterministic from the triple, so a backfill over
|
|
300
|
+
existing Segments and a fresh decomposition converge on the same edges."""
|
|
301
|
+
seen: set = set()
|
|
302
|
+
edges: List[Dict[str, Any]] = []
|
|
303
|
+
for tid in transcript_ids:
|
|
304
|
+
if not tid or tid in seen:
|
|
305
|
+
continue
|
|
306
|
+
seen.add(tid)
|
|
307
|
+
role = "text_from" if tid == text_from else "variant"
|
|
308
|
+
edges.append(make_edge(segment_id, tid, OverlayRelations.DERIVED_FROM, {"role": role}))
|
|
309
|
+
return edges
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def provenance_edges_from_segment_wire(
|
|
313
|
+
node: Dict[str, Any], # A Segment node wire (or projected row): {"id", "properties"?, "sources", "text_from"?}
|
|
314
|
+
) -> List[Dict[str, Any]]: # The segment's DERIVED_FROM edges, derived from its own refs
|
|
315
|
+
"""Derive a Segment's Transcript provenance edges from the node itself:
|
|
316
|
+
every `char`-sliced source ref's graph-node locator is a Transcript the
|
|
317
|
+
segment slices text from, and `text_from` (a property on a wire, a
|
|
318
|
+
projected field on a query row) names the authoritative one. ONE derivation
|
|
319
|
+
for both callers — `build_extension_payload` at decomposition time and the
|
|
320
|
+
`backfill-provenance` verb over Segments already on the graph — so the
|
|
321
|
+
backfill reproduces exactly what a fresh run would emit."""
|
|
322
|
+
props = node.get("properties") or {}
|
|
323
|
+
text_from = props.get("text_from", node.get("text_from"))
|
|
324
|
+
tids: List[str] = []
|
|
325
|
+
for ref in node.get("sources") or []:
|
|
326
|
+
sl = ref.get("slice") or {}
|
|
327
|
+
loc = ref.get("locator") or {}
|
|
328
|
+
if sl.get("kind") == "char" and loc.get("node_id"):
|
|
329
|
+
tids.append(loc["node_id"])
|
|
330
|
+
return segment_provenance_edges(node["id"], tids, text_from)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cjm-transcript-decomp-core
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
4
4
|
Summary: A frontend-agnostic core for the transcript decomposition workflow — composes isolated capability workers (forced alignment, VAD, graph storage) into a headless pipeline that decomposes transcription run manifests into a VAD-aligned context-graph spine with traceable provenance, and a CLI as its first driver.
|
|
5
5
|
Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -26,7 +26,7 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
|
|
|
26
26
|
|
|
27
27
|
## Modules
|
|
28
28
|
|
|
29
|
-
- **`cjm_transcript_decomp_core`**
|
|
29
|
+
- **`cjm_transcript_decomp_core.__init__`**
|
|
30
30
|
- **`cjm_transcript_decomp_core.alignment`** — Pure forced-alignment logic (no capability calls): map FA words back to character spans in the original text, assign words to VAD chunks by timestamp, and build one text segment per VAD chunk. Extracted from the page-centric ForcedAlignmentService (Tier-1 logic).
|
|
31
31
|
- **`cjm_transcript_decomp_core.cli`** — The CLI driver — the decomposition core's first (and currently only) frontend.
|
|
32
32
|
- **`cjm_transcript_decomp_core.discovery`** — Capability-role discovery by manifest surface match — the journaling-by-
|
|
@@ -53,6 +53,7 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
|
|
|
53
53
|
|
|
54
54
|
### `cjm_transcript_decomp_core.cli`
|
|
55
55
|
|
|
56
|
+
- `backfill_provenance_command` _function_ — Execute `backfill-provenance`: mint the Segment -> Transcript DERIVED_FROM
|
|
56
57
|
- `build_parser` _function_ — Build the CLI parser (subcommands: run).
|
|
57
58
|
- `load_capabilities` _function_ — Discover manifests + load each requested capability (default instance).
|
|
58
59
|
- `main` _function_ — CLI entry point (console script: `cjm-transcript-decomp-core`).
|
|
@@ -67,7 +68,9 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
|
|
|
67
68
|
|
|
68
69
|
- `SourceVerification` _class_ — Skeptical-lens verification of one Source's fine-spine extension under a
|
|
69
70
|
- `build_extension_payload` _function_ — Build the fine-spine EXTENSION payload (pure; no capability calls).
|
|
71
|
+
- `provenance_edges_from_segment_wire` _function_ — Derive a Segment's Transcript provenance edges from the node itself:
|
|
70
72
|
- `resolve_root_ids` _function_ — Recompute the transcription-emitted root node ids from manifest data.
|
|
73
|
+
- `segment_provenance_edges` _function_ — Text provenance as EDGES (finding 89b16be6, the references-must-be-edges
|
|
71
74
|
- `verify_source` _function_ — Verify a Source's committed extension via server-side AGGREGATES (D13/D19).
|
|
72
75
|
|
|
73
76
|
### `cjm_transcript_decomp_core.launch`
|
|
@@ -141,4 +144,4 @@ A frontend-agnostic core for the transcript decomposition workflow — composes
|
|
|
141
144
|
## Dependencies
|
|
142
145
|
|
|
143
146
|
**Depends on:** `cjm-capability-primitives`, `cjm-context-graph-layer`, `cjm-context-graph-primitives`, `cjm-substrate`, `cjm-transcript-graph-schema`, `pyyaml`
|
|
144
|
-
**Used by:** `cjm-transcript-decomp-qt
|
|
147
|
+
**Used by:** `cjm-transcript-decomp-qt`
|
|
@@ -123,3 +123,35 @@ def test_parallel_spines_disjoint_ids_and_split_metadata():
|
|
|
123
123
|
assert all("split_policy" not in n["properties"] for n in base_nodes)
|
|
124
124
|
assert all(n["properties"]["skeleton_hash"] == "sha256:skel-split" for n in split_nodes)
|
|
125
125
|
assert all(n["properties"]["split_policy"] == "sentence-split/v1" for n in split_nodes)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_segment_transcript_provenance_edges():
|
|
129
|
+
# finding 89b16be6: a Segment's text provenance is an EDGE, not only metadata —
|
|
130
|
+
# DERIVED_FROM per sliced Transcript, the authoritative one role=text_from, the
|
|
131
|
+
# others role=variant; an empty (audio-only) segment derives no text edge; and the
|
|
132
|
+
# wire-derived form (the backfill's input) reproduces the builder's edges exactly.
|
|
133
|
+
from cjm_transcript_decomp_core.graph import (provenance_edges_from_segment_wire,
|
|
134
|
+
segment_provenance_edges)
|
|
135
|
+
roots = resolve_root_ids(SOURCE_ENTRY, CAPABILITIES)
|
|
136
|
+
tv = roots["audio_segments"][0]["transcripts"]["voxtral"]
|
|
137
|
+
tw = roots["audio_segments"][0]["transcripts"]["whisper"]
|
|
138
|
+
nodes, edges, ids = build_extension_payload(SOURCE_ENTRY, CAPABILITIES, "sha256:vad", "voxtral", _segments())
|
|
139
|
+
derived = [e for e in edges if e["relation_type"] == "DERIVED_FROM"]
|
|
140
|
+
# seg 0 slices voxtral + whisper (2 edges), seg 1 voxtral only (1), seg 2 empty (0)
|
|
141
|
+
assert len(derived) == 3
|
|
142
|
+
by_src = {}
|
|
143
|
+
for e in derived:
|
|
144
|
+
by_src.setdefault(e["source_id"], []).append(e)
|
|
145
|
+
s0 = {e["target_id"]: e["properties"]["role"] for e in by_src[ids["segments"][0]]}
|
|
146
|
+
assert s0 == {tv: "text_from", tw: "variant"}
|
|
147
|
+
assert [(e["target_id"], e["properties"]["role"]) for e in by_src[ids["segments"][1]]] == [(tv, "text_from")]
|
|
148
|
+
assert ids["segments"][2] not in by_src
|
|
149
|
+
# Deterministic ids from the triple: a rebuilt edge collides into the same id.
|
|
150
|
+
again = segment_provenance_edges(ids["segments"][0], [tw, tv], tv)
|
|
151
|
+
assert {e["id"] for e in again} == {e["id"] for e in by_src[ids["segments"][0]]}
|
|
152
|
+
# The backfill derivation (from the node's own refs) == the builder's edges.
|
|
153
|
+
for n in nodes:
|
|
154
|
+
assert provenance_edges_from_segment_wire(n) == by_src.get(n["id"], [])
|
|
155
|
+
# A projected query row carries text_from as a field, not under properties.
|
|
156
|
+
row = {"id": nodes[0]["id"], "text_from": tv, "sources": nodes[0]["sources"]}
|
|
157
|
+
assert provenance_edges_from_segment_wire(row) == by_src[nodes[0]["id"]]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_alignment.py
RENAMED
|
File without changes
|
{cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_batch_logic.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_pipeline.py
RENAMED
|
File without changes
|
{cjm_transcript_decomp_core-0.0.9 → cjm_transcript_decomp_core-0.0.10}/tests/test_segment_logic.py
RENAMED
|
File without changes
|