cjm-transcript-graph-schema 0.0.6__tar.gz → 0.0.8__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 (21) hide show
  1. {cjm_transcript_graph_schema-0.0.6 → cjm_transcript_graph_schema-0.0.8}/MANIFEST.in +0 -1
  2. cjm_transcript_graph_schema-0.0.8/PKG-INFO +52 -0
  3. cjm_transcript_graph_schema-0.0.8/README.md +35 -0
  4. cjm_transcript_graph_schema-0.0.8/cjm_transcript_graph_schema/__init__.py +1 -0
  5. {cjm_transcript_graph_schema-0.0.6 → cjm_transcript_graph_schema-0.0.8}/cjm_transcript_graph_schema/schema.py +104 -27
  6. cjm_transcript_graph_schema-0.0.8/cjm_transcript_graph_schema.egg-info/PKG-INFO +52 -0
  7. {cjm_transcript_graph_schema-0.0.6 → cjm_transcript_graph_schema-0.0.8}/cjm_transcript_graph_schema.egg-info/SOURCES.txt +2 -3
  8. cjm_transcript_graph_schema-0.0.8/cjm_transcript_graph_schema.egg-info/requires.txt +2 -0
  9. {cjm_transcript_graph_schema-0.0.6 → cjm_transcript_graph_schema-0.0.8}/pyproject.toml +2 -7
  10. cjm_transcript_graph_schema-0.0.8/tests/test_schema.py +176 -0
  11. cjm_transcript_graph_schema-0.0.6/PKG-INFO +0 -356
  12. cjm_transcript_graph_schema-0.0.6/README.md +0 -338
  13. cjm_transcript_graph_schema-0.0.6/cjm_transcript_graph_schema/__init__.py +0 -1
  14. cjm_transcript_graph_schema-0.0.6/cjm_transcript_graph_schema/_modidx.py +0 -59
  15. cjm_transcript_graph_schema-0.0.6/cjm_transcript_graph_schema.egg-info/PKG-INFO +0 -356
  16. cjm_transcript_graph_schema-0.0.6/cjm_transcript_graph_schema.egg-info/entry_points.txt +0 -2
  17. cjm_transcript_graph_schema-0.0.6/cjm_transcript_graph_schema.egg-info/requires.txt +0 -2
  18. {cjm_transcript_graph_schema-0.0.6 → cjm_transcript_graph_schema-0.0.8}/LICENSE +0 -0
  19. {cjm_transcript_graph_schema-0.0.6 → cjm_transcript_graph_schema-0.0.8}/cjm_transcript_graph_schema.egg-info/dependency_links.txt +0 -0
  20. {cjm_transcript_graph_schema-0.0.6 → cjm_transcript_graph_schema-0.0.8}/cjm_transcript_graph_schema.egg-info/top_level.txt +0 -0
  21. {cjm_transcript_graph_schema-0.0.6 → cjm_transcript_graph_schema-0.0.8}/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,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: cjm-transcript-graph-schema
3
+ Version: 0.0.8
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
+ - `SegmentNode` _class_ — Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
36
+ - `SourceNode` _class_ — The provenance root: one ingested media file.
37
+ - `TranscriptGraphLabels` _class_ — Node labels of the audio-transcript layer schema.
38
+ - `TranscriptNode` _class_ — One transcriber's text for one AudioRendition (per-transcriber variants at
39
+ - `TranscriptSliceRef` _class_ — One per-transcriber char-range reference for a fine Segment: where this
40
+ - `audio_rendition_node_id` _function_ — AudioRendition identity = (audio segment, preprocessing chain).
41
+ - `audio_segment_node_id` _function_ — AudioSegment identity = (source, boundary range).
42
+ - `collection_edges` _function_ — Membership edges for a collection (ae3464fc: ORDER IS OPTIONAL).
43
+ - `collection_node_id` _function_ — Collection identity = the NORMALIZED TITLE (ratified fork, hub-session
44
+ - `normalize_collection_title` _function_ — Normalize a collection title for identity derivation.
45
+ - `segment_node_id` _function_ — Fine Segment identity = audio-side only (audio rendition, VAD config, chunk
46
+ - `source_node_id` _function_ — Source identity = the ingested file's content hash (Thread-1 ingested-root identity).
47
+ - `transcript_node_id` _function_ — Transcript identity = (audio rendition, transcriber, config) — MIRRORS the
48
+
49
+ ## Dependencies
50
+
51
+ **Depends on:** `cjm-context-graph-layer`, `cjm-context-graph-primitives`
52
+ **Used by:** `cjm-context-graph-layer`, `cjm-transcript-correction-core`, `cjm-transcript-correction-tui`, `cjm-transcript-decomp-core`, `cjm-transcription-core`
@@ -0,0 +1,35 @@
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
+ - `SegmentNode` _class_ — Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
19
+ - `SourceNode` _class_ — The provenance root: one ingested media file.
20
+ - `TranscriptGraphLabels` _class_ — Node labels of the audio-transcript layer schema.
21
+ - `TranscriptNode` _class_ — One transcriber's text for one AudioRendition (per-transcriber variants at
22
+ - `TranscriptSliceRef` _class_ — One per-transcriber char-range reference for a fine Segment: where this
23
+ - `audio_rendition_node_id` _function_ — AudioRendition identity = (audio segment, preprocessing chain).
24
+ - `audio_segment_node_id` _function_ — AudioSegment identity = (source, boundary range).
25
+ - `collection_edges` _function_ — Membership edges for a collection (ae3464fc: ORDER IS OPTIONAL).
26
+ - `collection_node_id` _function_ — Collection identity = the NORMALIZED TITLE (ratified fork, hub-session
27
+ - `normalize_collection_title` _function_ — Normalize a collection title for identity derivation.
28
+ - `segment_node_id` _function_ — Fine Segment identity = audio-side only (audio rendition, VAD config, chunk
29
+ - `source_node_id` _function_ — Source identity = the ingested file's content hash (Thread-1 ingested-root identity).
30
+ - `transcript_node_id` _function_ — Transcript identity = (audio rendition, transcriber, config) — MIRRORS the
31
+
32
+ ## Dependencies
33
+
34
+ **Depends on:** `cjm-context-graph-layer`, `cjm-context-graph-primitives`
35
+ **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.8"
@@ -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,21 @@ 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
31
23
 
32
24
  @classmethod
33
25
  def all(cls) -> list: # All schema labels
34
26
  """All schema labels."""
35
- return [cls.SOURCE, cls.AUDIO_SEGMENT, cls.AUDIO_RENDITION, cls.TRANSCRIPT, cls.SEGMENT]
27
+ return [cls.SOURCE, cls.AUDIO_SEGMENT, cls.AUDIO_RENDITION, cls.TRANSCRIPT, cls.SEGMENT, cls.COLLECTION]
28
+
36
29
 
37
- # %% ../nbs/schema.ipynb #a5ea12db
38
30
  def source_node_id(
39
31
  content_hash: str, # Content hash of the source media file ("algo:hexdigest")
40
32
  ) -> str: # Deterministic Source node id
41
33
  """Source identity = the ingested file's content hash (Thread-1 ingested-root identity)."""
42
34
  return derive_node_id("source", content_hash)
43
35
 
44
- # %% ../nbs/schema.ipynb #794a019f
36
+
45
37
  def audio_segment_node_id(
46
38
  source_id: str, # Owning Source node id
47
39
  start: float, # Boundary start (source-coordinate seconds)
@@ -55,7 +47,7 @@ def audio_segment_node_id(
55
47
  the id; the model-input WAV lives on the `AudioRendition` child, not here."""
56
48
  return derive_node_id("audio-segment", source_id, start, end)
57
49
 
58
- # %% ../nbs/schema.ipynb #8e147ac9
50
+
59
51
  def audio_rendition_node_id(
60
52
  audio_segment_id: str, # Owning AudioSegment node id
61
53
  chain: List[str], # Ordered preprocessing-chain descriptors; [] = the raw convert-only rendition
@@ -73,7 +65,7 @@ def audio_rendition_node_id(
73
65
  search)."""
74
66
  return derive_node_id("audio-rendition", audio_segment_id, *chain)
75
67
 
76
- # %% ../nbs/schema.ipynb #6fe30fc8
68
+
77
69
  def transcript_node_id(
78
70
  rendition_id: str, # Owning AudioRendition node id
79
71
  transcriber: str, # Transcriber capability name (e.g. "cjm-transcription-plugin-whisper")
@@ -85,7 +77,7 @@ def transcript_node_id(
85
77
  RENDITION, so a raw vs vocals transcript of the same boundary are distinct."""
86
78
  return derive_node_id("transcript", rendition_id, transcriber, config_hash)
87
79
 
88
- # %% ../nbs/schema.ipynb #f0e4f510
80
+
89
81
  def segment_node_id(
90
82
  rendition_id: str, # Owning AudioRendition node id
91
83
  vad_config_hash: str, # VAD capability config hash (skeleton identity input)
@@ -99,7 +91,7 @@ def segment_node_id(
99
91
  yield different VAD chunk boundaries than raw)."""
100
92
  return derive_node_id("segment", rendition_id, vad_config_hash, chunk_start, chunk_end)
101
93
 
102
- # %% ../nbs/schema.ipynb #d768584a
94
+
103
95
  @dataclass
104
96
  class SourceNode:
105
97
  """The provenance root: one ingested media file."""
@@ -128,7 +120,7 @@ class SourceNode:
128
120
  content_hash=self.content_hash).to_dict()],
129
121
  }
130
122
 
131
- # %% ../nbs/schema.ipynb #3d76c961
123
+
132
124
  @dataclass
133
125
  class AudioSegmentNode:
134
126
  """Coarse ~5-min spine member: a BOUNDARY range of the Source (an audio fact),
@@ -169,7 +161,7 @@ class AudioSegmentNode:
169
161
  "sources": [],
170
162
  }
171
163
 
172
- # %% ../nbs/schema.ipynb #c97ae502
164
+
173
165
  @dataclass
174
166
  class AudioRenditionNode:
175
167
  """A model-input rendition OF an AudioSegment — the materialized 16k-mono WAV
@@ -216,7 +208,7 @@ class AudioRenditionNode:
216
208
  """DERIVED_FROM edge: this rendition derives from its AudioSegment's audio."""
217
209
  return make_edge(self.id, self.audio_segment, OverlayRelations.DERIVED_FROM)
218
210
 
219
- # %% ../nbs/schema.ipynb #9948f2c5
211
+
220
212
  @dataclass
221
213
  class TranscriptNode:
222
214
  """One transcriber's text for one AudioRendition (per-transcriber variants at
@@ -259,7 +251,7 @@ class TranscriptNode:
259
251
  """DERIVED_FROM edge: this Transcript derives from its AudioRendition."""
260
252
  return make_edge(self.id, self.rendition, OverlayRelations.DERIVED_FROM)
261
253
 
262
- # %% ../nbs/schema.ipynb #0c830160
254
+
263
255
  @dataclass
264
256
  class TranscriptSliceRef:
265
257
  """One per-transcriber char-range reference for a fine Segment: where this
@@ -276,7 +268,7 @@ class TranscriptSliceRef:
276
268
  content_hash=SourceRef.compute_hash(self.text.encode()),
277
269
  slice=CharSlice(start=self.start_char, end=self.end_char)).to_dict()
278
270
 
279
- # %% ../nbs/schema.ipynb #fecb183e
271
+
280
272
  @dataclass
281
273
  class SegmentNode:
282
274
  """Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
@@ -331,3 +323,88 @@ class SegmentNode:
331
323
  "properties": props,
332
324
  "sources": sources,
333
325
  }
326
+
327
+
328
+ def normalize_collection_title(
329
+ title: str, # Raw title (folder name, user input, ...)
330
+ ) -> str: # Normalized identity form
331
+ """Normalize a collection title for identity derivation.
332
+
333
+ Casefold + underscores-to-spaces + whitespace collapse, so a folder named
334
+ ``Hardcore_History`` and a hand-typed ``Hardcore History`` converge on the
335
+ SAME collection node (ae3464fc: select-existing and retype-the-title are
336
+ the same act by construction). Display keeps the raw title; only identity
337
+ normalizes."""
338
+ return " ".join(title.replace("_", " ").split()).casefold()
339
+
340
+
341
+ def collection_node_id(
342
+ title: str, # Collection title (normalized internally)
343
+ ) -> str: # Deterministic Collection node id
344
+ """Collection identity = the NORMALIZED TITLE (ratified fork, hub-session
345
+ 2026-07-21): path-based identity breaks workspace relocatability, and
346
+ member-hash identity breaks late-added members — late binding is a feature.
347
+ The same book ingested twice (different paths, different machines, members
348
+ accreted over weeks) is the same collection; rare title collisions are
349
+ propose/confirm reconciler territory."""
350
+ return derive_node_id("collection", normalize_collection_title(title))
351
+
352
+
353
+ @dataclass
354
+ class CollectionNode:
355
+ """An intrinsic collection (book, podcast series, lecture course) — the
356
+ fractal spine layer ABOVE Source (the grammar's "series -> episode
357
+ tomorrow", arrived).
358
+
359
+ Status rides the node (ae3464fc actor criterion): a human who accepted,
360
+ renamed, or selected the collection at run launch has CONFIRMED it;
361
+ untouched tool defaults (headless folder run) land PROPOSED — visible and
362
+ grouping-usable everywhere, flagged as awaiting a human. A folder is
363
+ EVIDENCE of a grouping, not proof: proposed collections are cheap to
364
+ rename, refile out of, or dissolve. root_kind=asserted — a collection is
365
+ a grouping ASSERTION about ingested content, not ingested content itself.
366
+ Collection PART_OF Collection nesting is grammar-free (no v0 tooling)."""
367
+ title: str # Display title (identity input via normalize_collection_title)
368
+ status: str = "proposed" # "proposed" (tool inference) | "confirmed" (human act)
369
+ actor: str = "human" # Who declared it (attribution; e.g. "human", "tui:transcription", "cli:transcribe")
370
+ asserted_at: Optional[float] = None # Declaration timestamp; None = now
371
+
372
+ @property
373
+ def id(self) -> str: # Deterministic node id
374
+ """Deterministic node id."""
375
+ return collection_node_id(self.title)
376
+
377
+ def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
378
+ """Build the Collection node wire dict (asserted root; attribution included)."""
379
+ props: Dict[str, Any] = {
380
+ "title": self.title,
381
+ "status": self.status,
382
+ "root_kind": "asserted",
383
+ }
384
+ props.update(attribution(self.actor, method="collection-declaration",
385
+ asserted_at=self.asserted_at))
386
+ return {
387
+ "id": self.id,
388
+ "label": TranscriptGraphLabels.COLLECTION,
389
+ "properties": props,
390
+ "sources": [],
391
+ }
392
+
393
+
394
+ def collection_edges(
395
+ collection_id: str, # Collection node id
396
+ member_ids: List[str], # Member Source node ids (declaration order when ordered=True)
397
+ ordered: bool = False, # True = materialize the order (NEXT chain + STARTS_WITH)
398
+ ) -> List[Dict[str, Any]]: # Edge wire dicts
399
+ """Membership edges for a collection (ae3464fc: ORDER IS OPTIONAL).
400
+
401
+ Unordered membership is just PART_OF member->collection — individually
402
+ accreted members (chapters run one at a time) attach without fabricating a
403
+ sequence. When an order IS known (a folder run's sorted expansion, or a
404
+ curation act), the full fractal spine pattern materializes via
405
+ spine_edges: PART_OF + NEXT chain + STARTS_WITH — identical grammar to
406
+ every layer below. Reordering and late insertion are curation ops, never
407
+ capture-time guesses."""
408
+ if ordered:
409
+ return spine_edges(collection_id, member_ids)
410
+ return [make_edge(m, collection_id, SpineRelations.PART_OF) for m in member_ids]
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: cjm-transcript-graph-schema
3
+ Version: 0.0.8
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
+ - `SegmentNode` _class_ — Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
36
+ - `SourceNode` _class_ — The provenance root: one ingested media file.
37
+ - `TranscriptGraphLabels` _class_ — Node labels of the audio-transcript layer schema.
38
+ - `TranscriptNode` _class_ — One transcriber's text for one AudioRendition (per-transcriber variants at
39
+ - `TranscriptSliceRef` _class_ — One per-transcriber char-range reference for a fine Segment: where this
40
+ - `audio_rendition_node_id` _function_ — AudioRendition identity = (audio segment, preprocessing chain).
41
+ - `audio_segment_node_id` _function_ — AudioSegment identity = (source, boundary range).
42
+ - `collection_edges` _function_ — Membership edges for a collection (ae3464fc: ORDER IS OPTIONAL).
43
+ - `collection_node_id` _function_ — Collection identity = the NORMALIZED TITLE (ratified fork, hub-session
44
+ - `normalize_collection_title` _function_ — Normalize a collection title for identity derivation.
45
+ - `segment_node_id` _function_ — Fine Segment identity = audio-side only (audio rendition, VAD config, chunk
46
+ - `source_node_id` _function_ — Source identity = the ingested file's content hash (Thread-1 ingested-root identity).
47
+ - `transcript_node_id` _function_ — Transcript identity = (audio rendition, transcriber, config) — MIRRORS the
48
+
49
+ ## Dependencies
50
+
51
+ **Depends on:** `cjm-context-graph-layer`, `cjm-context-graph-primitives`
52
+ **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
@@ -0,0 +1,2 @@
1
+ cjm-context-graph-layer>=0.0.7
2
+ cjm-context-graph-primitives>=0.0.12
@@ -10,19 +10,15 @@ 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",
17
17
  ]
18
- dependencies = ['cjm-context-graph-layer>=0.0.5', 'cjm-context-graph-primitives>=0.0.10']
18
+ dependencies = ['cjm-context-graph-layer>=0.0.7', 'cjm-context-graph-primitives>=0.0.12']
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]
@@ -0,0 +1,176 @@
1
+ """Tests for cjm_transcript_graph_schema.schema — identity tuples + wire shapes.
2
+
3
+ Projected from the schema notebook's four test cells at the c25780e8 flip."""
4
+ from cjm_context_graph_primitives.provenance import SourceRef
5
+ from cjm_transcript_graph_schema.schema import (AudioRenditionNode, AudioSegmentNode,
6
+ CollectionNode, SegmentNode, SourceNode,
7
+ TranscriptNode, TranscriptSliceRef,
8
+ audio_rendition_node_id,
9
+ audio_segment_node_id,
10
+ collection_edges, collection_node_id,
11
+ normalize_collection_title,
12
+ segment_node_id, source_node_id,
13
+ transcript_node_id)
14
+
15
+ SID = source_node_id("sha256:abc")
16
+ A1 = audio_segment_node_id(SID, 0.0, 300.2)
17
+ R_RAW = audio_rendition_node_id(A1, [])
18
+ R_VOX = audio_rendition_node_id(A1, ["source_separation:demucs@cfg"])
19
+
20
+
21
+ def test_identity_determinism_and_cross_type_distinctness():
22
+ assert SID == source_node_id("sha256:abc")
23
+ assert A1 == audio_segment_node_id(SID, 0.0, 300.2)
24
+ assert A1 != audio_segment_node_id(SID, 0.0, 300.3)
25
+ # renditions: empty chain (raw) is stable + distinct from any preprocessing chain
26
+ assert R_RAW == audio_rendition_node_id(A1, [])
27
+ assert R_VOX != R_RAW, "vocals rendition is a distinct node from raw"
28
+ assert R_RAW != A1, "the raw rendition is its own node, not the audio segment"
29
+ assert audio_rendition_node_id(A1, ["a", "b"]) != audio_rendition_node_id(A1, ["b", "a"]), \
30
+ "chain order matters"
31
+ # transcript / segment key on the RENDITION now
32
+ t1 = transcript_node_id(R_RAW, "whisper", "cfg1")
33
+ assert t1 != transcript_node_id(R_RAW, "voxtral", "cfg1")
34
+ assert t1 != transcript_node_id(R_RAW, "whisper", "cfg2")
35
+ assert t1 != transcript_node_id(R_VOX, "whisper", "cfg1"), "raw vs vocals transcript distinct"
36
+ s1 = segment_node_id(R_RAW, "vadcfg", 1.5, 4.25)
37
+ assert s1 == segment_node_id(R_RAW, "vadcfg", 1.5, 4.25)
38
+ assert s1 != segment_node_id(R_VOX, "vadcfg", 1.5, 4.25), "raw vs vocals fine spine distinct"
39
+ assert len({SID, A1, R_RAW, R_VOX, t1, s1}) == 6, "kinds never collide"
40
+
41
+
42
+ def test_source_audio_segment_rendition_wire_shapes():
43
+ src = SourceNode(content_hash="sha256:abc", path="/media/ep1.mp3")
44
+ n = src.to_graph_node()
45
+ assert n["label"] == "Source" and n["id"] == source_node_id("sha256:abc")
46
+ assert n["properties"]["title"] == "ep1" and n["properties"]["root_kind"] == "ingested"
47
+ assert n["sources"][0]["content_hash"] == "sha256:abc"
48
+ assert SourceNode(content_hash="sha256:abc", path="/x.mp3",
49
+ title="Custom").to_graph_node()["properties"]["title"] == "Custom"
50
+
51
+ # AudioSegment is a hashless boundary now (model-input moved to the rendition)
52
+ aseg = AudioSegmentNode(source=src.id, index=0, start=0.0, end=300.2,
53
+ segment_path="/cuts/seg0.mp3")
54
+ an = aseg.to_graph_node()
55
+ assert an["id"] == audio_segment_node_id(src.id, 0.0, 300.2)
56
+ assert an["label"] == "AudioSegment"
57
+ assert an["properties"]["source_id"] == src.id
58
+ assert an["properties"]["segment_path"] == "/cuts/seg0.mp3"
59
+ assert "model_input_path" not in an["properties"], "model-input lives on the rendition now"
60
+ assert an["sources"] == [], "boundary node is hashless"
61
+
62
+ # AudioRendition owns the model-input; raw vs vocals coexist under one AudioSegment
63
+ raw = AudioRenditionNode(audio_segment=aseg.id, model_input_path="/cache/seg0.wav",
64
+ model_input_hash="sha256:wav0")
65
+ rn = raw.to_graph_node()
66
+ assert rn["id"] == audio_rendition_node_id(aseg.id, []) and rn["label"] == "AudioRendition"
67
+ assert rn["properties"]["audio_segment_id"] == aseg.id
68
+ assert rn["properties"]["is_raw"] is True
69
+ assert rn["properties"]["chain"] == [] and "preprocessing" not in rn["properties"]
70
+ assert rn["sources"][0]["content_hash"] == "sha256:wav0"
71
+ re = raw.derived_edge()
72
+ assert re["relation_type"] == "DERIVED_FROM"
73
+ assert re["source_id"] == rn["id"] and re["target_id"] == aseg.id
74
+
75
+ vox = AudioRenditionNode(audio_segment=aseg.id, model_input_path="/cache/seg0_vocals.wav",
76
+ model_input_hash="sha256:wav0vox",
77
+ chain=["source_separation:demucs@cfg"])
78
+ vn = vox.to_graph_node()
79
+ assert vn["id"] != rn["id"], "vocals rendition coexists as a distinct node"
80
+ assert vn["properties"]["is_raw"] is False
81
+ assert vn["properties"]["preprocessing"] == "source_separation:demucs@cfg"
82
+ assert vn["sources"][0]["content_hash"] == "sha256:wav0vox"
83
+
84
+
85
+ def test_transcript_shape_attribution_and_derived_edge():
86
+ t = TranscriptNode(rendition=R_RAW, transcriber="whisper", config_hash="cfg1",
87
+ text="hello world", audio_hash="sha256:wav0",
88
+ metadata={"model": "base"}, asserted_at=123.0)
89
+ tn = t.to_graph_node()
90
+ assert tn["id"] == transcript_node_id(R_RAW, "whisper", "cfg1")
91
+ assert tn["properties"]["actor"] == "capability:whisper"
92
+ assert tn["properties"]["method"] == "transcribe"
93
+ assert tn["properties"]["asserted_at"] == 123.0
94
+ assert tn["properties"]["text"] == "hello world"
95
+ assert tn["properties"]["metadata"] == {"model": "base"}
96
+ assert tn["properties"]["rendition_id"] == R_RAW
97
+ assert tn["sources"][0]["slice"]["kind"] == "full"
98
+ e = t.derived_edge()
99
+ assert e["relation_type"] == "DERIVED_FROM"
100
+ assert e["source_id"] == tn["id"] and e["target_id"] == R_RAW
101
+ assert e["id"] == t.derived_edge()["id"], "deterministic edge id"
102
+
103
+
104
+ def test_segment_shape_slices_identity_and_empty():
105
+ acc = TranscriptSliceRef(transcript="t-acc", start_char=0, end_char=11, text="hello world")
106
+ var = TranscriptSliceRef(transcript="t-lw", start_char=0, end_char=10, text="helo world")
107
+ seg = SegmentNode(rendition=R_RAW, vad_config_hash="vadcfg", chunk_start=1.5, chunk_end=4.25,
108
+ index=7, start_time=101.5, end_time=104.25, text="hello world",
109
+ audio_hash="sha256:wav0", source=SID, text_from="t-acc",
110
+ text_slices=[acc, var])
111
+ sn = seg.to_graph_node()
112
+ assert sn["id"] == segment_node_id(R_RAW, "vadcfg", 1.5, 4.25)
113
+ assert sn["properties"]["text"] == "hello world" and sn["properties"]["text_from"] == "t-acc"
114
+ assert sn["properties"]["source_id"] == SID and sn["properties"]["rendition_id"] == R_RAW
115
+ assert len(sn["sources"]) == 3
116
+ audio_ref, acc_ref, var_ref = sn["sources"]
117
+ assert audio_ref["slice"]["kind"] == "time" and audio_ref["slice"]["start"] == 1.5
118
+ assert audio_ref["locator"]["node_id"] == R_RAW, "audio ref points at the rendition"
119
+ assert acc_ref["slice"]["kind"] == "char"
120
+ assert acc_ref["content_hash"] == SourceRef.compute_hash(b"hello world")
121
+ assert var_ref["content_hash"] == SourceRef.compute_hash(b"helo world")
122
+
123
+ # identity is audio-side: text changes do NOT change the id
124
+ seg2 = SegmentNode(rendition=R_RAW, vad_config_hash="vadcfg", chunk_start=1.5, chunk_end=4.25,
125
+ index=7, start_time=101.5, end_time=104.25, text="different text")
126
+ assert seg2.id == seg.id
127
+
128
+ # empty (D14-class) segment: audio ref only, no text_from
129
+ empty = SegmentNode(rendition=R_RAW, vad_config_hash="vadcfg", chunk_start=9.0, chunk_end=9.5,
130
+ index=8, start_time=109.0, end_time=109.5, audio_hash="sha256:wav0")
131
+ en = empty.to_graph_node()
132
+ assert en["properties"]["text"] == "" and "text_from" not in en["properties"]
133
+ assert len(en["sources"]) == 1
134
+
135
+
136
+ def test_collection_identity_wire_shape_and_edges():
137
+ # identity: normalized-title — folder-name and hand-typed forms converge
138
+ assert normalize_collection_title("Hardcore_History") == "hardcore history"
139
+ cid = collection_node_id("Hardcore_History")
140
+ assert cid == collection_node_id("hardcore history")
141
+ assert cid != collection_node_id("Supernova in the East")
142
+ assert cid != SID, "kinds never collide"
143
+
144
+ # wire shape: proposed tool default vs confirmed human act (ae3464fc actor criterion)
145
+ proposed = CollectionNode(title="Hardcore_History", actor="cli:transcribe",
146
+ asserted_at=123.0)
147
+ pn = proposed.to_graph_node()
148
+ assert pn["id"] == cid and pn["label"] == "Collection"
149
+ assert pn["properties"]["status"] == "proposed"
150
+ assert pn["properties"]["root_kind"] == "asserted"
151
+ assert pn["properties"]["actor"] == "cli:transcribe"
152
+ assert pn["properties"]["method"] == "collection-declaration"
153
+ assert pn["properties"]["asserted_at"] == 123.0
154
+ assert pn["properties"]["title"] == "Hardcore_History", "display keeps the raw title"
155
+ assert pn["sources"] == [], "a collection asserts grouping; it ingests nothing"
156
+ confirmed = CollectionNode(title="Hardcore History", status="confirmed").to_graph_node()
157
+ assert confirmed["id"] == cid, "confirming/renaming to an equivalent title is the same node"
158
+ assert confirmed["properties"]["status"] == "confirmed"
159
+
160
+ # membership edges: unordered = PART_OF only; ordered = full fractal spine
161
+ m1 = source_node_id("sha256:ch1")
162
+ m2 = source_node_id("sha256:ch2")
163
+ m3 = source_node_id("sha256:ch3")
164
+ loose = collection_edges(cid, [m1, m3])
165
+ assert [e["relation_type"] for e in loose] == ["PART_OF", "PART_OF"]
166
+ assert all(e["target_id"] == cid for e in loose)
167
+ ordered = collection_edges(cid, [m1, m2, m3], ordered=True)
168
+ rels = sorted(e["relation_type"] for e in ordered)
169
+ assert rels == ["NEXT", "NEXT", "PART_OF", "PART_OF", "PART_OF", "STARTS_WITH"]
170
+ starts = [e for e in ordered if e["relation_type"] == "STARTS_WITH"][0]
171
+ assert starts["source_id"] == cid and starts["target_id"] == m1
172
+ nexts = {(e["source_id"], e["target_id"]) for e in ordered if e["relation_type"] == "NEXT"}
173
+ assert nexts == {(m1, m2), (m2, m3)}
174
+ # late-added member attaches to the SAME node without touching the chain
175
+ late = collection_edges(cid, [source_node_id("sha256:ch2.5")])
176
+ assert late[0]["relation_type"] == "PART_OF" and late[0]["target_id"] == cid