cjm-transcript-graph-schema 0.0.5__tar.gz → 0.0.6__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 (16) hide show
  1. {cjm_transcript_graph_schema-0.0.5/cjm_transcript_graph_schema.egg-info → cjm_transcript_graph_schema-0.0.6}/PKG-INFO +112 -44
  2. {cjm_transcript_graph_schema-0.0.5 → cjm_transcript_graph_schema-0.0.6}/README.md +111 -43
  3. cjm_transcript_graph_schema-0.0.6/cjm_transcript_graph_schema/__init__.py +1 -0
  4. {cjm_transcript_graph_schema-0.0.5 → cjm_transcript_graph_schema-0.0.6}/cjm_transcript_graph_schema/_modidx.py +11 -1
  5. {cjm_transcript_graph_schema-0.0.5 → cjm_transcript_graph_schema-0.0.6}/cjm_transcript_graph_schema/schema.py +123 -53
  6. {cjm_transcript_graph_schema-0.0.5 → cjm_transcript_graph_schema-0.0.6/cjm_transcript_graph_schema.egg-info}/PKG-INFO +112 -44
  7. cjm_transcript_graph_schema-0.0.5/cjm_transcript_graph_schema/__init__.py +0 -1
  8. {cjm_transcript_graph_schema-0.0.5 → cjm_transcript_graph_schema-0.0.6}/LICENSE +0 -0
  9. {cjm_transcript_graph_schema-0.0.5 → cjm_transcript_graph_schema-0.0.6}/MANIFEST.in +0 -0
  10. {cjm_transcript_graph_schema-0.0.5 → cjm_transcript_graph_schema-0.0.6}/cjm_transcript_graph_schema.egg-info/SOURCES.txt +0 -0
  11. {cjm_transcript_graph_schema-0.0.5 → cjm_transcript_graph_schema-0.0.6}/cjm_transcript_graph_schema.egg-info/dependency_links.txt +0 -0
  12. {cjm_transcript_graph_schema-0.0.5 → cjm_transcript_graph_schema-0.0.6}/cjm_transcript_graph_schema.egg-info/entry_points.txt +0 -0
  13. {cjm_transcript_graph_schema-0.0.5 → cjm_transcript_graph_schema-0.0.6}/cjm_transcript_graph_schema.egg-info/requires.txt +0 -0
  14. {cjm_transcript_graph_schema-0.0.5 → cjm_transcript_graph_schema-0.0.6}/cjm_transcript_graph_schema.egg-info/top_level.txt +0 -0
  15. {cjm_transcript_graph_schema-0.0.5 → cjm_transcript_graph_schema-0.0.6}/pyproject.toml +0 -0
  16. {cjm_transcript_graph_schema-0.0.5 → cjm_transcript_graph_schema-0.0.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-transcript-graph-schema
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
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
5
  Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
6
6
  License: Apache-2.0
@@ -30,7 +30,7 @@ pip install cjm_transcript_graph_schema
30
30
  ## Project Structure
31
31
 
32
32
  nbs/
33
- └── schema.ipynb # The audio-transcript layer schema (where-graph-begins locked layer schema): `Source -> AudioSegment(coarse, owns the model-input WAV) -> Transcript(per-transcriber variants)` emitted by transcription, extended with the fine `Segment` spine by decomposition. Deterministic identity tuples per the stage-5 ratified rule; `Document` from the pre-CR-18 era dissolves into `Source`.
33
+ └── schema.ipynb # 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`.
34
34
 
35
35
  Total: 1 notebook
36
36
 
@@ -55,10 +55,12 @@ Detailed documentation for each module in the project:
55
55
 
56
56
  > The audio-transcript layer schema (where-graph-begins locked layer
57
57
  > schema):
58
- > `Source -> AudioSegment(coarse, owns the model-input WAV) -> Transcript(per-transcriber variants)`
59
- > emitted by transcription, extended with the fine `Segment` spine by
60
- > decomposition. Deterministic identity tuples per the stage-5 ratified
61
- > rule; `Document` from the pre-CR-18 era dissolves into `Source`.
58
+ > `Source -> AudioSegment(coarse boundary) -> AudioRendition(model-input: raw | vocals | …) -> Transcript(per-transcriber variants)`
59
+ > emitted by transcription, extended with the fine `Segment` spine
60
+ > (per-rendition) by decomposition. Deterministic identity tuples per
61
+ > the stage-5 ratified rule; the `AudioRendition` node lets raw +
62
+ > preprocessed model-inputs of one boundary coexist in one graph.
63
+ > `Document` from the pre-CR-18 era dissolves into `Source`.
62
64
 
63
65
  #### Import
64
66
 
@@ -67,10 +69,12 @@ from cjm_transcript_graph_schema.schema import (
67
69
  TranscriptGraphLabels,
68
70
  source_node_id,
69
71
  audio_segment_node_id,
72
+ audio_rendition_node_id,
70
73
  transcript_node_id,
71
74
  segment_node_id,
72
75
  SourceNode,
73
76
  AudioSegmentNode,
77
+ AudioRenditionNode,
74
78
  TranscriptNode,
75
79
  TranscriptSliceRef,
76
80
  SegmentNode
@@ -95,36 +99,60 @@ def audio_segment_node_id(
95
99
  """
96
100
  AudioSegment identity = (source, boundary range).
97
101
 
98
- Conversion-config-independent: the model-input WAV is payload/provenance,
99
- not identity the boundary computation is pure and deterministic, so
100
- re-derivation reproduces the id.
102
+ Rendition-independent by design: an AudioSegment is a BOUNDARY of the source
103
+ (an audio fact), shared across every model-input rendition of it. The
104
+ boundary computation is pure and deterministic, so re-derivation reproduces
105
+ the id; the model-input WAV lives on the `AudioRendition` child, not here.
101
106
  """
102
107
  ```
103
108
 
104
109
  ``` python
105
- def transcript_node_id(
110
+ def audio_rendition_node_id(
106
111
  audio_segment_id: str, # Owning AudioSegment node id
107
- transcriber: str, # Transcriber capability name (e.g. "cjm-transcription-plugin-whisper")
108
- config_hash: str, # Transcriber config hash
112
+ chain: List[str], # Ordered preprocessing-chain descriptors; [] = the raw convert-only rendition
113
+ ) -> str: # Deterministic AudioRendition node id
114
+ """
115
+ AudioRendition identity = (audio segment, preprocessing chain).
116
+
117
+ A rendition is one model-input audio OF an AudioSegment. The chain is the
118
+ ordered list of preprocessing steps that produced it (each an opaque
119
+ descriptor string, e.g. ``"source_separation:cjm-media-plugin-demucs@<cfg>"``);
120
+ an EMPTY chain is the raw convert-only rendition, whose id is therefore
121
+ stable and chain-free. The universal 16k-mono resample is implicit (not a
122
+ chain step) — so raw + vocals are distinct renditions under one shared
123
+ AudioSegment, and the layer's identity-mismatch check never collides them.
124
+ Re-derivable from the manifest chain alone (extenders recompute, never
125
+ search).
126
+ """
127
+ ```
128
+
129
+ ``` python
130
+ def transcript_node_id(
131
+ rendition_id: str, # Owning AudioRendition node id
132
+ transcriber: str, # Transcriber capability name (e.g. "cjm-transcription-plugin-whisper")
133
+ config_hash: str, # Transcriber config hash
109
134
  ) -> str: # Deterministic Transcript node id
110
135
  """
111
- Transcript identity = (audio segment, transcriber, config) — MIRRORS the
136
+ Transcript identity = (audio rendition, transcriber, config) — MIRRORS the
112
137
  capability cache key UNIQUE(audio_path, config_hash), so the graph node is
113
- the durable face of the cached row (the structural E13 fix).
138
+ the durable face of the cached row (the structural E13 fix). Keyed on the
139
+ RENDITION, so a raw vs vocals transcript of the same boundary are distinct.
114
140
  """
115
141
  ```
116
142
 
117
143
  ``` python
118
144
  def segment_node_id(
119
- audio_segment_id: str, # Owning AudioSegment node id
120
- vad_config_hash: str, # VAD capability config hash (skeleton identity input)
121
- chunk_start: float, # VAD chunk start (chunk-local seconds within the AudioSegment)
122
- chunk_end: float, # VAD chunk end (chunk-local seconds)
145
+ rendition_id: str, # Owning AudioRendition node id
146
+ vad_config_hash: str, # VAD capability config hash (skeleton identity input)
147
+ chunk_start: float, # VAD chunk start (chunk-local seconds within the rendition WAV)
148
+ chunk_end: float, # VAD chunk end (chunk-local seconds)
123
149
  ) -> str: # Deterministic Segment node id
124
150
  """
125
- Fine Segment identity = audio-side only (audio segment, VAD config, chunk
151
+ Fine Segment identity = audio-side only (audio rendition, VAD config, chunk
126
152
  range) — so the skeleton's ids are SHARED across transcribers by
127
- construction (C4 "store agreement once" falls out of identity design).
153
+ construction (C4 "store agreement once" falls out of identity design). Keyed
154
+ on the RENDITION: each rendition has its own fine spine (vocals isolation can
155
+ yield different VAD chunk boundaries than raw).
128
156
  """
129
157
  ```
130
158
 
@@ -166,23 +194,23 @@ class SourceNode:
166
194
  @dataclass
167
195
  class AudioSegmentNode:
168
196
  """
169
- Coarse ~5-min spine member. Owns the model-input WAV (E14: the audio of
170
- record) as payload/provenance the WAV is NOT its own node.
197
+ Coarse ~5-min spine member: a BOUNDARY range of the Source (an audio fact),
198
+ shared across every model-input rendition of it.
171
199
 
172
- Provenance note (deliberate): the SourceRef anchors the owned model-input
173
- WAV artifact (hash_file-verifiable). A hash over the sliced ORIGINAL source
174
- bytes is not materializable without extracting per-range artifacts (decoded
175
- ranges are not byte ranges); the structural chain to the Source rides the
176
- PART_OF edge + the start/end properties.
200
+ Hashless by design: a boundary is not a materialized artifact — a hash over
201
+ the sliced ORIGINAL source bytes is not materializable without extracting
202
+ per-range artifacts (decoded ranges are not byte ranges). The materialized
203
+ model-input WAV lives on the `AudioRendition` child (which carries its
204
+ content hash); the structural chain to the Source rides the PART_OF edge +
205
+ the start/end properties. `segment_path` (the source-codec cut) stays here as
206
+ a rendition-independent archival pointer.
177
207
  """
178
208
 
179
209
  source: str # Owning Source node id
180
210
  index: int # Position in the coarse spine (0-based)
181
211
  start: float # Boundary start (source-coordinate seconds)
182
212
  end: float # Boundary end (source-coordinate seconds)
183
- model_input_path: str # The 16kHz mono WAV consumed by transcription/VAD/FA
184
- model_input_hash: str # Content hash over that WAV
185
- segment_path: Optional[str] # Source-codec cut file (archival pointer)
213
+ segment_path: Optional[str] # Source-codec cut file (rendition-independent archival pointer)
186
214
 
187
215
  def id(self) -> str: # Deterministic node id
188
216
  """Deterministic node id."""
@@ -192,30 +220,69 @@ class AudioSegmentNode:
192
220
  "Deterministic node id."
193
221
 
194
222
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
195
- """Build the AudioSegment node wire dict."""
223
+ """Build the AudioSegment node wire dict (hashless boundary; sources empty)."""
196
224
  props: Dict[str, Any] = {
197
- "Build the AudioSegment node wire dict."
225
+ "Build the AudioSegment node wire dict (hashless boundary; sources empty)."
226
+ ```
227
+
228
+ ``` python
229
+ @dataclass
230
+ class AudioRenditionNode:
231
+ """
232
+ A model-input rendition OF an AudioSegment — the materialized 16k-mono WAV
233
+ consumed by transcription/VAD/FA. OWNS the model-input (E14: the audio of
234
+ record); the AudioSegment above it is a hashless boundary.
235
+
236
+ Identity = (audio segment, preprocessing chain). The raw convert-only path is
237
+ an empty chain (its id is stable + chain-free); vocals isolation / future
238
+ speech-enhancement are non-empty chains. Raw + vocals are therefore distinct
239
+ renditions that COEXIST under one AudioSegment — the divergent model-input
240
+ hash that used to collide the AudioSegment now lives on distinct rendition
241
+ nodes. Connects UP to its AudioSegment by DERIVED_FROM (it is derived from the
242
+ segment's audio); the production act is recorded separately by a CR-18
243
+ `Derivation` event.
244
+ """
245
+
246
+ audio_segment: str # Owning AudioSegment node id
247
+ model_input_path: str # The 16kHz mono WAV consumed by transcription/VAD/FA
248
+ model_input_hash: str # Content hash over that WAV (the rendition's identity-of-record artifact)
249
+ chain: List[str] = field(...) # Ordered preprocessing-chain descriptors; [] = raw convert-only
250
+
251
+ def id(self) -> str: # Deterministic node id
252
+ """Deterministic node id."""
253
+ return audio_rendition_node_id(self.audio_segment, self.chain)
254
+
255
+ def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
256
+ "Deterministic node id."
257
+
258
+ def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
259
+ """Build the AudioRendition node wire dict (owns the model-input SourceRef)."""
260
+ props: Dict[str, Any] = {
261
+ "Build the AudioRendition node wire dict (owns the model-input SourceRef)."
262
+
263
+ def derived_edge(self) -> Dict[str, Any]: # Edge wire dict
264
+ "DERIVED_FROM edge: this rendition derives from its AudioSegment's audio."
198
265
  ```
199
266
 
200
267
  ``` python
201
268
  @dataclass
202
269
  class TranscriptNode:
203
270
  """
204
- One transcriber's text for one AudioSegment (per-transcriber variants at
271
+ One transcriber's text for one AudioRendition (per-transcriber variants at
205
272
  the coarse layer — cross-transcriber divergence lives here, C4/C14).
206
273
  """
207
274
 
208
- audio_segment: str # Owning AudioSegment node id
275
+ rendition: str # Owning AudioRendition node id
209
276
  transcriber: str # Transcriber capability name
210
277
  config_hash: str # Transcriber config hash
211
278
  text: str # The transcript text (stored ONCE here; fine Segments slice into it)
212
- audio_hash: str # Content hash of the consumed model-input WAV
279
+ audio_hash: str # Content hash of the consumed model-input WAV (the rendition's)
213
280
  metadata: Dict[str, Any] = field(...) # Transcriber-reported metadata
214
281
  asserted_at: Optional[float] # Derivation timestamp; None = now
215
282
 
216
283
  def id(self) -> str: # Deterministic node id
217
284
  """Deterministic node id."""
218
- return transcript_node_id(self.audio_segment, self.transcriber, self.config_hash)
285
+ return transcript_node_id(self.rendition, self.transcriber, self.config_hash)
219
286
 
220
287
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
221
288
  "Deterministic node id."
@@ -226,7 +293,7 @@ class TranscriptNode:
226
293
  "Build the Transcript node wire dict (capability attribution included)."
227
294
 
228
295
  def derived_edge(self) -> Dict[str, Any]: # Edge wire dict
229
- "DERIVED_FROM edge: this Transcript derives from its AudioSegment."
296
+ "DERIVED_FROM edge: this Transcript derives from its AudioRendition."
230
297
  ```
231
298
 
232
299
  ``` python
@@ -252,7 +319,8 @@ class TranscriptSliceRef:
252
319
  class SegmentNode:
253
320
  """
254
321
  Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
255
- text (the immutable-audio/mutable-text spine seam).
322
+ text (the immutable-audio/mutable-text spine seam). PART_OF its AudioRendition
323
+ (each rendition has its own fine spine).
256
324
 
257
325
  Layer-0 `text` is the ACCURACY transcriber's alignment; the designation is
258
326
  per-segment provenance, not global config (`text_from` names the
@@ -260,26 +328,26 @@ class SegmentNode:
260
328
  `text_slices`, the authoritative one included).
261
329
  """
262
330
 
263
- audio_segment: str # Owning AudioSegment node id
331
+ rendition: str # Owning AudioRendition node id
264
332
  vad_config_hash: str # VAD config hash (skeleton identity input)
265
- chunk_start: float # VAD chunk start (chunk-local seconds within the AudioSegment WAV)
333
+ chunk_start: float # VAD chunk start (chunk-local seconds within the rendition WAV)
266
334
  chunk_end: float # VAD chunk end (chunk-local seconds)
267
335
  index: int # Source-wide fine-spine index (0-based)
268
336
  start_time: float # Source-coordinate start (navigation)
269
337
  end_time: float # Source-coordinate end
270
338
  text: str = '' # Layer-0 text (accuracy alignment; "" = no aligned words, D14 class)
271
- audio_hash: str = '' # Content hash of the owning AudioSegment's model-input WAV
339
+ audio_hash: str = '' # Content hash of the owning rendition's model-input WAV
272
340
  source: Optional[str] # Source node id (convenience for direct filters)
273
341
  text_from: Optional[str] # Authoritative Transcript node id (None when text is empty)
274
342
  text_slices: List[TranscriptSliceRef] = field(...) # All per-transcriber slice refs
275
343
 
276
344
  def id(self) -> str: # Deterministic node id
277
- """Deterministic node id (audio-side identity; shared across transcribers)."""
278
- return segment_node_id(self.audio_segment, self.vad_config_hash,
345
+ """Deterministic node id (audio-side identity; shared across transcribers, per-rendition)."""
346
+ return segment_node_id(self.rendition, self.vad_config_hash,
279
347
  self.chunk_start, self.chunk_end)
280
348
 
281
349
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
282
- "Deterministic node id (audio-side identity; shared across transcribers)."
350
+ "Deterministic node id (audio-side identity; shared across transcribers, per-rendition)."
283
351
 
284
352
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
285
353
  """Build the Segment node wire dict (audio ref + per-transcriber text slice refs)."""
@@ -12,7 +12,7 @@ pip install cjm_transcript_graph_schema
12
12
  ## Project Structure
13
13
 
14
14
  nbs/
15
- └── schema.ipynb # The audio-transcript layer schema (where-graph-begins locked layer schema): `Source -> AudioSegment(coarse, owns the model-input WAV) -> Transcript(per-transcriber variants)` emitted by transcription, extended with the fine `Segment` spine by decomposition. Deterministic identity tuples per the stage-5 ratified rule; `Document` from the pre-CR-18 era dissolves into `Source`.
15
+ └── schema.ipynb # 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`.
16
16
 
17
17
  Total: 1 notebook
18
18
 
@@ -37,10 +37,12 @@ Detailed documentation for each module in the project:
37
37
 
38
38
  > The audio-transcript layer schema (where-graph-begins locked layer
39
39
  > schema):
40
- > `Source -> AudioSegment(coarse, owns the model-input WAV) -> Transcript(per-transcriber variants)`
41
- > emitted by transcription, extended with the fine `Segment` spine by
42
- > decomposition. Deterministic identity tuples per the stage-5 ratified
43
- > rule; `Document` from the pre-CR-18 era dissolves into `Source`.
40
+ > `Source -> AudioSegment(coarse boundary) -> AudioRendition(model-input: raw | vocals | …) -> Transcript(per-transcriber variants)`
41
+ > emitted by transcription, extended with the fine `Segment` spine
42
+ > (per-rendition) by decomposition. Deterministic identity tuples per
43
+ > the stage-5 ratified rule; the `AudioRendition` node lets raw +
44
+ > preprocessed model-inputs of one boundary coexist in one graph.
45
+ > `Document` from the pre-CR-18 era dissolves into `Source`.
44
46
 
45
47
  #### Import
46
48
 
@@ -49,10 +51,12 @@ from cjm_transcript_graph_schema.schema import (
49
51
  TranscriptGraphLabels,
50
52
  source_node_id,
51
53
  audio_segment_node_id,
54
+ audio_rendition_node_id,
52
55
  transcript_node_id,
53
56
  segment_node_id,
54
57
  SourceNode,
55
58
  AudioSegmentNode,
59
+ AudioRenditionNode,
56
60
  TranscriptNode,
57
61
  TranscriptSliceRef,
58
62
  SegmentNode
@@ -77,36 +81,60 @@ def audio_segment_node_id(
77
81
  """
78
82
  AudioSegment identity = (source, boundary range).
79
83
 
80
- Conversion-config-independent: the model-input WAV is payload/provenance,
81
- not identity the boundary computation is pure and deterministic, so
82
- re-derivation reproduces the id.
84
+ Rendition-independent by design: an AudioSegment is a BOUNDARY of the source
85
+ (an audio fact), shared across every model-input rendition of it. The
86
+ boundary computation is pure and deterministic, so re-derivation reproduces
87
+ the id; the model-input WAV lives on the `AudioRendition` child, not here.
83
88
  """
84
89
  ```
85
90
 
86
91
  ``` python
87
- def transcript_node_id(
92
+ def audio_rendition_node_id(
88
93
  audio_segment_id: str, # Owning AudioSegment node id
89
- transcriber: str, # Transcriber capability name (e.g. "cjm-transcription-plugin-whisper")
90
- config_hash: str, # Transcriber config hash
94
+ chain: List[str], # Ordered preprocessing-chain descriptors; [] = the raw convert-only rendition
95
+ ) -> str: # Deterministic AudioRendition node id
96
+ """
97
+ AudioRendition identity = (audio segment, preprocessing chain).
98
+
99
+ A rendition is one model-input audio OF an AudioSegment. The chain is the
100
+ ordered list of preprocessing steps that produced it (each an opaque
101
+ descriptor string, e.g. ``"source_separation:cjm-media-plugin-demucs@<cfg>"``);
102
+ an EMPTY chain is the raw convert-only rendition, whose id is therefore
103
+ stable and chain-free. The universal 16k-mono resample is implicit (not a
104
+ chain step) — so raw + vocals are distinct renditions under one shared
105
+ AudioSegment, and the layer's identity-mismatch check never collides them.
106
+ Re-derivable from the manifest chain alone (extenders recompute, never
107
+ search).
108
+ """
109
+ ```
110
+
111
+ ``` python
112
+ def transcript_node_id(
113
+ rendition_id: str, # Owning AudioRendition node id
114
+ transcriber: str, # Transcriber capability name (e.g. "cjm-transcription-plugin-whisper")
115
+ config_hash: str, # Transcriber config hash
91
116
  ) -> str: # Deterministic Transcript node id
92
117
  """
93
- Transcript identity = (audio segment, transcriber, config) — MIRRORS the
118
+ Transcript identity = (audio rendition, transcriber, config) — MIRRORS the
94
119
  capability cache key UNIQUE(audio_path, config_hash), so the graph node is
95
- the durable face of the cached row (the structural E13 fix).
120
+ the durable face of the cached row (the structural E13 fix). Keyed on the
121
+ RENDITION, so a raw vs vocals transcript of the same boundary are distinct.
96
122
  """
97
123
  ```
98
124
 
99
125
  ``` python
100
126
  def segment_node_id(
101
- audio_segment_id: str, # Owning AudioSegment node id
102
- vad_config_hash: str, # VAD capability config hash (skeleton identity input)
103
- chunk_start: float, # VAD chunk start (chunk-local seconds within the AudioSegment)
104
- chunk_end: float, # VAD chunk end (chunk-local seconds)
127
+ rendition_id: str, # Owning AudioRendition node id
128
+ vad_config_hash: str, # VAD capability config hash (skeleton identity input)
129
+ chunk_start: float, # VAD chunk start (chunk-local seconds within the rendition WAV)
130
+ chunk_end: float, # VAD chunk end (chunk-local seconds)
105
131
  ) -> str: # Deterministic Segment node id
106
132
  """
107
- Fine Segment identity = audio-side only (audio segment, VAD config, chunk
133
+ Fine Segment identity = audio-side only (audio rendition, VAD config, chunk
108
134
  range) — so the skeleton's ids are SHARED across transcribers by
109
- construction (C4 "store agreement once" falls out of identity design).
135
+ construction (C4 "store agreement once" falls out of identity design). Keyed
136
+ on the RENDITION: each rendition has its own fine spine (vocals isolation can
137
+ yield different VAD chunk boundaries than raw).
110
138
  """
111
139
  ```
112
140
 
@@ -148,23 +176,23 @@ class SourceNode:
148
176
  @dataclass
149
177
  class AudioSegmentNode:
150
178
  """
151
- Coarse ~5-min spine member. Owns the model-input WAV (E14: the audio of
152
- record) as payload/provenance the WAV is NOT its own node.
179
+ Coarse ~5-min spine member: a BOUNDARY range of the Source (an audio fact),
180
+ shared across every model-input rendition of it.
153
181
 
154
- Provenance note (deliberate): the SourceRef anchors the owned model-input
155
- WAV artifact (hash_file-verifiable). A hash over the sliced ORIGINAL source
156
- bytes is not materializable without extracting per-range artifacts (decoded
157
- ranges are not byte ranges); the structural chain to the Source rides the
158
- PART_OF edge + the start/end properties.
182
+ Hashless by design: a boundary is not a materialized artifact — a hash over
183
+ the sliced ORIGINAL source bytes is not materializable without extracting
184
+ per-range artifacts (decoded ranges are not byte ranges). The materialized
185
+ model-input WAV lives on the `AudioRendition` child (which carries its
186
+ content hash); the structural chain to the Source rides the PART_OF edge +
187
+ the start/end properties. `segment_path` (the source-codec cut) stays here as
188
+ a rendition-independent archival pointer.
159
189
  """
160
190
 
161
191
  source: str # Owning Source node id
162
192
  index: int # Position in the coarse spine (0-based)
163
193
  start: float # Boundary start (source-coordinate seconds)
164
194
  end: float # Boundary end (source-coordinate seconds)
165
- model_input_path: str # The 16kHz mono WAV consumed by transcription/VAD/FA
166
- model_input_hash: str # Content hash over that WAV
167
- segment_path: Optional[str] # Source-codec cut file (archival pointer)
195
+ segment_path: Optional[str] # Source-codec cut file (rendition-independent archival pointer)
168
196
 
169
197
  def id(self) -> str: # Deterministic node id
170
198
  """Deterministic node id."""
@@ -174,30 +202,69 @@ class AudioSegmentNode:
174
202
  "Deterministic node id."
175
203
 
176
204
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
177
- """Build the AudioSegment node wire dict."""
205
+ """Build the AudioSegment node wire dict (hashless boundary; sources empty)."""
178
206
  props: Dict[str, Any] = {
179
- "Build the AudioSegment node wire dict."
207
+ "Build the AudioSegment node wire dict (hashless boundary; sources empty)."
208
+ ```
209
+
210
+ ``` python
211
+ @dataclass
212
+ class AudioRenditionNode:
213
+ """
214
+ A model-input rendition OF an AudioSegment — the materialized 16k-mono WAV
215
+ consumed by transcription/VAD/FA. OWNS the model-input (E14: the audio of
216
+ record); the AudioSegment above it is a hashless boundary.
217
+
218
+ Identity = (audio segment, preprocessing chain). The raw convert-only path is
219
+ an empty chain (its id is stable + chain-free); vocals isolation / future
220
+ speech-enhancement are non-empty chains. Raw + vocals are therefore distinct
221
+ renditions that COEXIST under one AudioSegment — the divergent model-input
222
+ hash that used to collide the AudioSegment now lives on distinct rendition
223
+ nodes. Connects UP to its AudioSegment by DERIVED_FROM (it is derived from the
224
+ segment's audio); the production act is recorded separately by a CR-18
225
+ `Derivation` event.
226
+ """
227
+
228
+ audio_segment: str # Owning AudioSegment node id
229
+ model_input_path: str # The 16kHz mono WAV consumed by transcription/VAD/FA
230
+ model_input_hash: str # Content hash over that WAV (the rendition's identity-of-record artifact)
231
+ chain: List[str] = field(...) # Ordered preprocessing-chain descriptors; [] = raw convert-only
232
+
233
+ def id(self) -> str: # Deterministic node id
234
+ """Deterministic node id."""
235
+ return audio_rendition_node_id(self.audio_segment, self.chain)
236
+
237
+ def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
238
+ "Deterministic node id."
239
+
240
+ def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
241
+ """Build the AudioRendition node wire dict (owns the model-input SourceRef)."""
242
+ props: Dict[str, Any] = {
243
+ "Build the AudioRendition node wire dict (owns the model-input SourceRef)."
244
+
245
+ def derived_edge(self) -> Dict[str, Any]: # Edge wire dict
246
+ "DERIVED_FROM edge: this rendition derives from its AudioSegment's audio."
180
247
  ```
181
248
 
182
249
  ``` python
183
250
  @dataclass
184
251
  class TranscriptNode:
185
252
  """
186
- One transcriber's text for one AudioSegment (per-transcriber variants at
253
+ One transcriber's text for one AudioRendition (per-transcriber variants at
187
254
  the coarse layer — cross-transcriber divergence lives here, C4/C14).
188
255
  """
189
256
 
190
- audio_segment: str # Owning AudioSegment node id
257
+ rendition: str # Owning AudioRendition node id
191
258
  transcriber: str # Transcriber capability name
192
259
  config_hash: str # Transcriber config hash
193
260
  text: str # The transcript text (stored ONCE here; fine Segments slice into it)
194
- audio_hash: str # Content hash of the consumed model-input WAV
261
+ audio_hash: str # Content hash of the consumed model-input WAV (the rendition's)
195
262
  metadata: Dict[str, Any] = field(...) # Transcriber-reported metadata
196
263
  asserted_at: Optional[float] # Derivation timestamp; None = now
197
264
 
198
265
  def id(self) -> str: # Deterministic node id
199
266
  """Deterministic node id."""
200
- return transcript_node_id(self.audio_segment, self.transcriber, self.config_hash)
267
+ return transcript_node_id(self.rendition, self.transcriber, self.config_hash)
201
268
 
202
269
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
203
270
  "Deterministic node id."
@@ -208,7 +275,7 @@ class TranscriptNode:
208
275
  "Build the Transcript node wire dict (capability attribution included)."
209
276
 
210
277
  def derived_edge(self) -> Dict[str, Any]: # Edge wire dict
211
- "DERIVED_FROM edge: this Transcript derives from its AudioSegment."
278
+ "DERIVED_FROM edge: this Transcript derives from its AudioRendition."
212
279
  ```
213
280
 
214
281
  ``` python
@@ -234,7 +301,8 @@ class TranscriptSliceRef:
234
301
  class SegmentNode:
235
302
  """
236
303
  Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
237
- text (the immutable-audio/mutable-text spine seam).
304
+ text (the immutable-audio/mutable-text spine seam). PART_OF its AudioRendition
305
+ (each rendition has its own fine spine).
238
306
 
239
307
  Layer-0 `text` is the ACCURACY transcriber's alignment; the designation is
240
308
  per-segment provenance, not global config (`text_from` names the
@@ -242,26 +310,26 @@ class SegmentNode:
242
310
  `text_slices`, the authoritative one included).
243
311
  """
244
312
 
245
- audio_segment: str # Owning AudioSegment node id
313
+ rendition: str # Owning AudioRendition node id
246
314
  vad_config_hash: str # VAD config hash (skeleton identity input)
247
- chunk_start: float # VAD chunk start (chunk-local seconds within the AudioSegment WAV)
315
+ chunk_start: float # VAD chunk start (chunk-local seconds within the rendition WAV)
248
316
  chunk_end: float # VAD chunk end (chunk-local seconds)
249
317
  index: int # Source-wide fine-spine index (0-based)
250
318
  start_time: float # Source-coordinate start (navigation)
251
319
  end_time: float # Source-coordinate end
252
320
  text: str = '' # Layer-0 text (accuracy alignment; "" = no aligned words, D14 class)
253
- audio_hash: str = '' # Content hash of the owning AudioSegment's model-input WAV
321
+ audio_hash: str = '' # Content hash of the owning rendition's model-input WAV
254
322
  source: Optional[str] # Source node id (convenience for direct filters)
255
323
  text_from: Optional[str] # Authoritative Transcript node id (None when text is empty)
256
324
  text_slices: List[TranscriptSliceRef] = field(...) # All per-transcriber slice refs
257
325
 
258
326
  def id(self) -> str: # Deterministic node id
259
- """Deterministic node id (audio-side identity; shared across transcribers)."""
260
- return segment_node_id(self.audio_segment, self.vad_config_hash,
327
+ """Deterministic node id (audio-side identity; shared across transcribers, per-rendition)."""
328
+ return segment_node_id(self.rendition, self.vad_config_hash,
261
329
  self.chunk_start, self.chunk_end)
262
330
 
263
331
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
264
- "Deterministic node id (audio-side identity; shared across transcribers)."
332
+ "Deterministic node id (audio-side identity; shared across transcribers, per-rendition)."
265
333
 
266
334
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
267
335
  """Build the Segment node wire dict (audio ref + per-transcriber text slice refs)."""
@@ -0,0 +1 @@
1
+ __version__ = "0.0.6"
@@ -5,7 +5,15 @@ d = { 'settings': { 'branch': 'main',
5
5
  'doc_host': 'https://cj-mills.github.io',
6
6
  'git_url': 'https://github.com/cj-mills/cjm-transcript-graph-schema',
7
7
  'lib_path': 'cjm_transcript_graph_schema'},
8
- 'syms': { 'cjm_transcript_graph_schema.schema': { 'cjm_transcript_graph_schema.schema.AudioSegmentNode': ( 'schema.html#audiosegmentnode',
8
+ 'syms': { 'cjm_transcript_graph_schema.schema': { 'cjm_transcript_graph_schema.schema.AudioRenditionNode': ( 'schema.html#audiorenditionnode',
9
+ 'cjm_transcript_graph_schema/schema.py'),
10
+ 'cjm_transcript_graph_schema.schema.AudioRenditionNode.derived_edge': ( 'schema.html#audiorenditionnode.derived_edge',
11
+ 'cjm_transcript_graph_schema/schema.py'),
12
+ 'cjm_transcript_graph_schema.schema.AudioRenditionNode.id': ( 'schema.html#audiorenditionnode.id',
13
+ 'cjm_transcript_graph_schema/schema.py'),
14
+ 'cjm_transcript_graph_schema.schema.AudioRenditionNode.to_graph_node': ( 'schema.html#audiorenditionnode.to_graph_node',
15
+ 'cjm_transcript_graph_schema/schema.py'),
16
+ 'cjm_transcript_graph_schema.schema.AudioSegmentNode': ( 'schema.html#audiosegmentnode',
9
17
  'cjm_transcript_graph_schema/schema.py'),
10
18
  'cjm_transcript_graph_schema.schema.AudioSegmentNode.id': ( 'schema.html#audiosegmentnode.id',
11
19
  'cjm_transcript_graph_schema/schema.py'),
@@ -39,6 +47,8 @@ d = { 'settings': { 'branch': 'main',
39
47
  'cjm_transcript_graph_schema/schema.py'),
40
48
  'cjm_transcript_graph_schema.schema.TranscriptSliceRef.to_source_ref': ( 'schema.html#transcriptsliceref.to_source_ref',
41
49
  'cjm_transcript_graph_schema/schema.py'),
50
+ 'cjm_transcript_graph_schema.schema.audio_rendition_node_id': ( 'schema.html#audio_rendition_node_id',
51
+ 'cjm_transcript_graph_schema/schema.py'),
42
52
  'cjm_transcript_graph_schema.schema.audio_segment_node_id': ( 'schema.html#audio_segment_node_id',
43
53
  'cjm_transcript_graph_schema/schema.py'),
44
54
  'cjm_transcript_graph_schema.schema.segment_node_id': ( 'schema.html#segment_node_id',
@@ -1,12 +1,13 @@
1
- """The audio-transcript layer schema (where-graph-begins locked layer schema): `Source -> AudioSegment(coarse, owns the model-input WAV) -> Transcript(per-transcriber variants)` emitted by transcription, extended with the fine `Segment` spine by decomposition. Deterministic identity tuples per the stage-5 ratified rule; `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. 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
3
  Docs: https://cj-mills.github.io/cjm-transcript-graph-schemaschema.html.md"""
4
4
 
5
5
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/schema.ipynb.
6
6
 
7
7
  # %% auto #0
8
- __all__ = ['TranscriptGraphLabels', 'source_node_id', 'audio_segment_node_id', 'transcript_node_id', 'segment_node_id',
9
- 'SourceNode', 'AudioSegmentNode', 'TranscriptNode', 'TranscriptSliceRef', 'SegmentNode']
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']
10
11
 
11
12
  # %% ../nbs/schema.ipynb #76d0dd1c
12
13
  from pathlib import Path
@@ -22,15 +23,16 @@ from cjm_context_graph_primitives.provenance import SourceRef
22
23
  # %% ../nbs/schema.ipynb #ff94773a
23
24
  class TranscriptGraphLabels:
24
25
  """Node labels of the audio-transcript layer schema."""
25
- SOURCE = "Source" # Provenance root: one ingested media file
26
- AUDIO_SEGMENT = "AudioSegment" # Coarse ~5-min spine member; owns the model-input WAV
27
- TRANSCRIPT = "Transcript" # Per-transcriber text variant of one AudioSegment
28
- SEGMENT = "Segment" # Fine spine member: one VAD chunk (immutable audio + correctable text)
26
+ SOURCE = "Source" # Provenance root: one ingested media file
27
+ AUDIO_SEGMENT = "AudioSegment" # Coarse ~5-min spine member: a boundary range of the Source (an audio FACT; rendition-independent)
28
+ AUDIO_RENDITION = "AudioRendition" # A model-input rendition OF an AudioSegment (raw convert | vocals | future enhancement); OWNS the model-input WAV
29
+ TRANSCRIPT = "Transcript" # Per-transcriber text variant of one AudioRendition
30
+ SEGMENT = "Segment" # Fine spine member: one VAD chunk (immutable audio + correctable text), per-rendition
29
31
 
30
32
  @classmethod
31
33
  def all(cls) -> list: # All schema labels
32
34
  """All schema labels."""
33
- return [cls.SOURCE, cls.AUDIO_SEGMENT, cls.TRANSCRIPT, cls.SEGMENT]
35
+ return [cls.SOURCE, cls.AUDIO_SEGMENT, cls.AUDIO_RENDITION, cls.TRANSCRIPT, cls.SEGMENT]
34
36
 
35
37
  # %% ../nbs/schema.ipynb #a5ea12db
36
38
  def source_node_id(
@@ -47,33 +49,55 @@ def audio_segment_node_id(
47
49
  ) -> str: # Deterministic AudioSegment node id
48
50
  """AudioSegment identity = (source, boundary range).
49
51
 
50
- Conversion-config-independent: the model-input WAV is payload/provenance,
51
- not identity the boundary computation is pure and deterministic, so
52
- re-derivation reproduces the id."""
52
+ Rendition-independent by design: an AudioSegment is a BOUNDARY of the source
53
+ (an audio fact), shared across every model-input rendition of it. The
54
+ boundary computation is pure and deterministic, so re-derivation reproduces
55
+ the id; the model-input WAV lives on the `AudioRendition` child, not here."""
53
56
  return derive_node_id("audio-segment", source_id, start, end)
54
57
 
58
+ # %% ../nbs/schema.ipynb #8e147ac9
59
+ def audio_rendition_node_id(
60
+ audio_segment_id: str, # Owning AudioSegment node id
61
+ chain: List[str], # Ordered preprocessing-chain descriptors; [] = the raw convert-only rendition
62
+ ) -> str: # Deterministic AudioRendition node id
63
+ """AudioRendition identity = (audio segment, preprocessing chain).
64
+
65
+ A rendition is one model-input audio OF an AudioSegment. The chain is the
66
+ ordered list of preprocessing steps that produced it (each an opaque
67
+ descriptor string, e.g. ``"source_separation:cjm-media-plugin-demucs@<cfg>"``);
68
+ an EMPTY chain is the raw convert-only rendition, whose id is therefore
69
+ stable and chain-free. The universal 16k-mono resample is implicit (not a
70
+ chain step) — so raw + vocals are distinct renditions under one shared
71
+ AudioSegment, and the layer's identity-mismatch check never collides them.
72
+ Re-derivable from the manifest chain alone (extenders recompute, never
73
+ search)."""
74
+ return derive_node_id("audio-rendition", audio_segment_id, *chain)
75
+
55
76
  # %% ../nbs/schema.ipynb #6fe30fc8
56
77
  def transcript_node_id(
57
- audio_segment_id: str, # Owning AudioSegment node id
58
- transcriber: str, # Transcriber capability name (e.g. "cjm-transcription-plugin-whisper")
59
- config_hash: str, # Transcriber config hash
78
+ rendition_id: str, # Owning AudioRendition node id
79
+ transcriber: str, # Transcriber capability name (e.g. "cjm-transcription-plugin-whisper")
80
+ config_hash: str, # Transcriber config hash
60
81
  ) -> str: # Deterministic Transcript node id
61
- """Transcript identity = (audio segment, transcriber, config) — MIRRORS the
82
+ """Transcript identity = (audio rendition, transcriber, config) — MIRRORS the
62
83
  capability cache key UNIQUE(audio_path, config_hash), so the graph node is
63
- the durable face of the cached row (the structural E13 fix)."""
64
- return derive_node_id("transcript", audio_segment_id, transcriber, config_hash)
84
+ the durable face of the cached row (the structural E13 fix). Keyed on the
85
+ RENDITION, so a raw vs vocals transcript of the same boundary are distinct."""
86
+ return derive_node_id("transcript", rendition_id, transcriber, config_hash)
65
87
 
66
88
  # %% ../nbs/schema.ipynb #f0e4f510
67
89
  def segment_node_id(
68
- audio_segment_id: str, # Owning AudioSegment node id
69
- vad_config_hash: str, # VAD capability config hash (skeleton identity input)
70
- chunk_start: float, # VAD chunk start (chunk-local seconds within the AudioSegment)
71
- chunk_end: float, # VAD chunk end (chunk-local seconds)
90
+ rendition_id: str, # Owning AudioRendition node id
91
+ vad_config_hash: str, # VAD capability config hash (skeleton identity input)
92
+ chunk_start: float, # VAD chunk start (chunk-local seconds within the rendition WAV)
93
+ chunk_end: float, # VAD chunk end (chunk-local seconds)
72
94
  ) -> str: # Deterministic Segment node id
73
- """Fine Segment identity = audio-side only (audio segment, VAD config, chunk
95
+ """Fine Segment identity = audio-side only (audio rendition, VAD config, chunk
74
96
  range) — so the skeleton's ids are SHARED across transcribers by
75
- construction (C4 "store agreement once" falls out of identity design)."""
76
- return derive_node_id("segment", audio_segment_id, vad_config_hash, chunk_start, chunk_end)
97
+ construction (C4 "store agreement once" falls out of identity design). Keyed
98
+ on the RENDITION: each rendition has its own fine spine (vocals isolation can
99
+ yield different VAD chunk boundaries than raw)."""
100
+ return derive_node_id("segment", rendition_id, vad_config_hash, chunk_start, chunk_end)
77
101
 
78
102
  # %% ../nbs/schema.ipynb #d768584a
79
103
  @dataclass
@@ -107,21 +131,21 @@ class SourceNode:
107
131
  # %% ../nbs/schema.ipynb #3d76c961
108
132
  @dataclass
109
133
  class AudioSegmentNode:
110
- """Coarse ~5-min spine member. Owns the model-input WAV (E14: the audio of
111
- record) as payload/provenance the WAV is NOT its own node.
112
-
113
- Provenance note (deliberate): the SourceRef anchors the owned model-input
114
- WAV artifact (hash_file-verifiable). A hash over the sliced ORIGINAL source
115
- bytes is not materializable without extracting per-range artifacts (decoded
116
- ranges are not byte ranges); the structural chain to the Source rides the
117
- PART_OF edge + the start/end properties."""
134
+ """Coarse ~5-min spine member: a BOUNDARY range of the Source (an audio fact),
135
+ shared across every model-input rendition of it.
136
+
137
+ Hashless by design: a boundary is not a materialized artifact — a hash over
138
+ the sliced ORIGINAL source bytes is not materializable without extracting
139
+ per-range artifacts (decoded ranges are not byte ranges). The materialized
140
+ model-input WAV lives on the `AudioRendition` child (which carries its
141
+ content hash); the structural chain to the Source rides the PART_OF edge +
142
+ the start/end properties. `segment_path` (the source-codec cut) stays here as
143
+ a rendition-independent archival pointer."""
118
144
  source: str # Owning Source node id
119
145
  index: int # Position in the coarse spine (0-based)
120
146
  start: float # Boundary start (source-coordinate seconds)
121
147
  end: float # Boundary end (source-coordinate seconds)
122
- model_input_path: str # The 16kHz mono WAV consumed by transcription/VAD/FA
123
- model_input_hash: str # Content hash over that WAV
124
- segment_path: Optional[str] = None # Source-codec cut file (archival pointer)
148
+ segment_path: Optional[str] = None # Source-codec cut file (rendition-independent archival pointer)
125
149
 
126
150
  @property
127
151
  def id(self) -> str: # Deterministic node id
@@ -129,12 +153,11 @@ class AudioSegmentNode:
129
153
  return audio_segment_node_id(self.source, self.start, self.end)
130
154
 
131
155
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
132
- """Build the AudioSegment node wire dict."""
156
+ """Build the AudioSegment node wire dict (hashless boundary; sources empty)."""
133
157
  props: Dict[str, Any] = {
134
158
  "index": self.index,
135
159
  "start": self.start,
136
160
  "end": self.end,
137
- "model_input_path": self.model_input_path,
138
161
  "source_id": self.source,
139
162
  }
140
163
  if self.segment_path:
@@ -143,27 +166,73 @@ class AudioSegmentNode:
143
166
  "id": self.id,
144
167
  "label": TranscriptGraphLabels.AUDIO_SEGMENT,
145
168
  "properties": props,
169
+ "sources": [],
170
+ }
171
+
172
+ # %% ../nbs/schema.ipynb #c97ae502
173
+ @dataclass
174
+ class AudioRenditionNode:
175
+ """A model-input rendition OF an AudioSegment — the materialized 16k-mono WAV
176
+ consumed by transcription/VAD/FA. OWNS the model-input (E14: the audio of
177
+ record); the AudioSegment above it is a hashless boundary.
178
+
179
+ Identity = (audio segment, preprocessing chain). The raw convert-only path is
180
+ an empty chain (its id is stable + chain-free); vocals isolation / future
181
+ speech-enhancement are non-empty chains. Raw + vocals are therefore distinct
182
+ renditions that COEXIST under one AudioSegment — the divergent model-input
183
+ hash that used to collide the AudioSegment now lives on distinct rendition
184
+ nodes. Connects UP to its AudioSegment by DERIVED_FROM (it is derived from the
185
+ segment's audio); the production act is recorded separately by a CR-18
186
+ `Derivation` event."""
187
+ audio_segment: str # Owning AudioSegment node id
188
+ model_input_path: str # The 16kHz mono WAV consumed by transcription/VAD/FA
189
+ model_input_hash: str # Content hash over that WAV (the rendition's identity-of-record artifact)
190
+ chain: List[str] = field(default_factory=list) # Ordered preprocessing-chain descriptors; [] = raw convert-only
191
+
192
+ @property
193
+ def id(self) -> str: # Deterministic node id
194
+ """Deterministic node id."""
195
+ return audio_rendition_node_id(self.audio_segment, self.chain)
196
+
197
+ def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
198
+ """Build the AudioRendition node wire dict (owns the model-input SourceRef)."""
199
+ props: Dict[str, Any] = {
200
+ "audio_segment_id": self.audio_segment,
201
+ "model_input_path": self.model_input_path,
202
+ "chain": list(self.chain),
203
+ "is_raw": not self.chain,
204
+ }
205
+ if self.chain:
206
+ props["preprocessing"] = " | ".join(self.chain) # readable display label (the retired interim property)
207
+ return {
208
+ "id": self.id,
209
+ "label": TranscriptGraphLabels.AUDIO_RENDITION,
210
+ "properties": props,
146
211
  "sources": [SourceRef(locator=FileRef(path=self.model_input_path),
147
212
  content_hash=self.model_input_hash).to_dict()],
148
213
  }
149
214
 
215
+ def derived_edge(self) -> Dict[str, Any]: # Edge wire dict
216
+ """DERIVED_FROM edge: this rendition derives from its AudioSegment's audio."""
217
+ return make_edge(self.id, self.audio_segment, OverlayRelations.DERIVED_FROM)
218
+
150
219
  # %% ../nbs/schema.ipynb #9948f2c5
151
220
  @dataclass
152
221
  class TranscriptNode:
153
- """One transcriber's text for one AudioSegment (per-transcriber variants at
222
+ """One transcriber's text for one AudioRendition (per-transcriber variants at
154
223
  the coarse layer — cross-transcriber divergence lives here, C4/C14)."""
155
- audio_segment: str # Owning AudioSegment node id
224
+ rendition: str # Owning AudioRendition node id
156
225
  transcriber: str # Transcriber capability name
157
226
  config_hash: str # Transcriber config hash
158
227
  text: str # The transcript text (stored ONCE here; fine Segments slice into it)
159
- audio_hash: str # Content hash of the consumed model-input WAV
228
+ audio_hash: str # Content hash of the consumed model-input WAV (the rendition's)
160
229
  metadata: Dict[str, Any] = field(default_factory=dict) # Transcriber-reported metadata
161
230
  asserted_at: Optional[float] = None # Derivation timestamp; None = now
162
231
 
163
232
  @property
164
233
  def id(self) -> str: # Deterministic node id
165
234
  """Deterministic node id."""
166
- return transcript_node_id(self.audio_segment, self.transcriber, self.config_hash)
235
+ return transcript_node_id(self.rendition, self.transcriber, self.config_hash)
167
236
 
168
237
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
169
238
  """Build the Transcript node wire dict (capability attribution included)."""
@@ -171,7 +240,7 @@ class TranscriptNode:
171
240
  "transcriber": self.transcriber,
172
241
  "config_hash": self.config_hash,
173
242
  "text": self.text,
174
- "audio_segment_id": self.audio_segment,
243
+ "rendition_id": self.rendition,
175
244
  }
176
245
  if self.metadata:
177
246
  props["metadata"] = dict(self.metadata)
@@ -181,14 +250,14 @@ class TranscriptNode:
181
250
  "id": self.id,
182
251
  "label": TranscriptGraphLabels.TRANSCRIPT,
183
252
  "properties": props,
184
- "sources": [SourceRef(locator=GraphNodeRef(node_id=self.audio_segment),
253
+ "sources": [SourceRef(locator=GraphNodeRef(node_id=self.rendition),
185
254
  content_hash=self.audio_hash,
186
255
  slice=FullContent("audio")).to_dict()],
187
256
  }
188
257
 
189
258
  def derived_edge(self) -> Dict[str, Any]: # Edge wire dict
190
- """DERIVED_FROM edge: this Transcript derives from its AudioSegment."""
191
- return make_edge(self.id, self.audio_segment, OverlayRelations.DERIVED_FROM)
259
+ """DERIVED_FROM edge: this Transcript derives from its AudioRendition."""
260
+ return make_edge(self.id, self.rendition, OverlayRelations.DERIVED_FROM)
192
261
 
193
262
  # %% ../nbs/schema.ipynb #0c830160
194
263
  @dataclass
@@ -211,29 +280,30 @@ class TranscriptSliceRef:
211
280
  @dataclass
212
281
  class SegmentNode:
213
282
  """Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
214
- text (the immutable-audio/mutable-text spine seam).
283
+ text (the immutable-audio/mutable-text spine seam). PART_OF its AudioRendition
284
+ (each rendition has its own fine spine).
215
285
 
216
286
  Layer-0 `text` is the ACCURACY transcriber's alignment; the designation is
217
287
  per-segment provenance, not global config (`text_from` names the
218
288
  authoritative Transcript; every transcriber's char range rides
219
289
  `text_slices`, the authoritative one included)."""
220
- audio_segment: str # Owning AudioSegment node id
290
+ rendition: str # Owning AudioRendition node id
221
291
  vad_config_hash: str # VAD config hash (skeleton identity input)
222
- chunk_start: float # VAD chunk start (chunk-local seconds within the AudioSegment WAV)
292
+ chunk_start: float # VAD chunk start (chunk-local seconds within the rendition WAV)
223
293
  chunk_end: float # VAD chunk end (chunk-local seconds)
224
294
  index: int # Source-wide fine-spine index (0-based)
225
295
  start_time: float # Source-coordinate start (navigation)
226
296
  end_time: float # Source-coordinate end
227
297
  text: str = "" # Layer-0 text (accuracy alignment; "" = no aligned words, D14 class)
228
- audio_hash: str = "" # Content hash of the owning AudioSegment's model-input WAV
298
+ audio_hash: str = "" # Content hash of the owning rendition's model-input WAV
229
299
  source: Optional[str] = None # Source node id (convenience for direct filters)
230
300
  text_from: Optional[str] = None # Authoritative Transcript node id (None when text is empty)
231
301
  text_slices: List[TranscriptSliceRef] = field(default_factory=list) # All per-transcriber slice refs
232
302
 
233
303
  @property
234
304
  def id(self) -> str: # Deterministic node id
235
- """Deterministic node id (audio-side identity; shared across transcribers)."""
236
- return segment_node_id(self.audio_segment, self.vad_config_hash,
305
+ """Deterministic node id (audio-side identity; shared across transcribers, per-rendition)."""
306
+ return segment_node_id(self.rendition, self.vad_config_hash,
237
307
  self.chunk_start, self.chunk_end)
238
308
 
239
309
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
@@ -245,13 +315,13 @@ class SegmentNode:
245
315
  "end_time": self.end_time,
246
316
  "chunk_start": self.chunk_start,
247
317
  "chunk_end": self.chunk_end,
248
- "audio_segment_id": self.audio_segment,
318
+ "rendition_id": self.rendition,
249
319
  }
250
320
  if self.source:
251
321
  props["source_id"] = self.source
252
322
  if self.text_from:
253
323
  props["text_from"] = self.text_from
254
- sources = [SourceRef(locator=GraphNodeRef(node_id=self.audio_segment),
324
+ sources = [SourceRef(locator=GraphNodeRef(node_id=self.rendition),
255
325
  content_hash=self.audio_hash,
256
326
  slice=TimeSlice(start=self.chunk_start, end=self.chunk_end)).to_dict()]
257
327
  sources.extend(ts.to_source_ref() for ts in self.text_slices)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-transcript-graph-schema
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
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
5
  Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
6
6
  License: Apache-2.0
@@ -30,7 +30,7 @@ pip install cjm_transcript_graph_schema
30
30
  ## Project Structure
31
31
 
32
32
  nbs/
33
- └── schema.ipynb # The audio-transcript layer schema (where-graph-begins locked layer schema): `Source -> AudioSegment(coarse, owns the model-input WAV) -> Transcript(per-transcriber variants)` emitted by transcription, extended with the fine `Segment` spine by decomposition. Deterministic identity tuples per the stage-5 ratified rule; `Document` from the pre-CR-18 era dissolves into `Source`.
33
+ └── schema.ipynb # 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`.
34
34
 
35
35
  Total: 1 notebook
36
36
 
@@ -55,10 +55,12 @@ Detailed documentation for each module in the project:
55
55
 
56
56
  > The audio-transcript layer schema (where-graph-begins locked layer
57
57
  > schema):
58
- > `Source -> AudioSegment(coarse, owns the model-input WAV) -> Transcript(per-transcriber variants)`
59
- > emitted by transcription, extended with the fine `Segment` spine by
60
- > decomposition. Deterministic identity tuples per the stage-5 ratified
61
- > rule; `Document` from the pre-CR-18 era dissolves into `Source`.
58
+ > `Source -> AudioSegment(coarse boundary) -> AudioRendition(model-input: raw | vocals | …) -> Transcript(per-transcriber variants)`
59
+ > emitted by transcription, extended with the fine `Segment` spine
60
+ > (per-rendition) by decomposition. Deterministic identity tuples per
61
+ > the stage-5 ratified rule; the `AudioRendition` node lets raw +
62
+ > preprocessed model-inputs of one boundary coexist in one graph.
63
+ > `Document` from the pre-CR-18 era dissolves into `Source`.
62
64
 
63
65
  #### Import
64
66
 
@@ -67,10 +69,12 @@ from cjm_transcript_graph_schema.schema import (
67
69
  TranscriptGraphLabels,
68
70
  source_node_id,
69
71
  audio_segment_node_id,
72
+ audio_rendition_node_id,
70
73
  transcript_node_id,
71
74
  segment_node_id,
72
75
  SourceNode,
73
76
  AudioSegmentNode,
77
+ AudioRenditionNode,
74
78
  TranscriptNode,
75
79
  TranscriptSliceRef,
76
80
  SegmentNode
@@ -95,36 +99,60 @@ def audio_segment_node_id(
95
99
  """
96
100
  AudioSegment identity = (source, boundary range).
97
101
 
98
- Conversion-config-independent: the model-input WAV is payload/provenance,
99
- not identity the boundary computation is pure and deterministic, so
100
- re-derivation reproduces the id.
102
+ Rendition-independent by design: an AudioSegment is a BOUNDARY of the source
103
+ (an audio fact), shared across every model-input rendition of it. The
104
+ boundary computation is pure and deterministic, so re-derivation reproduces
105
+ the id; the model-input WAV lives on the `AudioRendition` child, not here.
101
106
  """
102
107
  ```
103
108
 
104
109
  ``` python
105
- def transcript_node_id(
110
+ def audio_rendition_node_id(
106
111
  audio_segment_id: str, # Owning AudioSegment node id
107
- transcriber: str, # Transcriber capability name (e.g. "cjm-transcription-plugin-whisper")
108
- config_hash: str, # Transcriber config hash
112
+ chain: List[str], # Ordered preprocessing-chain descriptors; [] = the raw convert-only rendition
113
+ ) -> str: # Deterministic AudioRendition node id
114
+ """
115
+ AudioRendition identity = (audio segment, preprocessing chain).
116
+
117
+ A rendition is one model-input audio OF an AudioSegment. The chain is the
118
+ ordered list of preprocessing steps that produced it (each an opaque
119
+ descriptor string, e.g. ``"source_separation:cjm-media-plugin-demucs@<cfg>"``);
120
+ an EMPTY chain is the raw convert-only rendition, whose id is therefore
121
+ stable and chain-free. The universal 16k-mono resample is implicit (not a
122
+ chain step) — so raw + vocals are distinct renditions under one shared
123
+ AudioSegment, and the layer's identity-mismatch check never collides them.
124
+ Re-derivable from the manifest chain alone (extenders recompute, never
125
+ search).
126
+ """
127
+ ```
128
+
129
+ ``` python
130
+ def transcript_node_id(
131
+ rendition_id: str, # Owning AudioRendition node id
132
+ transcriber: str, # Transcriber capability name (e.g. "cjm-transcription-plugin-whisper")
133
+ config_hash: str, # Transcriber config hash
109
134
  ) -> str: # Deterministic Transcript node id
110
135
  """
111
- Transcript identity = (audio segment, transcriber, config) — MIRRORS the
136
+ Transcript identity = (audio rendition, transcriber, config) — MIRRORS the
112
137
  capability cache key UNIQUE(audio_path, config_hash), so the graph node is
113
- the durable face of the cached row (the structural E13 fix).
138
+ the durable face of the cached row (the structural E13 fix). Keyed on the
139
+ RENDITION, so a raw vs vocals transcript of the same boundary are distinct.
114
140
  """
115
141
  ```
116
142
 
117
143
  ``` python
118
144
  def segment_node_id(
119
- audio_segment_id: str, # Owning AudioSegment node id
120
- vad_config_hash: str, # VAD capability config hash (skeleton identity input)
121
- chunk_start: float, # VAD chunk start (chunk-local seconds within the AudioSegment)
122
- chunk_end: float, # VAD chunk end (chunk-local seconds)
145
+ rendition_id: str, # Owning AudioRendition node id
146
+ vad_config_hash: str, # VAD capability config hash (skeleton identity input)
147
+ chunk_start: float, # VAD chunk start (chunk-local seconds within the rendition WAV)
148
+ chunk_end: float, # VAD chunk end (chunk-local seconds)
123
149
  ) -> str: # Deterministic Segment node id
124
150
  """
125
- Fine Segment identity = audio-side only (audio segment, VAD config, chunk
151
+ Fine Segment identity = audio-side only (audio rendition, VAD config, chunk
126
152
  range) — so the skeleton's ids are SHARED across transcribers by
127
- construction (C4 "store agreement once" falls out of identity design).
153
+ construction (C4 "store agreement once" falls out of identity design). Keyed
154
+ on the RENDITION: each rendition has its own fine spine (vocals isolation can
155
+ yield different VAD chunk boundaries than raw).
128
156
  """
129
157
  ```
130
158
 
@@ -166,23 +194,23 @@ class SourceNode:
166
194
  @dataclass
167
195
  class AudioSegmentNode:
168
196
  """
169
- Coarse ~5-min spine member. Owns the model-input WAV (E14: the audio of
170
- record) as payload/provenance the WAV is NOT its own node.
197
+ Coarse ~5-min spine member: a BOUNDARY range of the Source (an audio fact),
198
+ shared across every model-input rendition of it.
171
199
 
172
- Provenance note (deliberate): the SourceRef anchors the owned model-input
173
- WAV artifact (hash_file-verifiable). A hash over the sliced ORIGINAL source
174
- bytes is not materializable without extracting per-range artifacts (decoded
175
- ranges are not byte ranges); the structural chain to the Source rides the
176
- PART_OF edge + the start/end properties.
200
+ Hashless by design: a boundary is not a materialized artifact — a hash over
201
+ the sliced ORIGINAL source bytes is not materializable without extracting
202
+ per-range artifacts (decoded ranges are not byte ranges). The materialized
203
+ model-input WAV lives on the `AudioRendition` child (which carries its
204
+ content hash); the structural chain to the Source rides the PART_OF edge +
205
+ the start/end properties. `segment_path` (the source-codec cut) stays here as
206
+ a rendition-independent archival pointer.
177
207
  """
178
208
 
179
209
  source: str # Owning Source node id
180
210
  index: int # Position in the coarse spine (0-based)
181
211
  start: float # Boundary start (source-coordinate seconds)
182
212
  end: float # Boundary end (source-coordinate seconds)
183
- model_input_path: str # The 16kHz mono WAV consumed by transcription/VAD/FA
184
- model_input_hash: str # Content hash over that WAV
185
- segment_path: Optional[str] # Source-codec cut file (archival pointer)
213
+ segment_path: Optional[str] # Source-codec cut file (rendition-independent archival pointer)
186
214
 
187
215
  def id(self) -> str: # Deterministic node id
188
216
  """Deterministic node id."""
@@ -192,30 +220,69 @@ class AudioSegmentNode:
192
220
  "Deterministic node id."
193
221
 
194
222
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
195
- """Build the AudioSegment node wire dict."""
223
+ """Build the AudioSegment node wire dict (hashless boundary; sources empty)."""
196
224
  props: Dict[str, Any] = {
197
- "Build the AudioSegment node wire dict."
225
+ "Build the AudioSegment node wire dict (hashless boundary; sources empty)."
226
+ ```
227
+
228
+ ``` python
229
+ @dataclass
230
+ class AudioRenditionNode:
231
+ """
232
+ A model-input rendition OF an AudioSegment — the materialized 16k-mono WAV
233
+ consumed by transcription/VAD/FA. OWNS the model-input (E14: the audio of
234
+ record); the AudioSegment above it is a hashless boundary.
235
+
236
+ Identity = (audio segment, preprocessing chain). The raw convert-only path is
237
+ an empty chain (its id is stable + chain-free); vocals isolation / future
238
+ speech-enhancement are non-empty chains. Raw + vocals are therefore distinct
239
+ renditions that COEXIST under one AudioSegment — the divergent model-input
240
+ hash that used to collide the AudioSegment now lives on distinct rendition
241
+ nodes. Connects UP to its AudioSegment by DERIVED_FROM (it is derived from the
242
+ segment's audio); the production act is recorded separately by a CR-18
243
+ `Derivation` event.
244
+ """
245
+
246
+ audio_segment: str # Owning AudioSegment node id
247
+ model_input_path: str # The 16kHz mono WAV consumed by transcription/VAD/FA
248
+ model_input_hash: str # Content hash over that WAV (the rendition's identity-of-record artifact)
249
+ chain: List[str] = field(...) # Ordered preprocessing-chain descriptors; [] = raw convert-only
250
+
251
+ def id(self) -> str: # Deterministic node id
252
+ """Deterministic node id."""
253
+ return audio_rendition_node_id(self.audio_segment, self.chain)
254
+
255
+ def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
256
+ "Deterministic node id."
257
+
258
+ def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
259
+ """Build the AudioRendition node wire dict (owns the model-input SourceRef)."""
260
+ props: Dict[str, Any] = {
261
+ "Build the AudioRendition node wire dict (owns the model-input SourceRef)."
262
+
263
+ def derived_edge(self) -> Dict[str, Any]: # Edge wire dict
264
+ "DERIVED_FROM edge: this rendition derives from its AudioSegment's audio."
198
265
  ```
199
266
 
200
267
  ``` python
201
268
  @dataclass
202
269
  class TranscriptNode:
203
270
  """
204
- One transcriber's text for one AudioSegment (per-transcriber variants at
271
+ One transcriber's text for one AudioRendition (per-transcriber variants at
205
272
  the coarse layer — cross-transcriber divergence lives here, C4/C14).
206
273
  """
207
274
 
208
- audio_segment: str # Owning AudioSegment node id
275
+ rendition: str # Owning AudioRendition node id
209
276
  transcriber: str # Transcriber capability name
210
277
  config_hash: str # Transcriber config hash
211
278
  text: str # The transcript text (stored ONCE here; fine Segments slice into it)
212
- audio_hash: str # Content hash of the consumed model-input WAV
279
+ audio_hash: str # Content hash of the consumed model-input WAV (the rendition's)
213
280
  metadata: Dict[str, Any] = field(...) # Transcriber-reported metadata
214
281
  asserted_at: Optional[float] # Derivation timestamp; None = now
215
282
 
216
283
  def id(self) -> str: # Deterministic node id
217
284
  """Deterministic node id."""
218
- return transcript_node_id(self.audio_segment, self.transcriber, self.config_hash)
285
+ return transcript_node_id(self.rendition, self.transcriber, self.config_hash)
219
286
 
220
287
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
221
288
  "Deterministic node id."
@@ -226,7 +293,7 @@ class TranscriptNode:
226
293
  "Build the Transcript node wire dict (capability attribution included)."
227
294
 
228
295
  def derived_edge(self) -> Dict[str, Any]: # Edge wire dict
229
- "DERIVED_FROM edge: this Transcript derives from its AudioSegment."
296
+ "DERIVED_FROM edge: this Transcript derives from its AudioRendition."
230
297
  ```
231
298
 
232
299
  ``` python
@@ -252,7 +319,8 @@ class TranscriptSliceRef:
252
319
  class SegmentNode:
253
320
  """
254
321
  Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
255
- text (the immutable-audio/mutable-text spine seam).
322
+ text (the immutable-audio/mutable-text spine seam). PART_OF its AudioRendition
323
+ (each rendition has its own fine spine).
256
324
 
257
325
  Layer-0 `text` is the ACCURACY transcriber's alignment; the designation is
258
326
  per-segment provenance, not global config (`text_from` names the
@@ -260,26 +328,26 @@ class SegmentNode:
260
328
  `text_slices`, the authoritative one included).
261
329
  """
262
330
 
263
- audio_segment: str # Owning AudioSegment node id
331
+ rendition: str # Owning AudioRendition node id
264
332
  vad_config_hash: str # VAD config hash (skeleton identity input)
265
- chunk_start: float # VAD chunk start (chunk-local seconds within the AudioSegment WAV)
333
+ chunk_start: float # VAD chunk start (chunk-local seconds within the rendition WAV)
266
334
  chunk_end: float # VAD chunk end (chunk-local seconds)
267
335
  index: int # Source-wide fine-spine index (0-based)
268
336
  start_time: float # Source-coordinate start (navigation)
269
337
  end_time: float # Source-coordinate end
270
338
  text: str = '' # Layer-0 text (accuracy alignment; "" = no aligned words, D14 class)
271
- audio_hash: str = '' # Content hash of the owning AudioSegment's model-input WAV
339
+ audio_hash: str = '' # Content hash of the owning rendition's model-input WAV
272
340
  source: Optional[str] # Source node id (convenience for direct filters)
273
341
  text_from: Optional[str] # Authoritative Transcript node id (None when text is empty)
274
342
  text_slices: List[TranscriptSliceRef] = field(...) # All per-transcriber slice refs
275
343
 
276
344
  def id(self) -> str: # Deterministic node id
277
- """Deterministic node id (audio-side identity; shared across transcribers)."""
278
- return segment_node_id(self.audio_segment, self.vad_config_hash,
345
+ """Deterministic node id (audio-side identity; shared across transcribers, per-rendition)."""
346
+ return segment_node_id(self.rendition, self.vad_config_hash,
279
347
  self.chunk_start, self.chunk_end)
280
348
 
281
349
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
282
- "Deterministic node id (audio-side identity; shared across transcribers)."
350
+ "Deterministic node id (audio-side identity; shared across transcribers, per-rendition)."
283
351
 
284
352
  def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
285
353
  """Build the Segment node wire dict (audio ref + per-transcriber text slice refs)."""
@@ -1 +0,0 @@
1
- __version__ = "0.0.5"