cjm-transcript-graph-schema 0.0.8__tar.gz → 0.0.11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cjm_transcript_graph_schema-0.0.8/cjm_transcript_graph_schema.egg-info → cjm_transcript_graph_schema-0.0.11}/PKG-INFO +2 -1
- {cjm_transcript_graph_schema-0.0.8 → cjm_transcript_graph_schema-0.0.11}/README.md +1 -0
- cjm_transcript_graph_schema-0.0.11/cjm_transcript_graph_schema/__init__.py +1 -0
- {cjm_transcript_graph_schema-0.0.8 → cjm_transcript_graph_schema-0.0.11}/cjm_transcript_graph_schema/schema.py +137 -9
- {cjm_transcript_graph_schema-0.0.8 → cjm_transcript_graph_schema-0.0.11/cjm_transcript_graph_schema.egg-info}/PKG-INFO +2 -1
- {cjm_transcript_graph_schema-0.0.8 → cjm_transcript_graph_schema-0.0.11}/tests/test_schema.py +115 -0
- cjm_transcript_graph_schema-0.0.8/cjm_transcript_graph_schema/__init__.py +0 -1
- {cjm_transcript_graph_schema-0.0.8 → cjm_transcript_graph_schema-0.0.11}/LICENSE +0 -0
- {cjm_transcript_graph_schema-0.0.8 → cjm_transcript_graph_schema-0.0.11}/MANIFEST.in +0 -0
- {cjm_transcript_graph_schema-0.0.8 → cjm_transcript_graph_schema-0.0.11}/cjm_transcript_graph_schema.egg-info/SOURCES.txt +0 -0
- {cjm_transcript_graph_schema-0.0.8 → cjm_transcript_graph_schema-0.0.11}/cjm_transcript_graph_schema.egg-info/dependency_links.txt +0 -0
- {cjm_transcript_graph_schema-0.0.8 → cjm_transcript_graph_schema-0.0.11}/cjm_transcript_graph_schema.egg-info/requires.txt +0 -0
- {cjm_transcript_graph_schema-0.0.8 → cjm_transcript_graph_schema-0.0.11}/cjm_transcript_graph_schema.egg-info/top_level.txt +0 -0
- {cjm_transcript_graph_schema-0.0.8 → cjm_transcript_graph_schema-0.0.11}/pyproject.toml +0 -0
- {cjm_transcript_graph_schema-0.0.8 → cjm_transcript_graph_schema-0.0.11}/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.
|
|
3
|
+
Version: 0.0.11
|
|
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
|
|
@@ -32,6 +32,7 @@ Audio-transcript layer schema for context graphs: Source, AudioSegment, Transcri
|
|
|
32
32
|
- `AudioRenditionNode` _class_ — A model-input rendition OF an AudioSegment — the materialized 16k-mono WAV
|
|
33
33
|
- `AudioSegmentNode` _class_ — Coarse ~5-min spine member: a BOUNDARY range of the Source (an audio fact),
|
|
34
34
|
- `CollectionNode` _class_ — An intrinsic collection (book, podcast series, lecture course) — the
|
|
35
|
+
- `RespinedChunkEntry` _class_ — One chunk respine as the Source records it (0b4d5cfa (4)): the entry the
|
|
35
36
|
- `SegmentNode` _class_ — Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
|
|
36
37
|
- `SourceNode` _class_ — The provenance root: one ingested media file.
|
|
37
38
|
- `TranscriptGraphLabels` _class_ — Node labels of the audio-transcript layer schema.
|
|
@@ -15,6 +15,7 @@ Audio-transcript layer schema for context graphs: Source, AudioSegment, Transcri
|
|
|
15
15
|
- `AudioRenditionNode` _class_ — A model-input rendition OF an AudioSegment — the materialized 16k-mono WAV
|
|
16
16
|
- `AudioSegmentNode` _class_ — Coarse ~5-min spine member: a BOUNDARY range of the Source (an audio fact),
|
|
17
17
|
- `CollectionNode` _class_ — An intrinsic collection (book, podcast series, lecture course) — the
|
|
18
|
+
- `RespinedChunkEntry` _class_ — One chunk respine as the Source records it (0b4d5cfa (4)): the entry the
|
|
18
19
|
- `SegmentNode` _class_ — Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
|
|
19
20
|
- `SourceNode` _class_ — The provenance root: one ingested media file.
|
|
20
21
|
- `TranscriptGraphLabels` _class_ — Node labels of the audio-transcript layer schema.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.11"
|
|
@@ -20,11 +20,13 @@ class TranscriptGraphLabels:
|
|
|
20
20
|
TRANSCRIPT = "Transcript" # Per-transcriber text variant of one AudioRendition
|
|
21
21
|
SEGMENT = "Segment" # Fine spine member: one VAD chunk (immutable audio + correctable text), per-rendition
|
|
22
22
|
COLLECTION = "Collection" # Intrinsic collection (book / series / course): the fractal spine layer ABOVE Source
|
|
23
|
+
REFERENCE = "Reference" # Human-added resource link attached to a Source (publisher page / author post / related notes / cited work): Source -HAS_REFERENCE-> Reference (ruling a7ca900d (3), item ae103970)
|
|
23
24
|
|
|
24
25
|
@classmethod
|
|
25
26
|
def all(cls) -> list: # All schema labels
|
|
26
27
|
"""All schema labels."""
|
|
27
|
-
return [cls.SOURCE, cls.AUDIO_SEGMENT, cls.AUDIO_RENDITION, cls.TRANSCRIPT, cls.SEGMENT, cls.COLLECTION
|
|
28
|
+
return [cls.SOURCE, cls.AUDIO_SEGMENT, cls.AUDIO_RENDITION, cls.TRANSCRIPT, cls.SEGMENT, cls.COLLECTION,
|
|
29
|
+
cls.REFERENCE]
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
def source_node_id(
|
|
@@ -78,17 +80,61 @@ def transcript_node_id(
|
|
|
78
80
|
return derive_node_id("transcript", rendition_id, transcriber, config_hash)
|
|
79
81
|
|
|
80
82
|
|
|
83
|
+
EXTERNAL_TRANSCRIBER_MARKER = "/manual" # Suffix naming an operator-landed (pasted) transcript producer
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def external_transcriber_name(
|
|
87
|
+
model_id: str, # The external model that produced the text (e.g. "gemini-2.5-pro")
|
|
88
|
+
) -> str: # The transcriber name an external landing files under
|
|
89
|
+
"""Transcriber name for an EXTERNAL landing (ruling 9ffce5f7 (1)): the model id
|
|
90
|
+
plus a manual marker, so a pasted transcript is a THIRD TRANSCRIBER beside the
|
|
91
|
+
local capabilities — decomp folds it per chunk like any other variant, and the
|
|
92
|
+
marker keeps it distinguishable from a capability run of the same model."""
|
|
93
|
+
mid = (model_id or "").strip()
|
|
94
|
+
if not mid:
|
|
95
|
+
raise ValueError("external_transcriber_name: model_id is required")
|
|
96
|
+
return mid + EXTERNAL_TRANSCRIBER_MARKER
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def external_config_hash(
|
|
100
|
+
model_id: str, # The external model id
|
|
101
|
+
prompt_hash: str, # Hash of the prompt template the operator used ("" = no prompt recorded)
|
|
102
|
+
) -> str: # "sha256:<hex>" — the config-hash slot of an external landing
|
|
103
|
+
"""The config hash an external landing carries where a capability's effective
|
|
104
|
+
config hash goes: sha256 over (model id, prompt hash). The prompt is DATA
|
|
105
|
+
(f304d31d), so a different prompt template is a different variant — and the
|
|
106
|
+
Transcript identity (rendition, transcriber, config_hash) stays recomputable
|
|
107
|
+
from the derived manifest alone."""
|
|
108
|
+
import hashlib
|
|
109
|
+
h = hashlib.sha256()
|
|
110
|
+
h.update((model_id or "").strip().encode("utf-8"))
|
|
111
|
+
h.update(b"\n")
|
|
112
|
+
h.update((prompt_hash or "").strip().encode("utf-8"))
|
|
113
|
+
return "sha256:" + h.hexdigest()
|
|
114
|
+
|
|
115
|
+
|
|
81
116
|
def segment_node_id(
|
|
82
117
|
rendition_id: str, # Owning AudioRendition node id
|
|
83
|
-
vad_config_hash: str, #
|
|
118
|
+
vad_config_hash: str, # Skeleton config hash (identity input: VAD config, + split policy when a split stage ran)
|
|
84
119
|
chunk_start: float, # VAD chunk start (chunk-local seconds within the rendition WAV)
|
|
85
120
|
chunk_end: float, # VAD chunk end (chunk-local seconds)
|
|
121
|
+
identity_salt: Optional[str] = None, # Respine salt (0b4d5cfa (3)): joins the id ONLY; None = the spine's original derivation
|
|
86
122
|
) -> str: # Deterministic Segment node id
|
|
87
123
|
"""Fine Segment identity = audio-side only (audio rendition, VAD config, chunk
|
|
88
124
|
range) — so the skeleton's ids are SHARED across transcribers by
|
|
89
125
|
construction (C4 "store agreement once" falls out of identity design). Keyed
|
|
90
126
|
on the RENDITION: each rendition has its own fine spine (vocals isolation can
|
|
91
|
-
yield different VAD chunk boundaries than raw).
|
|
127
|
+
yield different VAD chunk boundaries than raw).
|
|
128
|
+
|
|
129
|
+
A CHUNK RESPINE (0b4d5cfa) re-derives one coarse chunk into the LIVE spine
|
|
130
|
+
from a landed external transcript: its segments keep the live skeleton hash
|
|
131
|
+
(every picker groups spines by it) and fork identity through `identity_salt`
|
|
132
|
+
= the landed Transcript's node id, so the same landing twice collides into
|
|
133
|
+
a no-op and a different landing mints fresh segments. None (the default)
|
|
134
|
+
reproduces every pre-salt id unchanged."""
|
|
135
|
+
if identity_salt:
|
|
136
|
+
return derive_node_id("segment", rendition_id, vad_config_hash, chunk_start, chunk_end,
|
|
137
|
+
identity_salt)
|
|
92
138
|
return derive_node_id("segment", rendition_id, vad_config_hash, chunk_start, chunk_end)
|
|
93
139
|
|
|
94
140
|
|
|
@@ -220,6 +266,8 @@ class TranscriptNode:
|
|
|
220
266
|
audio_hash: str # Content hash of the consumed model-input WAV (the rendition's)
|
|
221
267
|
metadata: Dict[str, Any] = field(default_factory=dict) # Transcriber-reported metadata
|
|
222
268
|
asserted_at: Optional[float] = None # Derivation timestamp; None = now
|
|
269
|
+
actor: Optional[str] = None # Attribution actor; None = "capability:<transcriber>" (an EXTERNAL landing names the operator, 9ffce5f7 (1))
|
|
270
|
+
method: str = "transcribe" # Attribution method ("transcribe" for a capability run; "external-landing" for pasted text)
|
|
223
271
|
|
|
224
272
|
@property
|
|
225
273
|
def id(self) -> str: # Deterministic node id
|
|
@@ -236,7 +284,7 @@ class TranscriptNode:
|
|
|
236
284
|
}
|
|
237
285
|
if self.metadata:
|
|
238
286
|
props["metadata"] = dict(self.metadata)
|
|
239
|
-
props.update(attribution(f"capability:{self.transcriber}", method=
|
|
287
|
+
props.update(attribution(self.actor or f"capability:{self.transcriber}", method=self.method,
|
|
240
288
|
asserted_at=self.asserted_at))
|
|
241
289
|
return {
|
|
242
290
|
"id": self.id,
|
|
@@ -251,6 +299,18 @@ class TranscriptNode:
|
|
|
251
299
|
"""DERIVED_FROM edge: this Transcript derives from its AudioRendition."""
|
|
252
300
|
return make_edge(self.id, self.rendition, OverlayRelations.DERIVED_FROM)
|
|
253
301
|
|
|
302
|
+
def supersedes_edge(
|
|
303
|
+
self,
|
|
304
|
+
prior_transcript_id: str, # The Transcript variant this one replaces (same rendition + transcriber, an older config)
|
|
305
|
+
) -> Dict[str, Any]: # Edge wire dict
|
|
306
|
+
"""SUPERSEDES edge: this variant replaces a prior one for the same (rendition,
|
|
307
|
+
transcriber) — a chunk-targeted re-run under a new config, or a re-landed
|
|
308
|
+
external transcript (ruling 910f3692 (2)). The prior node STAYS as append-only
|
|
309
|
+
history; readers treat a variant with an incoming SUPERSEDES edge as NOT LIVE."""
|
|
310
|
+
if prior_transcript_id == self.id:
|
|
311
|
+
raise ValueError("supersedes_edge: a Transcript cannot supersede itself")
|
|
312
|
+
return make_edge(self.id, prior_transcript_id, OverlayRelations.SUPERSEDES)
|
|
313
|
+
|
|
254
314
|
|
|
255
315
|
@dataclass
|
|
256
316
|
class TranscriptSliceRef:
|
|
@@ -273,14 +333,23 @@ class TranscriptSliceRef:
|
|
|
273
333
|
class SegmentNode:
|
|
274
334
|
"""Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
|
|
275
335
|
text (the immutable-audio/mutable-text spine seam). PART_OF its AudioRendition
|
|
276
|
-
(each rendition has its own fine spine).
|
|
336
|
+
(each rendition has its own fine spine). Parallel SPINES coexist under one
|
|
337
|
+
rendition (sentence-split, DEC f1024568): the skeleton hash feeds every node
|
|
338
|
+
id AND rides the queryable `skeleton_hash` prop, so spine-scoped reads never
|
|
339
|
+
mix coexisting skeletons (pre-split nodes lack the prop = the legacy spine).
|
|
277
340
|
|
|
278
341
|
Layer-0 `text` is the ACCURACY transcriber's alignment; the designation is
|
|
279
342
|
per-segment provenance, not global config (`text_from` names the
|
|
280
343
|
authoritative Transcript; every transcriber's char range rides
|
|
281
|
-
`text_slices`, the authoritative one included).
|
|
344
|
+
`text_slices`, the authoritative one included).
|
|
345
|
+
|
|
346
|
+
A CHUNK RESPINE (0b4d5cfa) replaces one coarse chunk's segments INSIDE the
|
|
347
|
+
live spine: the new segments carry `identity_salt` (joins the id only —
|
|
348
|
+
`skeleton_hash` stays the live spine's, so no reader splits the spine),
|
|
349
|
+
the old ones are stamped `superseded_by` (the respine op id) and leave the
|
|
350
|
+
live view — readers filter `superseded_by is null`; nothing is deleted."""
|
|
282
351
|
rendition: str # Owning AudioRendition node id
|
|
283
|
-
vad_config_hash: str #
|
|
352
|
+
vad_config_hash: str # Skeleton config hash (identity input: VAD config, + split policy when a split stage ran)
|
|
284
353
|
chunk_start: float # VAD chunk start (chunk-local seconds within the rendition WAV)
|
|
285
354
|
chunk_end: float # VAD chunk end (chunk-local seconds)
|
|
286
355
|
index: int # Source-wide fine-spine index (0-based)
|
|
@@ -290,13 +359,18 @@ class SegmentNode:
|
|
|
290
359
|
audio_hash: str = "" # Content hash of the owning rendition's model-input WAV
|
|
291
360
|
source: Optional[str] = None # Source node id (convenience for direct filters)
|
|
292
361
|
text_from: Optional[str] = None # Authoritative Transcript node id (None when text is empty)
|
|
362
|
+
split_policy: Optional[str] = None # Split policy+version that refined the skeleton (None = raw VAD chunks)
|
|
293
363
|
text_slices: List[TranscriptSliceRef] = field(default_factory=list) # All per-transcriber slice refs
|
|
364
|
+
identity_salt: Optional[str] = None # Respine salt (the landed Transcript id): joins the id ONLY (0b4d5cfa (3)); None = original derivation
|
|
365
|
+
superseded_by: Optional[str] = None # Respine op id that retired this segment from the live view (stamped post-hoc; None = live)
|
|
294
366
|
|
|
295
367
|
@property
|
|
296
368
|
def id(self) -> str: # Deterministic node id
|
|
297
|
-
"""Deterministic node id (audio-side identity; shared across transcribers, per-rendition
|
|
369
|
+
"""Deterministic node id (audio-side identity; shared across transcribers, per-rendition;
|
|
370
|
+
forked by `identity_salt` on a chunk respine)."""
|
|
298
371
|
return segment_node_id(self.rendition, self.vad_config_hash,
|
|
299
|
-
self.chunk_start, self.chunk_end
|
|
372
|
+
self.chunk_start, self.chunk_end,
|
|
373
|
+
identity_salt=self.identity_salt)
|
|
300
374
|
|
|
301
375
|
def to_graph_node(self) -> Dict[str, Any]: # Node wire dict
|
|
302
376
|
"""Build the Segment node wire dict (audio ref + per-transcriber text slice refs)."""
|
|
@@ -308,11 +382,18 @@ class SegmentNode:
|
|
|
308
382
|
"chunk_start": self.chunk_start,
|
|
309
383
|
"chunk_end": self.chunk_end,
|
|
310
384
|
"rendition_id": self.rendition,
|
|
385
|
+
"skeleton_hash": self.vad_config_hash,
|
|
311
386
|
}
|
|
387
|
+
if self.split_policy:
|
|
388
|
+
props["split_policy"] = self.split_policy
|
|
312
389
|
if self.source:
|
|
313
390
|
props["source_id"] = self.source
|
|
314
391
|
if self.text_from:
|
|
315
392
|
props["text_from"] = self.text_from
|
|
393
|
+
if self.identity_salt:
|
|
394
|
+
props["identity_salt"] = self.identity_salt
|
|
395
|
+
if self.superseded_by:
|
|
396
|
+
props["superseded_by"] = self.superseded_by
|
|
316
397
|
sources = [SourceRef(locator=GraphNodeRef(node_id=self.rendition),
|
|
317
398
|
content_hash=self.audio_hash,
|
|
318
399
|
slice=TimeSlice(start=self.chunk_start, end=self.chunk_end)).to_dict()]
|
|
@@ -325,6 +406,53 @@ class SegmentNode:
|
|
|
325
406
|
}
|
|
326
407
|
|
|
327
408
|
|
|
409
|
+
RESPINE_OP_VERB = "chunk-respine" # The journaled spine-fact op that lands a chunk respine (0b4d5cfa (4))
|
|
410
|
+
SOURCE_RESPINED_CHUNKS_PROP = "respined_chunks" # Source prop: the list of RespinedChunkEntry dicts, append-only
|
|
411
|
+
SEGMENT_SUPERSEDED_BY_PROP = "superseded_by" # Segment prop stamped on the chunk's old segments (live readers filter it null)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@dataclass
|
|
415
|
+
class RespinedChunkEntry:
|
|
416
|
+
"""One chunk respine as the Source records it (0b4d5cfa (4)): the entry the
|
|
417
|
+
`chunk-respine` op appends to the Source's `respined_chunks` list — WHICH
|
|
418
|
+
coarse chunk, WHICH landed Transcript re-derived it (= the new segments'
|
|
419
|
+
identity salt), under WHICH decomp run, when, and what the op did to the
|
|
420
|
+
dependents (stranded ids on explicit say-so; carried = the corrections the
|
|
421
|
+
chunk-scoped transfer re-homed by time: accepted event inserts, speaker
|
|
422
|
+
assignments). The op id is what `superseded_by` on the old segments names."""
|
|
423
|
+
audio_segment: str # AudioSegment node id (the coarse chunk)
|
|
424
|
+
transcript: str # The landed Transcript node id (= identity_salt of the new segments)
|
|
425
|
+
run: str # The decomp run id the re-derivation ran under
|
|
426
|
+
op_id: str # The chunk-respine op id (stamped as superseded_by)
|
|
427
|
+
ts: float # Wall-clock seconds of the landing
|
|
428
|
+
old_segments: List[str] = field(default_factory=list) # The superseded segment ids, index order
|
|
429
|
+
new_segments: List[str] = field(default_factory=list) # The replacement segment ids, index order
|
|
430
|
+
stranded: List[str] = field(default_factory=list) # Dependent Correction ids that stopped projecting (on say-so)
|
|
431
|
+
carried: List[str] = field(default_factory=list) # Fresh Correction ids the transfer minted onto the new segments
|
|
432
|
+
straddles: List[str] = field(default_factory=list) # New segment ids left unassigned because they straddle two donor speakers (4a7ec4f8 (3))
|
|
433
|
+
|
|
434
|
+
def to_dict(self) -> Dict[str, Any]: # The wire entry
|
|
435
|
+
"""The entry as it rides the Source prop (plain JSON, list order preserved)."""
|
|
436
|
+
return {"audio_segment": self.audio_segment, "transcript": self.transcript,
|
|
437
|
+
"run": self.run, "op_id": self.op_id, "ts": self.ts,
|
|
438
|
+
"old_segments": list(self.old_segments), "new_segments": list(self.new_segments),
|
|
439
|
+
"stranded": list(self.stranded), "carried": list(self.carried),
|
|
440
|
+
"straddles": list(self.straddles)}
|
|
441
|
+
|
|
442
|
+
@classmethod
|
|
443
|
+
def from_dict(cls, d: Dict[str, Any]) -> "RespinedChunkEntry": # Parsed entry
|
|
444
|
+
"""Read an entry back off the Source prop (missing lists read empty)."""
|
|
445
|
+
return cls(audio_segment=str(d.get("audio_segment") or ""),
|
|
446
|
+
transcript=str(d.get("transcript") or ""),
|
|
447
|
+
run=str(d.get("run") or ""), op_id=str(d.get("op_id") or ""),
|
|
448
|
+
ts=float(d.get("ts") or 0.0),
|
|
449
|
+
old_segments=[str(s) for s in (d.get("old_segments") or [])],
|
|
450
|
+
new_segments=[str(s) for s in (d.get("new_segments") or [])],
|
|
451
|
+
stranded=[str(s) for s in (d.get("stranded") or [])],
|
|
452
|
+
carried=[str(s) for s in (d.get("carried") or [])],
|
|
453
|
+
straddles=[str(s) for s in (d.get("straddles") or [])])
|
|
454
|
+
|
|
455
|
+
|
|
328
456
|
def normalize_collection_title(
|
|
329
457
|
title: str, # Raw title (folder name, user input, ...)
|
|
330
458
|
) -> str: # Normalized identity form
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cjm-transcript-graph-schema
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.11
|
|
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
|
|
@@ -32,6 +32,7 @@ Audio-transcript layer schema for context graphs: Source, AudioSegment, Transcri
|
|
|
32
32
|
- `AudioRenditionNode` _class_ — A model-input rendition OF an AudioSegment — the materialized 16k-mono WAV
|
|
33
33
|
- `AudioSegmentNode` _class_ — Coarse ~5-min spine member: a BOUNDARY range of the Source (an audio fact),
|
|
34
34
|
- `CollectionNode` _class_ — An intrinsic collection (book, podcast series, lecture course) — the
|
|
35
|
+
- `RespinedChunkEntry` _class_ — One chunk respine as the Source records it (0b4d5cfa (4)): the entry the
|
|
35
36
|
- `SegmentNode` _class_ — Fine spine member: one VAD chunk — IMMUTABLE audio range + CORRECTABLE
|
|
36
37
|
- `SourceNode` _class_ — The provenance root: one ingested media file.
|
|
37
38
|
- `TranscriptGraphLabels` _class_ — Node labels of the audio-transcript layer schema.
|
{cjm_transcript_graph_schema-0.0.8 → cjm_transcript_graph_schema-0.0.11}/tests/test_schema.py
RENAMED
|
@@ -174,3 +174,118 @@ def test_collection_identity_wire_shape_and_edges():
|
|
|
174
174
|
# late-added member attaches to the SAME node without touching the chain
|
|
175
175
|
late = collection_edges(cid, [source_node_id("sha256:ch2.5")])
|
|
176
176
|
assert late[0]["relation_type"] == "PART_OF" and late[0]["target_id"] == cid
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_segment_skeleton_hash_prop_and_split_policy():
|
|
180
|
+
# Every new commit records its skeleton identity input as a QUERYABLE prop
|
|
181
|
+
# (spine-scoped reads filter on it; legacy nodes lack it entirely).
|
|
182
|
+
seg = SegmentNode(rendition=R_RAW, vad_config_hash="sha256:skel", chunk_start=1.0,
|
|
183
|
+
chunk_end=2.0, index=0, start_time=1.0, end_time=2.0, text="hi")
|
|
184
|
+
props = seg.to_graph_node()["properties"]
|
|
185
|
+
assert props["skeleton_hash"] == "sha256:skel"
|
|
186
|
+
assert "split_policy" not in props, "no split stage -> no split_policy prop"
|
|
187
|
+
|
|
188
|
+
split = SegmentNode(rendition=R_RAW, vad_config_hash="sha256:skel2", chunk_start=1.0,
|
|
189
|
+
chunk_end=1.6, index=0, start_time=1.0, end_time=1.6, text="hi",
|
|
190
|
+
split_policy="sentence-split/v1")
|
|
191
|
+
sprops = split.to_graph_node()["properties"]
|
|
192
|
+
assert sprops["skeleton_hash"] == "sha256:skel2"
|
|
193
|
+
assert sprops["split_policy"] == "sentence-split/v1"
|
|
194
|
+
|
|
195
|
+
# split_policy is METADATA, never an identity input — the id stays a pure
|
|
196
|
+
# function of (rendition, skeleton hash, chunk range).
|
|
197
|
+
a = SegmentNode(rendition=R_RAW, vad_config_hash="h", chunk_start=1.0, chunk_end=2.0,
|
|
198
|
+
index=0, start_time=1.0, end_time=2.0)
|
|
199
|
+
b = SegmentNode(rendition=R_RAW, vad_config_hash="h", chunk_start=1.0, chunk_end=2.0,
|
|
200
|
+
index=0, start_time=1.0, end_time=2.0, split_policy="sentence-split/v1")
|
|
201
|
+
assert a.id == b.id
|
|
202
|
+
# ...while the skeleton hash itself DOES fork identity (parallel spines
|
|
203
|
+
# coexist because no node id is shared across skeletons).
|
|
204
|
+
c = SegmentNode(rendition=R_RAW, vad_config_hash="h2", chunk_start=1.0, chunk_end=2.0,
|
|
205
|
+
index=0, start_time=1.0, end_time=2.0)
|
|
206
|
+
assert c.id != a.id
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_transcript_supersedes_edge_external_identity_and_operator_attribution():
|
|
210
|
+
"""cf0b91d6 / ruling 910f3692: a re-run's new variant SUPERSEDES the prior variant for
|
|
211
|
+
the same (rendition, transcriber) with a deterministic edge id and refuses a self-edge;
|
|
212
|
+
an EXTERNAL landing files under `<model id>/manual` with a config hash derived from
|
|
213
|
+
(model id, prompt hash) — a different prompt is a different variant — and carries the
|
|
214
|
+
OPERATOR as actor (default attribution stays capability:<transcriber>/transcribe)."""
|
|
215
|
+
import pytest
|
|
216
|
+
from cjm_transcript_graph_schema.schema import (EXTERNAL_TRANSCRIBER_MARKER, external_config_hash,
|
|
217
|
+
external_transcriber_name)
|
|
218
|
+
|
|
219
|
+
old = TranscriptNode(rendition=R_RAW, transcriber="voxtral", config_hash="cfg-old",
|
|
220
|
+
text="some some some", audio_hash="sha256:wav0")
|
|
221
|
+
new = TranscriptNode(rendition=R_RAW, transcriber="voxtral", config_hash="cfg-new",
|
|
222
|
+
text="the good prefix", audio_hash="sha256:wav0")
|
|
223
|
+
assert old.id != new.id
|
|
224
|
+
e = new.supersedes_edge(old.id)
|
|
225
|
+
assert (e["source_id"], e["target_id"], e["relation_type"]) == (new.id, old.id, "SUPERSEDES")
|
|
226
|
+
assert e["id"] == new.supersedes_edge(old.id)["id"], "deterministic edge id"
|
|
227
|
+
with pytest.raises(ValueError):
|
|
228
|
+
new.supersedes_edge(new.id)
|
|
229
|
+
# External landing identity.
|
|
230
|
+
name = external_transcriber_name("gemini-2.5-pro")
|
|
231
|
+
assert name == "gemini-2.5-pro" + EXTERNAL_TRANSCRIBER_MARKER == "gemini-2.5-pro/manual"
|
|
232
|
+
with pytest.raises(ValueError):
|
|
233
|
+
external_transcriber_name(" ")
|
|
234
|
+
h1 = external_config_hash("gemini-2.5-pro", "sha256:prompt-a")
|
|
235
|
+
h2 = external_config_hash("gemini-2.5-pro", "sha256:prompt-b")
|
|
236
|
+
assert h1.startswith("sha256:") and h1 != h2
|
|
237
|
+
assert h1 == external_config_hash("gemini-2.5-pro", "sha256:prompt-a"), "deterministic"
|
|
238
|
+
ext = TranscriptNode(rendition=R_RAW, transcriber=name, config_hash=h1, text="NCCL, not nickel",
|
|
239
|
+
audio_hash="sha256:wav0", actor="human:operator", method="external-landing")
|
|
240
|
+
en = ext.to_graph_node()
|
|
241
|
+
assert en["id"] == transcript_node_id(R_RAW, name, h1)
|
|
242
|
+
assert en["properties"]["actor"] == "human:operator" and en["properties"]["method"] == "external-landing"
|
|
243
|
+
# The default attribution is unchanged for capability runs.
|
|
244
|
+
assert new.to_graph_node()["properties"]["actor"] == "capability:voxtral"
|
|
245
|
+
assert new.to_graph_node()["properties"]["method"] == "transcribe"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_segment_identity_salt_and_superseded_by_respine_props():
|
|
249
|
+
"""0b4d5cfa (3)/(4), schema 0.0.11: a chunk respine forks Segment identity through
|
|
250
|
+
`identity_salt` ONLY — the skeleton_hash prop stays the live spine's, so no reader
|
|
251
|
+
splits the spine; the salt is the landed Transcript id, so the same landing twice
|
|
252
|
+
collides into the same id (idempotent) and a different landing mints fresh ids.
|
|
253
|
+
`superseded_by` rides as a prop only when set (live segments carry none)."""
|
|
254
|
+
from cjm_transcript_graph_schema.schema import (RESPINE_OP_VERB, SEGMENT_SUPERSEDED_BY_PROP,
|
|
255
|
+
SOURCE_RESPINED_CHUNKS_PROP, RespinedChunkEntry)
|
|
256
|
+
|
|
257
|
+
live = SegmentNode(rendition=R_RAW, vad_config_hash="sha256:skel", chunk_start=1.0,
|
|
258
|
+
chunk_end=2.0, index=7, start_time=301.0, end_time=302.0, text="hi")
|
|
259
|
+
t_a = transcript_node_id(R_RAW, "gemini-3.8-flash/manual", "sha256:cfg-a")
|
|
260
|
+
t_b = transcript_node_id(R_RAW, "gemini-3.8-flash/manual", "sha256:cfg-b")
|
|
261
|
+
salted = SegmentNode(rendition=R_RAW, vad_config_hash="sha256:skel", chunk_start=1.0,
|
|
262
|
+
chunk_end=2.0, index=7, start_time=301.0, end_time=302.0, text="hi",
|
|
263
|
+
identity_salt=t_a)
|
|
264
|
+
again = SegmentNode(rendition=R_RAW, vad_config_hash="sha256:skel", chunk_start=1.0,
|
|
265
|
+
chunk_end=2.0, index=7, start_time=301.0, end_time=302.0, text="hi",
|
|
266
|
+
identity_salt=t_a)
|
|
267
|
+
other = SegmentNode(rendition=R_RAW, vad_config_hash="sha256:skel", chunk_start=1.0,
|
|
268
|
+
chunk_end=2.0, index=7, start_time=301.0, end_time=302.0, text="hi",
|
|
269
|
+
identity_salt=t_b)
|
|
270
|
+
assert live.id == segment_node_id(R_RAW, "sha256:skel", 1.0, 2.0), "pre-salt ids unchanged"
|
|
271
|
+
assert live.id == segment_node_id(R_RAW, "sha256:skel", 1.0, 2.0, identity_salt=None)
|
|
272
|
+
assert salted.id != live.id and salted.id == again.id and other.id != salted.id
|
|
273
|
+
assert salted.id == segment_node_id(R_RAW, "sha256:skel", 1.0, 2.0, identity_salt=t_a)
|
|
274
|
+
lp, sp = live.to_graph_node()["properties"], salted.to_graph_node()["properties"]
|
|
275
|
+
assert sp["skeleton_hash"] == lp["skeleton_hash"] == "sha256:skel", "the salt never touches the hash"
|
|
276
|
+
assert sp["identity_salt"] == t_a and "identity_salt" not in lp
|
|
277
|
+
assert SEGMENT_SUPERSEDED_BY_PROP not in lp and SEGMENT_SUPERSEDED_BY_PROP not in sp
|
|
278
|
+
old = SegmentNode(rendition=R_RAW, vad_config_hash="sha256:skel", chunk_start=1.0,
|
|
279
|
+
chunk_end=2.0, index=7, start_time=301.0, end_time=302.0, text="hi",
|
|
280
|
+
superseded_by="op-1")
|
|
281
|
+
assert old.id == live.id, "superseded_by is a stamp, never identity"
|
|
282
|
+
assert old.to_graph_node()["properties"][SEGMENT_SUPERSEDED_BY_PROP] == "op-1"
|
|
283
|
+
# The Source-side record round-trips (missing lists read empty).
|
|
284
|
+
assert (RESPINE_OP_VERB, SOURCE_RESPINED_CHUNKS_PROP) == ("chunk-respine", "respined_chunks")
|
|
285
|
+
entry = RespinedChunkEntry(audio_segment=A1, transcript=t_a, run="run-1", op_id="op-1",
|
|
286
|
+
ts=1.5, old_segments=[live.id], new_segments=[salted.id],
|
|
287
|
+
stranded=["c1"], carried=["c2"], straddles=[salted.id])
|
|
288
|
+
d = entry.to_dict()
|
|
289
|
+
assert RespinedChunkEntry.from_dict(d) == entry
|
|
290
|
+
assert RespinedChunkEntry.from_dict({"audio_segment": A1, "transcript": t_a,
|
|
291
|
+
"run": "r", "op_id": "o", "ts": 2}).stranded == []
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.8"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|