cjm-transcript-graph-schema 0.0.7__tar.gz → 0.0.11__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.
Files changed (20) hide show
  1. {cjm_transcript_graph_schema-0.0.7 → cjm_transcript_graph_schema-0.0.11}/MANIFEST.in +0 -1
  2. cjm_transcript_graph_schema-0.0.11/PKG-INFO +53 -0
  3. cjm_transcript_graph_schema-0.0.11/README.md +36 -0
  4. cjm_transcript_graph_schema-0.0.11/cjm_transcript_graph_schema/__init__.py +1 -0
  5. {cjm_transcript_graph_schema-0.0.7 → cjm_transcript_graph_schema-0.0.11}/cjm_transcript_graph_schema/schema.py +240 -35
  6. cjm_transcript_graph_schema-0.0.11/cjm_transcript_graph_schema.egg-info/PKG-INFO +53 -0
  7. {cjm_transcript_graph_schema-0.0.7 → cjm_transcript_graph_schema-0.0.11}/cjm_transcript_graph_schema.egg-info/SOURCES.txt +2 -3
  8. {cjm_transcript_graph_schema-0.0.7 → cjm_transcript_graph_schema-0.0.11}/pyproject.toml +1 -6
  9. cjm_transcript_graph_schema-0.0.11/tests/test_schema.py +291 -0
  10. cjm_transcript_graph_schema-0.0.7/PKG-INFO +0 -356
  11. cjm_transcript_graph_schema-0.0.7/README.md +0 -338
  12. cjm_transcript_graph_schema-0.0.7/cjm_transcript_graph_schema/__init__.py +0 -1
  13. cjm_transcript_graph_schema-0.0.7/cjm_transcript_graph_schema/_modidx.py +0 -59
  14. cjm_transcript_graph_schema-0.0.7/cjm_transcript_graph_schema.egg-info/PKG-INFO +0 -356
  15. cjm_transcript_graph_schema-0.0.7/cjm_transcript_graph_schema.egg-info/entry_points.txt +0 -2
  16. {cjm_transcript_graph_schema-0.0.7 → cjm_transcript_graph_schema-0.0.11}/LICENSE +0 -0
  17. {cjm_transcript_graph_schema-0.0.7 → cjm_transcript_graph_schema-0.0.11}/cjm_transcript_graph_schema.egg-info/dependency_links.txt +0 -0
  18. {cjm_transcript_graph_schema-0.0.7 → cjm_transcript_graph_schema-0.0.11}/cjm_transcript_graph_schema.egg-info/requires.txt +0 -0
  19. {cjm_transcript_graph_schema-0.0.7 → cjm_transcript_graph_schema-0.0.11}/cjm_transcript_graph_schema.egg-info/top_level.txt +0 -0
  20. {cjm_transcript_graph_schema-0.0.7 → cjm_transcript_graph_schema-0.0.11}/setup.cfg +0 -0
@@ -1,5 +1,4 @@
1
1
  include pyproject.toml
2
2
  include LICENSE
3
- include CONTRIBUTING.md
4
3
  include README.md
5
4
  recursive-exclude * __pycache__
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: cjm-transcript-graph-schema
3
+ Version: 0.0.11
4
+ Summary: Audio-transcript layer schema for context graphs: Source, AudioSegment, Transcript, and Segment nodes with deterministic identity tuples and graph-node mapping, shared by the transcription, decomposition, and correction workflow cores.
5
+ Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
6
+ License: Apache-2.0
7
+ Project-URL: Repository, https://github.com/cj-mills/cjm-transcript-graph-schema
8
+ Keywords: python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: cjm-context-graph-layer>=0.0.7
15
+ Requires-Dist: cjm-context-graph-primitives>=0.0.12
16
+ Dynamic: license-file
17
+
18
+ # cjm-transcript-graph-schema
19
+
20
+ <!-- generated from the context graph by `cjm-context-graph readme` — do not edit by hand; edit the graph (the urge to hand-edit = move it on-graph) -->
21
+
22
+ Audio-transcript layer schema for context graphs: Source, AudioSegment, Transcript, and Segment nodes with deterministic identity tuples and graph-node mapping, shared by the transcription, decomposition, and correction workflow cores.
23
+
24
+ ## Modules
25
+
26
+ - **`cjm_transcript_graph_schema.schema`** — The audio-transcript layer schema (where-graph-begins locked layer schema): Source -> AudioSegment(coarse boundary) -> AudioRendition(model-input: raw | vocals | ...) -> Transcript(per-transcriber variants) emitted by transcription, extended with the fine Segment spine (per-rendition) by decomposition. Deterministic identity tuples per the stage-5 ratified rule; the AudioRendition node lets raw + preprocessed model-inputs of one boundary coexist in one graph. Document from the pre-CR-18 era dissolves into Source.
27
+
28
+ ## API
29
+
30
+ ### `cjm_transcript_graph_schema.schema`
31
+
32
+ - `AudioRenditionNode` _class_ — A model-input rendition OF an AudioSegment — the materialized 16k-mono WAV
33
+ - `AudioSegmentNode` _class_ — Coarse ~5-min spine member: a BOUNDARY range of the Source (an audio fact),
34
+ - `CollectionNode` _class_ — An intrinsic collection (book, podcast series, lecture course) — the
35
+ - `RespinedChunkEntry` _class_ — One chunk respine as the Source records it (0b4d5cfa (4)): the entry the
36
+ - `SegmentNode` _class_ — Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
37
+ - `SourceNode` _class_ — The provenance root: one ingested media file.
38
+ - `TranscriptGraphLabels` _class_ — Node labels of the audio-transcript layer schema.
39
+ - `TranscriptNode` _class_ — One transcriber's text for one AudioRendition (per-transcriber variants at
40
+ - `TranscriptSliceRef` _class_ — One per-transcriber char-range reference for a fine Segment: where this
41
+ - `audio_rendition_node_id` _function_ — AudioRendition identity = (audio segment, preprocessing chain).
42
+ - `audio_segment_node_id` _function_ — AudioSegment identity = (source, boundary range).
43
+ - `collection_edges` _function_ — Membership edges for a collection (ae3464fc: ORDER IS OPTIONAL).
44
+ - `collection_node_id` _function_ — Collection identity = the NORMALIZED TITLE (ratified fork, hub-session
45
+ - `normalize_collection_title` _function_ — Normalize a collection title for identity derivation.
46
+ - `segment_node_id` _function_ — Fine Segment identity = audio-side only (audio rendition, VAD config, chunk
47
+ - `source_node_id` _function_ — Source identity = the ingested file's content hash (Thread-1 ingested-root identity).
48
+ - `transcript_node_id` _function_ — Transcript identity = (audio rendition, transcriber, config) — MIRRORS the
49
+
50
+ ## Dependencies
51
+
52
+ **Depends on:** `cjm-context-graph-layer`, `cjm-context-graph-primitives`
53
+ **Used by:** `cjm-context-graph-layer`, `cjm-transcript-correction-core`, `cjm-transcript-correction-tui`, `cjm-transcript-decomp-core`, `cjm-transcription-core`
@@ -0,0 +1,36 @@
1
+ # cjm-transcript-graph-schema
2
+
3
+ <!-- generated from the context graph by `cjm-context-graph readme` — do not edit by hand; edit the graph (the urge to hand-edit = move it on-graph) -->
4
+
5
+ Audio-transcript layer schema for context graphs: Source, AudioSegment, Transcript, and Segment nodes with deterministic identity tuples and graph-node mapping, shared by the transcription, decomposition, and correction workflow cores.
6
+
7
+ ## Modules
8
+
9
+ - **`cjm_transcript_graph_schema.schema`** — The audio-transcript layer schema (where-graph-begins locked layer schema): Source -> AudioSegment(coarse boundary) -> AudioRendition(model-input: raw | vocals | ...) -> Transcript(per-transcriber variants) emitted by transcription, extended with the fine Segment spine (per-rendition) by decomposition. Deterministic identity tuples per the stage-5 ratified rule; the AudioRendition node lets raw + preprocessed model-inputs of one boundary coexist in one graph. Document from the pre-CR-18 era dissolves into Source.
10
+
11
+ ## API
12
+
13
+ ### `cjm_transcript_graph_schema.schema`
14
+
15
+ - `AudioRenditionNode` _class_ — A model-input rendition OF an AudioSegment — the materialized 16k-mono WAV
16
+ - `AudioSegmentNode` _class_ — Coarse ~5-min spine member: a BOUNDARY range of the Source (an audio fact),
17
+ - `CollectionNode` _class_ — An intrinsic collection (book, podcast series, lecture course) — the
18
+ - `RespinedChunkEntry` _class_ — One chunk respine as the Source records it (0b4d5cfa (4)): the entry the
19
+ - `SegmentNode` _class_ — Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
20
+ - `SourceNode` _class_ — The provenance root: one ingested media file.
21
+ - `TranscriptGraphLabels` _class_ — Node labels of the audio-transcript layer schema.
22
+ - `TranscriptNode` _class_ — One transcriber's text for one AudioRendition (per-transcriber variants at
23
+ - `TranscriptSliceRef` _class_ — One per-transcriber char-range reference for a fine Segment: where this
24
+ - `audio_rendition_node_id` _function_ — AudioRendition identity = (audio segment, preprocessing chain).
25
+ - `audio_segment_node_id` _function_ — AudioSegment identity = (source, boundary range).
26
+ - `collection_edges` _function_ — Membership edges for a collection (ae3464fc: ORDER IS OPTIONAL).
27
+ - `collection_node_id` _function_ — Collection identity = the NORMALIZED TITLE (ratified fork, hub-session
28
+ - `normalize_collection_title` _function_ — Normalize a collection title for identity derivation.
29
+ - `segment_node_id` _function_ — Fine Segment identity = audio-side only (audio rendition, VAD config, chunk
30
+ - `source_node_id` _function_ — Source identity = the ingested file's content hash (Thread-1 ingested-root identity).
31
+ - `transcript_node_id` _function_ — Transcript identity = (audio rendition, transcriber, config) — MIRRORS the
32
+
33
+ ## Dependencies
34
+
35
+ **Depends on:** `cjm-context-graph-layer`, `cjm-context-graph-primitives`
36
+ **Used by:** `cjm-context-graph-layer`, `cjm-transcript-correction-core`, `cjm-transcript-correction-tui`, `cjm-transcript-decomp-core`, `cjm-transcription-core`
@@ -0,0 +1 @@
1
+ __version__ = "0.0.11"
@@ -1,26 +1,17 @@
1
- """The audio-transcript layer schema (where-graph-begins locked layer schema): `Source -> AudioSegment(coarse boundary) -> AudioRendition(model-input: raw | vocals | …) -> Transcript(per-transcriber variants)` emitted by transcription, extended with the fine `Segment` spine (per-rendition) by decomposition. Deterministic identity tuples per the stage-5 ratified rule; the `AudioRendition` node lets raw + preprocessed model-inputs of one boundary coexist in one graph. `Document` from the pre-CR-18 era dissolves into `Source`.
1
+ """The audio-transcript layer schema (where-graph-begins locked layer schema): Source -> AudioSegment(coarse boundary) -> AudioRendition(model-input: raw | vocals | ...) -> Transcript(per-transcriber variants) emitted by transcription, extended with the fine Segment spine (per-rendition) by decomposition, and with the intrinsic Collection layer ABOVE Source (book/series/course; proposed-by-tool / confirmed-by-actor, order optional) captured at transcription hand-off. Deterministic identity tuples per the stage-5 ratified rule; the AudioRendition node lets raw + preprocessed model-inputs of one boundary coexist in one graph. Document from the pre-CR-18 era dissolves into Source."""
2
2
 
3
- Docs: https://cj-mills.github.io/cjm-transcript-graph-schemaschema.html.md"""
4
-
5
- # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/schema.ipynb.
6
-
7
- # %% auto #0
8
- __all__ = ['TranscriptGraphLabels', 'source_node_id', 'audio_segment_node_id', 'audio_rendition_node_id', 'transcript_node_id',
9
- 'segment_node_id', 'SourceNode', 'AudioSegmentNode', 'AudioRenditionNode', 'TranscriptNode',
10
- 'TranscriptSliceRef', 'SegmentNode']
11
-
12
- # %% ../nbs/schema.ipynb #76d0dd1c
13
- from pathlib import Path
14
3
  from dataclasses import dataclass, field
4
+ from pathlib import Path
15
5
  from typing import Any, Dict, List, Optional
16
6
 
7
+ from cjm_context_graph_layer.grammar import (attribution, make_edge, OverlayRelations, spine_edges,
8
+ SpineRelations)
17
9
  from cjm_context_graph_layer.identity import derive_node_id
18
- from cjm_context_graph_layer.grammar import OverlayRelations, attribution, make_edge
19
10
  from cjm_context_graph_primitives.locators import FileRef, GraphNodeRef
20
- from cjm_context_graph_primitives.slices import TimeSlice, CharSlice, FullContent
21
11
  from cjm_context_graph_primitives.provenance import SourceRef
12
+ from cjm_context_graph_primitives.slices import CharSlice, FullContent, TimeSlice
13
+
22
14
 
23
- # %% ../nbs/schema.ipynb #ff94773a
24
15
  class TranscriptGraphLabels:
25
16
  """Node labels of the audio-transcript layer schema."""
26
17
  SOURCE = "Source" # Provenance root: one ingested media file
@@ -28,20 +19,23 @@ class TranscriptGraphLabels:
28
19
  AUDIO_RENDITION = "AudioRendition" # A model-input rendition OF an AudioSegment (raw convert | vocals | future enhancement); OWNS the model-input WAV
29
20
  TRANSCRIPT = "Transcript" # Per-transcriber text variant of one AudioRendition
30
21
  SEGMENT = "Segment" # Fine spine member: one VAD chunk (immutable audio + correctable text), per-rendition
22
+ COLLECTION = "Collection" # Intrinsic collection (book / series / course): the fractal spine layer ABOVE Source
23
+ REFERENCE = "Reference" # Human-added resource link attached to a Source (publisher page / author post / related notes / cited work): Source -HAS_REFERENCE-> Reference (ruling a7ca900d (3), item ae103970)
31
24
 
32
25
  @classmethod
33
26
  def all(cls) -> list: # All schema labels
34
27
  """All schema labels."""
35
- return [cls.SOURCE, cls.AUDIO_SEGMENT, cls.AUDIO_RENDITION, cls.TRANSCRIPT, cls.SEGMENT]
28
+ return [cls.SOURCE, cls.AUDIO_SEGMENT, cls.AUDIO_RENDITION, cls.TRANSCRIPT, cls.SEGMENT, cls.COLLECTION,
29
+ cls.REFERENCE]
30
+
36
31
 
37
- # %% ../nbs/schema.ipynb #a5ea12db
38
32
  def source_node_id(
39
33
  content_hash: str, # Content hash of the source media file ("algo:hexdigest")
40
34
  ) -> str: # Deterministic Source node id
41
35
  """Source identity = the ingested file's content hash (Thread-1 ingested-root identity)."""
42
36
  return derive_node_id("source", content_hash)
43
37
 
44
- # %% ../nbs/schema.ipynb #794a019f
38
+
45
39
  def audio_segment_node_id(
46
40
  source_id: str, # Owning Source node id
47
41
  start: float, # Boundary start (source-coordinate seconds)
@@ -55,7 +49,7 @@ def audio_segment_node_id(
55
49
  the id; the model-input WAV lives on the `AudioRendition` child, not here."""
56
50
  return derive_node_id("audio-segment", source_id, start, end)
57
51
 
58
- # %% ../nbs/schema.ipynb #8e147ac9
52
+
59
53
  def audio_rendition_node_id(
60
54
  audio_segment_id: str, # Owning AudioSegment node id
61
55
  chain: List[str], # Ordered preprocessing-chain descriptors; [] = the raw convert-only rendition
@@ -73,7 +67,7 @@ def audio_rendition_node_id(
73
67
  search)."""
74
68
  return derive_node_id("audio-rendition", audio_segment_id, *chain)
75
69
 
76
- # %% ../nbs/schema.ipynb #6fe30fc8
70
+
77
71
  def transcript_node_id(
78
72
  rendition_id: str, # Owning AudioRendition node id
79
73
  transcriber: str, # Transcriber capability name (e.g. "cjm-transcription-plugin-whisper")
@@ -85,21 +79,65 @@ def transcript_node_id(
85
79
  RENDITION, so a raw vs vocals transcript of the same boundary are distinct."""
86
80
  return derive_node_id("transcript", rendition_id, transcriber, config_hash)
87
81
 
88
- # %% ../nbs/schema.ipynb #f0e4f510
82
+
83
+ EXTERNAL_TRANSCRIBER_MARKER = "/manual" # Suffix naming an operator-landed (pasted) transcript producer
84
+
85
+
86
+ def external_transcriber_name(
87
+ model_id: str, # The external model that produced the text (e.g. "gemini-2.5-pro")
88
+ ) -> str: # The transcriber name an external landing files under
89
+ """Transcriber name for an EXTERNAL landing (ruling 9ffce5f7 (1)): the model id
90
+ plus a manual marker, so a pasted transcript is a THIRD TRANSCRIBER beside the
91
+ local capabilities — decomp folds it per chunk like any other variant, and the
92
+ marker keeps it distinguishable from a capability run of the same model."""
93
+ mid = (model_id or "").strip()
94
+ if not mid:
95
+ raise ValueError("external_transcriber_name: model_id is required")
96
+ return mid + EXTERNAL_TRANSCRIBER_MARKER
97
+
98
+
99
+ def external_config_hash(
100
+ model_id: str, # The external model id
101
+ prompt_hash: str, # Hash of the prompt template the operator used ("" = no prompt recorded)
102
+ ) -> str: # "sha256:<hex>" — the config-hash slot of an external landing
103
+ """The config hash an external landing carries where a capability's effective
104
+ config hash goes: sha256 over (model id, prompt hash). The prompt is DATA
105
+ (f304d31d), so a different prompt template is a different variant — and the
106
+ Transcript identity (rendition, transcriber, config_hash) stays recomputable
107
+ from the derived manifest alone."""
108
+ import hashlib
109
+ h = hashlib.sha256()
110
+ h.update((model_id or "").strip().encode("utf-8"))
111
+ h.update(b"\n")
112
+ h.update((prompt_hash or "").strip().encode("utf-8"))
113
+ return "sha256:" + h.hexdigest()
114
+
115
+
89
116
  def segment_node_id(
90
117
  rendition_id: str, # Owning AudioRendition node id
91
- vad_config_hash: str, # VAD capability config hash (skeleton identity input)
118
+ vad_config_hash: str, # Skeleton config hash (identity input: VAD config, + split policy when a split stage ran)
92
119
  chunk_start: float, # VAD chunk start (chunk-local seconds within the rendition WAV)
93
120
  chunk_end: float, # VAD chunk end (chunk-local seconds)
121
+ identity_salt: Optional[str] = None, # Respine salt (0b4d5cfa (3)): joins the id ONLY; None = the spine's original derivation
94
122
  ) -> str: # Deterministic Segment node id
95
123
  """Fine Segment identity = audio-side only (audio rendition, VAD config, chunk
96
124
  range) — so the skeleton's ids are SHARED across transcribers by
97
125
  construction (C4 "store agreement once" falls out of identity design). Keyed
98
126
  on the RENDITION: each rendition has its own fine spine (vocals isolation can
99
- yield different VAD chunk boundaries than raw)."""
127
+ yield different VAD chunk boundaries than raw).
128
+
129
+ A CHUNK RESPINE (0b4d5cfa) re-derives one coarse chunk into the LIVE spine
130
+ from a landed external transcript: its segments keep the live skeleton hash
131
+ (every picker groups spines by it) and fork identity through `identity_salt`
132
+ = the landed Transcript's node id, so the same landing twice collides into
133
+ a no-op and a different landing mints fresh segments. None (the default)
134
+ reproduces every pre-salt id unchanged."""
135
+ if identity_salt:
136
+ return derive_node_id("segment", rendition_id, vad_config_hash, chunk_start, chunk_end,
137
+ identity_salt)
100
138
  return derive_node_id("segment", rendition_id, vad_config_hash, chunk_start, chunk_end)
101
139
 
102
- # %% ../nbs/schema.ipynb #d768584a
140
+
103
141
  @dataclass
104
142
  class SourceNode:
105
143
  """The provenance root: one ingested media file."""
@@ -128,7 +166,7 @@ class SourceNode:
128
166
  content_hash=self.content_hash).to_dict()],
129
167
  }
130
168
 
131
- # %% ../nbs/schema.ipynb #3d76c961
169
+
132
170
  @dataclass
133
171
  class AudioSegmentNode:
134
172
  """Coarse ~5-min spine member: a BOUNDARY range of the Source (an audio fact),
@@ -169,7 +207,7 @@ class AudioSegmentNode:
169
207
  "sources": [],
170
208
  }
171
209
 
172
- # %% ../nbs/schema.ipynb #c97ae502
210
+
173
211
  @dataclass
174
212
  class AudioRenditionNode:
175
213
  """A model-input rendition OF an AudioSegment — the materialized 16k-mono WAV
@@ -216,7 +254,7 @@ class AudioRenditionNode:
216
254
  """DERIVED_FROM edge: this rendition derives from its AudioSegment's audio."""
217
255
  return make_edge(self.id, self.audio_segment, OverlayRelations.DERIVED_FROM)
218
256
 
219
- # %% ../nbs/schema.ipynb #9948f2c5
257
+
220
258
  @dataclass
221
259
  class TranscriptNode:
222
260
  """One transcriber's text for one AudioRendition (per-transcriber variants at
@@ -228,6 +266,8 @@ class TranscriptNode:
228
266
  audio_hash: str # Content hash of the consumed model-input WAV (the rendition's)
229
267
  metadata: Dict[str, Any] = field(default_factory=dict) # Transcriber-reported metadata
230
268
  asserted_at: Optional[float] = None # Derivation timestamp; None = now
269
+ actor: Optional[str] = None # Attribution actor; None = "capability:<transcriber>" (an EXTERNAL landing names the operator, 9ffce5f7 (1))
270
+ method: str = "transcribe" # Attribution method ("transcribe" for a capability run; "external-landing" for pasted text)
231
271
 
232
272
  @property
233
273
  def id(self) -> str: # Deterministic node id
@@ -244,7 +284,7 @@ class TranscriptNode:
244
284
  }
245
285
  if self.metadata:
246
286
  props["metadata"] = dict(self.metadata)
247
- props.update(attribution(f"capability:{self.transcriber}", method="transcribe",
287
+ props.update(attribution(self.actor or f"capability:{self.transcriber}", method=self.method,
248
288
  asserted_at=self.asserted_at))
249
289
  return {
250
290
  "id": self.id,
@@ -259,7 +299,19 @@ class TranscriptNode:
259
299
  """DERIVED_FROM edge: this Transcript derives from its AudioRendition."""
260
300
  return make_edge(self.id, self.rendition, OverlayRelations.DERIVED_FROM)
261
301
 
262
- # %% ../nbs/schema.ipynb #0c830160
302
+ def supersedes_edge(
303
+ self,
304
+ prior_transcript_id: str, # The Transcript variant this one replaces (same rendition + transcriber, an older config)
305
+ ) -> Dict[str, Any]: # Edge wire dict
306
+ """SUPERSEDES edge: this variant replaces a prior one for the same (rendition,
307
+ transcriber) — a chunk-targeted re-run under a new config, or a re-landed
308
+ external transcript (ruling 910f3692 (2)). The prior node STAYS as append-only
309
+ history; readers treat a variant with an incoming SUPERSEDES edge as NOT LIVE."""
310
+ if prior_transcript_id == self.id:
311
+ raise ValueError("supersedes_edge: a Transcript cannot supersede itself")
312
+ return make_edge(self.id, prior_transcript_id, OverlayRelations.SUPERSEDES)
313
+
314
+
263
315
  @dataclass
264
316
  class TranscriptSliceRef:
265
317
  """One per-transcriber char-range reference for a fine Segment: where this
@@ -276,19 +328,28 @@ class TranscriptSliceRef:
276
328
  content_hash=SourceRef.compute_hash(self.text.encode()),
277
329
  slice=CharSlice(start=self.start_char, end=self.end_char)).to_dict()
278
330
 
279
- # %% ../nbs/schema.ipynb #fecb183e
331
+
280
332
  @dataclass
281
333
  class SegmentNode:
282
334
  """Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
283
335
  text (the immutable-audio/mutable-text spine seam). PART_OF its AudioRendition
284
- (each rendition has its own fine spine).
336
+ (each rendition has its own fine spine). Parallel SPINES coexist under one
337
+ rendition (sentence-split, DEC f1024568): the skeleton hash feeds every node
338
+ id AND rides the queryable `skeleton_hash` prop, so spine-scoped reads never
339
+ mix coexisting skeletons (pre-split nodes lack the prop = the legacy spine).
285
340
 
286
341
  Layer-0 `text` is the ACCURACY transcriber's alignment; the designation is
287
342
  per-segment provenance, not global config (`text_from` names the
288
343
  authoritative Transcript; every transcriber's char range rides
289
- `text_slices`, the authoritative one included)."""
344
+ `text_slices`, the authoritative one included).
345
+
346
+ A CHUNK RESPINE (0b4d5cfa) replaces one coarse chunk's segments INSIDE the
347
+ live spine: the new segments carry `identity_salt` (joins the id only —
348
+ `skeleton_hash` stays the live spine's, so no reader splits the spine),
349
+ the old ones are stamped `superseded_by` (the respine op id) and leave the
350
+ live view — readers filter `superseded_by is null`; nothing is deleted."""
290
351
  rendition: str # Owning AudioRendition node id
291
- vad_config_hash: str # VAD config hash (skeleton identity input)
352
+ vad_config_hash: str # Skeleton config hash (identity input: VAD config, + split policy when a split stage ran)
292
353
  chunk_start: float # VAD chunk start (chunk-local seconds within the rendition WAV)
293
354
  chunk_end: float # VAD chunk end (chunk-local seconds)
294
355
  index: int # Source-wide fine-spine index (0-based)
@@ -298,13 +359,18 @@ class SegmentNode:
298
359
  audio_hash: str = "" # Content hash of the owning rendition's model-input WAV
299
360
  source: Optional[str] = None # Source node id (convenience for direct filters)
300
361
  text_from: Optional[str] = None # Authoritative Transcript node id (None when text is empty)
362
+ split_policy: Optional[str] = None # Split policy+version that refined the skeleton (None = raw VAD chunks)
301
363
  text_slices: List[TranscriptSliceRef] = field(default_factory=list) # All per-transcriber slice refs
364
+ identity_salt: Optional[str] = None # Respine salt (the landed Transcript id): joins the id ONLY (0b4d5cfa (3)); None = original derivation
365
+ superseded_by: Optional[str] = None # Respine op id that retired this segment from the live view (stamped post-hoc; None = live)
302
366
 
303
367
  @property
304
368
  def id(self) -> str: # Deterministic node id
305
- """Deterministic node id (audio-side identity; shared across transcribers, per-rendition)."""
369
+ """Deterministic node id (audio-side identity; shared across transcribers, per-rendition;
370
+ forked by `identity_salt` on a chunk respine)."""
306
371
  return segment_node_id(self.rendition, self.vad_config_hash,
307
- self.chunk_start, self.chunk_end)
372
+ self.chunk_start, self.chunk_end,
373
+ identity_salt=self.identity_salt)
308
374
 
309
375
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
310
376
  """Build the Segment node wire dict (audio ref + per-transcriber text slice refs)."""
@@ -316,11 +382,18 @@ class SegmentNode:
316
382
  "chunk_start": self.chunk_start,
317
383
  "chunk_end": self.chunk_end,
318
384
  "rendition_id": self.rendition,
385
+ "skeleton_hash": self.vad_config_hash,
319
386
  }
387
+ if self.split_policy:
388
+ props["split_policy"] = self.split_policy
320
389
  if self.source:
321
390
  props["source_id"] = self.source
322
391
  if self.text_from:
323
392
  props["text_from"] = self.text_from
393
+ if self.identity_salt:
394
+ props["identity_salt"] = self.identity_salt
395
+ if self.superseded_by:
396
+ props["superseded_by"] = self.superseded_by
324
397
  sources = [SourceRef(locator=GraphNodeRef(node_id=self.rendition),
325
398
  content_hash=self.audio_hash,
326
399
  slice=TimeSlice(start=self.chunk_start, end=self.chunk_end)).to_dict()]
@@ -331,3 +404,135 @@ class SegmentNode:
331
404
  "properties": props,
332
405
  "sources": sources,
333
406
  }
407
+
408
+
409
+ RESPINE_OP_VERB = "chunk-respine" # The journaled spine-fact op that lands a chunk respine (0b4d5cfa (4))
410
+ SOURCE_RESPINED_CHUNKS_PROP = "respined_chunks" # Source prop: the list of RespinedChunkEntry dicts, append-only
411
+ SEGMENT_SUPERSEDED_BY_PROP = "superseded_by" # Segment prop stamped on the chunk's old segments (live readers filter it null)
412
+
413
+
414
+ @dataclass
415
+ class RespinedChunkEntry:
416
+ """One chunk respine as the Source records it (0b4d5cfa (4)): the entry the
417
+ `chunk-respine` op appends to the Source's `respined_chunks` list — WHICH
418
+ coarse chunk, WHICH landed Transcript re-derived it (= the new segments'
419
+ identity salt), under WHICH decomp run, when, and what the op did to the
420
+ dependents (stranded ids on explicit say-so; carried = the corrections the
421
+ chunk-scoped transfer re-homed by time: accepted event inserts, speaker
422
+ assignments). The op id is what `superseded_by` on the old segments names."""
423
+ audio_segment: str # AudioSegment node id (the coarse chunk)
424
+ transcript: str # The landed Transcript node id (= identity_salt of the new segments)
425
+ run: str # The decomp run id the re-derivation ran under
426
+ op_id: str # The chunk-respine op id (stamped as superseded_by)
427
+ ts: float # Wall-clock seconds of the landing
428
+ old_segments: List[str] = field(default_factory=list) # The superseded segment ids, index order
429
+ new_segments: List[str] = field(default_factory=list) # The replacement segment ids, index order
430
+ stranded: List[str] = field(default_factory=list) # Dependent Correction ids that stopped projecting (on say-so)
431
+ carried: List[str] = field(default_factory=list) # Fresh Correction ids the transfer minted onto the new segments
432
+ straddles: List[str] = field(default_factory=list) # New segment ids left unassigned because they straddle two donor speakers (4a7ec4f8 (3))
433
+
434
+ def to_dict(self) -> Dict[str, Any]: # The wire entry
435
+ """The entry as it rides the Source prop (plain JSON, list order preserved)."""
436
+ return {"audio_segment": self.audio_segment, "transcript": self.transcript,
437
+ "run": self.run, "op_id": self.op_id, "ts": self.ts,
438
+ "old_segments": list(self.old_segments), "new_segments": list(self.new_segments),
439
+ "stranded": list(self.stranded), "carried": list(self.carried),
440
+ "straddles": list(self.straddles)}
441
+
442
+ @classmethod
443
+ def from_dict(cls, d: Dict[str, Any]) -> "RespinedChunkEntry": # Parsed entry
444
+ """Read an entry back off the Source prop (missing lists read empty)."""
445
+ return cls(audio_segment=str(d.get("audio_segment") or ""),
446
+ transcript=str(d.get("transcript") or ""),
447
+ run=str(d.get("run") or ""), op_id=str(d.get("op_id") or ""),
448
+ ts=float(d.get("ts") or 0.0),
449
+ old_segments=[str(s) for s in (d.get("old_segments") or [])],
450
+ new_segments=[str(s) for s in (d.get("new_segments") or [])],
451
+ stranded=[str(s) for s in (d.get("stranded") or [])],
452
+ carried=[str(s) for s in (d.get("carried") or [])],
453
+ straddles=[str(s) for s in (d.get("straddles") or [])])
454
+
455
+
456
+ def normalize_collection_title(
457
+ title: str, # Raw title (folder name, user input, ...)
458
+ ) -> str: # Normalized identity form
459
+ """Normalize a collection title for identity derivation.
460
+
461
+ Casefold + underscores-to-spaces + whitespace collapse, so a folder named
462
+ ``Hardcore_History`` and a hand-typed ``Hardcore History`` converge on the
463
+ SAME collection node (ae3464fc: select-existing and retype-the-title are
464
+ the same act by construction). Display keeps the raw title; only identity
465
+ normalizes."""
466
+ return " ".join(title.replace("_", " ").split()).casefold()
467
+
468
+
469
+ def collection_node_id(
470
+ title: str, # Collection title (normalized internally)
471
+ ) -> str: # Deterministic Collection node id
472
+ """Collection identity = the NORMALIZED TITLE (ratified fork, hub-session
473
+ 2026-07-21): path-based identity breaks workspace relocatability, and
474
+ member-hash identity breaks late-added members — late binding is a feature.
475
+ The same book ingested twice (different paths, different machines, members
476
+ accreted over weeks) is the same collection; rare title collisions are
477
+ propose/confirm reconciler territory."""
478
+ return derive_node_id("collection", normalize_collection_title(title))
479
+
480
+
481
+ @dataclass
482
+ class CollectionNode:
483
+ """An intrinsic collection (book, podcast series, lecture course) — the
484
+ fractal spine layer ABOVE Source (the grammar's "series -> episode
485
+ tomorrow", arrived).
486
+
487
+ Status rides the node (ae3464fc actor criterion): a human who accepted,
488
+ renamed, or selected the collection at run launch has CONFIRMED it;
489
+ untouched tool defaults (headless folder run) land PROPOSED — visible and
490
+ grouping-usable everywhere, flagged as awaiting a human. A folder is
491
+ EVIDENCE of a grouping, not proof: proposed collections are cheap to
492
+ rename, refile out of, or dissolve. root_kind=asserted — a collection is
493
+ a grouping ASSERTION about ingested content, not ingested content itself.
494
+ Collection PART_OF Collection nesting is grammar-free (no v0 tooling)."""
495
+ title: str # Display title (identity input via normalize_collection_title)
496
+ status: str = "proposed" # "proposed" (tool inference) | "confirmed" (human act)
497
+ actor: str = "human" # Who declared it (attribution; e.g. "human", "tui:transcription", "cli:transcribe")
498
+ asserted_at: Optional[float] = None # Declaration timestamp; None = now
499
+
500
+ @property
501
+ def id(self) -> str: # Deterministic node id
502
+ """Deterministic node id."""
503
+ return collection_node_id(self.title)
504
+
505
+ def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
506
+ """Build the Collection node wire dict (asserted root; attribution included)."""
507
+ props: Dict[str, Any] = {
508
+ "title": self.title,
509
+ "status": self.status,
510
+ "root_kind": "asserted",
511
+ }
512
+ props.update(attribution(self.actor, method="collection-declaration",
513
+ asserted_at=self.asserted_at))
514
+ return {
515
+ "id": self.id,
516
+ "label": TranscriptGraphLabels.COLLECTION,
517
+ "properties": props,
518
+ "sources": [],
519
+ }
520
+
521
+
522
+ def collection_edges(
523
+ collection_id: str, # Collection node id
524
+ member_ids: List[str], # Member Source node ids (declaration order when ordered=True)
525
+ ordered: bool = False, # True = materialize the order (NEXT chain + STARTS_WITH)
526
+ ) -> List[Dict[str, Any]]: # Edge wire dicts
527
+ """Membership edges for a collection (ae3464fc: ORDER IS OPTIONAL).
528
+
529
+ Unordered membership is just PART_OF member->collection — individually
530
+ accreted members (chapters run one at a time) attach without fabricating a
531
+ sequence. When an order IS known (a folder run's sorted expansion, or a
532
+ curation act), the full fractal spine pattern materializes via
533
+ spine_edges: PART_OF + NEXT chain + STARTS_WITH — identical grammar to
534
+ every layer below. Reordering and late insertion are curation ops, never
535
+ capture-time guesses."""
536
+ if ordered:
537
+ return spine_edges(collection_id, member_ids)
538
+ return [make_edge(m, collection_id, SpineRelations.PART_OF) for m in member_ids]
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: cjm-transcript-graph-schema
3
+ Version: 0.0.11
4
+ Summary: Audio-transcript layer schema for context graphs: Source, AudioSegment, Transcript, and Segment nodes with deterministic identity tuples and graph-node mapping, shared by the transcription, decomposition, and correction workflow cores.
5
+ Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
6
+ License: Apache-2.0
7
+ Project-URL: Repository, https://github.com/cj-mills/cjm-transcript-graph-schema
8
+ Keywords: python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: cjm-context-graph-layer>=0.0.7
15
+ Requires-Dist: cjm-context-graph-primitives>=0.0.12
16
+ Dynamic: license-file
17
+
18
+ # cjm-transcript-graph-schema
19
+
20
+ <!-- generated from the context graph by `cjm-context-graph readme` — do not edit by hand; edit the graph (the urge to hand-edit = move it on-graph) -->
21
+
22
+ Audio-transcript layer schema for context graphs: Source, AudioSegment, Transcript, and Segment nodes with deterministic identity tuples and graph-node mapping, shared by the transcription, decomposition, and correction workflow cores.
23
+
24
+ ## Modules
25
+
26
+ - **`cjm_transcript_graph_schema.schema`** — The audio-transcript layer schema (where-graph-begins locked layer schema): Source -> AudioSegment(coarse boundary) -> AudioRendition(model-input: raw | vocals | ...) -> Transcript(per-transcriber variants) emitted by transcription, extended with the fine Segment spine (per-rendition) by decomposition. Deterministic identity tuples per the stage-5 ratified rule; the AudioRendition node lets raw + preprocessed model-inputs of one boundary coexist in one graph. Document from the pre-CR-18 era dissolves into Source.
27
+
28
+ ## API
29
+
30
+ ### `cjm_transcript_graph_schema.schema`
31
+
32
+ - `AudioRenditionNode` _class_ — A model-input rendition OF an AudioSegment — the materialized 16k-mono WAV
33
+ - `AudioSegmentNode` _class_ — Coarse ~5-min spine member: a BOUNDARY range of the Source (an audio fact),
34
+ - `CollectionNode` _class_ — An intrinsic collection (book, podcast series, lecture course) — the
35
+ - `RespinedChunkEntry` _class_ — One chunk respine as the Source records it (0b4d5cfa (4)): the entry the
36
+ - `SegmentNode` _class_ — Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
37
+ - `SourceNode` _class_ — The provenance root: one ingested media file.
38
+ - `TranscriptGraphLabels` _class_ — Node labels of the audio-transcript layer schema.
39
+ - `TranscriptNode` _class_ — One transcriber's text for one AudioRendition (per-transcriber variants at
40
+ - `TranscriptSliceRef` _class_ — One per-transcriber char-range reference for a fine Segment: where this
41
+ - `audio_rendition_node_id` _function_ — AudioRendition identity = (audio segment, preprocessing chain).
42
+ - `audio_segment_node_id` _function_ — AudioSegment identity = (source, boundary range).
43
+ - `collection_edges` _function_ — Membership edges for a collection (ae3464fc: ORDER IS OPTIONAL).
44
+ - `collection_node_id` _function_ — Collection identity = the NORMALIZED TITLE (ratified fork, hub-session
45
+ - `normalize_collection_title` _function_ — Normalize a collection title for identity derivation.
46
+ - `segment_node_id` _function_ — Fine Segment identity = audio-side only (audio rendition, VAD config, chunk
47
+ - `source_node_id` _function_ — Source identity = the ingested file's content hash (Thread-1 ingested-root identity).
48
+ - `transcript_node_id` _function_ — Transcript identity = (audio rendition, transcriber, config) — MIRRORS the
49
+
50
+ ## Dependencies
51
+
52
+ **Depends on:** `cjm-context-graph-layer`, `cjm-context-graph-primitives`
53
+ **Used by:** `cjm-context-graph-layer`, `cjm-transcript-correction-core`, `cjm-transcript-correction-tui`, `cjm-transcript-decomp-core`, `cjm-transcription-core`
@@ -3,11 +3,10 @@ MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
5
  cjm_transcript_graph_schema/__init__.py
6
- cjm_transcript_graph_schema/_modidx.py
7
6
  cjm_transcript_graph_schema/schema.py
8
7
  cjm_transcript_graph_schema.egg-info/PKG-INFO
9
8
  cjm_transcript_graph_schema.egg-info/SOURCES.txt
10
9
  cjm_transcript_graph_schema.egg-info/dependency_links.txt
11
- cjm_transcript_graph_schema.egg-info/entry_points.txt
12
10
  cjm_transcript_graph_schema.egg-info/requires.txt
13
- cjm_transcript_graph_schema.egg-info/top_level.txt
11
+ cjm_transcript_graph_schema.egg-info/top_level.txt
12
+ tests/test_schema.py
@@ -10,7 +10,7 @@ readme = "README.md"
10
10
  requires-python = ">=3.12"
11
11
  license = {text = "Apache-2.0"}
12
12
  authors = [{name = "Christian J. Mills", email = "9126128+cj-mills@users.noreply.github.com"}]
13
- keywords = ['nbdev']
13
+ keywords = ['python']
14
14
  classifiers = [
15
15
  "Programming Language :: Python :: 3",
16
16
  "Programming Language :: Python :: 3 :: Only",
@@ -19,10 +19,6 @@ dependencies = ['cjm-context-graph-layer>=0.0.7', 'cjm-context-graph-primitives>
19
19
 
20
20
  [project.urls]
21
21
  Repository = "https://github.com/cj-mills/cjm-transcript-graph-schema"
22
- Documentation = "https://cj-mills.github.io/cjm-transcript-graph-schema/"
23
-
24
- [project.entry-points.nbdev]
25
- cjm_transcript_graph_schema = "cjm_transcript_graph_schema._modidx:d"
26
22
 
27
23
  [tool.setuptools.dynamic]
28
24
  version = {attr = "cjm_transcript_graph_schema.__version__"}
@@ -30,4 +26,3 @@ version = {attr = "cjm_transcript_graph_schema.__version__"}
30
26
  [tool.setuptools.packages.find]
31
27
  include = ["cjm_transcript_graph_schema*"]
32
28
 
33
- [tool.nbdev]