cjm-transcript-correction-tui 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,68 @@
1
+ import argparse
2
+ import os
3
+
4
+ from cjm_substrate.core.workspace import resolve_workspace
5
+
6
+ from .app import CorrectionApp
7
+
8
+
9
+ def build_parser() -> argparse.ArgumentParser: # Configured CLI parser
10
+ """The TUI driver's argument surface (mirrors correction-core's run/review args)."""
11
+ p = argparse.ArgumentParser(
12
+ prog="cjm-transcript-correction-tui",
13
+ description="Keyboard-first correction loop over a transcription context graph "
14
+ "(document-order segment walk, VAD-chunk auto-play, fidelity edits).")
15
+ p.add_argument("--graph-db-path", required=True,
16
+ help="The shared transcription graph db (the committed spine)")
17
+ p.add_argument("--source", default=None,
18
+ help="Source node id or title substring (required when the graph "
19
+ "holds more than one Source)")
20
+ p.add_argument("--manifests-dir", default=None,
21
+ help="Capability manifests directory (default: the workspace's "
22
+ ".cjm/manifests when one is active, else .cjm/manifests under the cwd)")
23
+ p.add_argument("--workspace", default=None,
24
+ help="Workspace root (5daadfc4; default: CJM_WORKSPACE env, else upward walk "
25
+ "from cwd). Supplies the manifests default and is exported so capability "
26
+ "workers resolve workspace-scoped paths; run/source DISCOVERY over the "
27
+ "workspace graph is the 2ce81638 follow-on")
28
+ p.add_argument("--rendition", default=None,
29
+ help="AudioRendition selector when a source has more than one "
30
+ "(\"raw\" or a preprocessing substring); default: auto-select")
31
+ p.add_argument("--actor", default="human",
32
+ help="Actor recorded on corrections + review markers")
33
+ p.add_argument("--no-autoplay", action="store_true",
34
+ help="Do not auto-play the focused segment's VAD chunk")
35
+ p.add_argument("--audio-device", default=None,
36
+ help="Output device index or name substring (default: the system "
37
+ "default sink — pipewire/pulse routing when available)")
38
+ p.add_argument("--no-resume", action="store_true",
39
+ help="Start at segment 0 instead of the source's last-focused segment")
40
+ p.add_argument("--shift-floor-ms", type=int, default=0,
41
+ help="Minimum milliseconds between held-key boundary shifts; 0 = ungoverned "
42
+ "(the async commit guard is the real governor — a 1ms floor read as "
43
+ "residual keystroke latency in the 2026-07-14 drive). "
44
+ "Measure key rates with tests_manual/keyrate_probe.py")
45
+ return p
46
+
47
+
48
+ def main() -> int: # Console-script entry point
49
+ """Parse args, run the correction loop (the app owns the event loop + teardown)."""
50
+ args = build_parser().parse_args()
51
+ # 5daadfc4 workspace: resolve before anything reads paths; export so
52
+ # capability workers (ffmpeg etc.) are workspace-scoped.
53
+ ws = resolve_workspace(explicit=args.workspace)
54
+ if ws is not None:
55
+ os.environ["CJM_WORKSPACE"] = str(ws.root)
56
+ if args.manifests_dir is None:
57
+ args.manifests_dir = (str(ws.substrate_data_dir / "manifests")
58
+ if ws is not None else ".cjm/manifests")
59
+ device = args.audio_device
60
+ if device is not None and device.isdigit():
61
+ device = int(device)
62
+ app = CorrectionApp(args.graph_db_path, source=args.source,
63
+ manifests_dir=args.manifests_dir, rendition=args.rendition,
64
+ actor=args.actor, autoplay=not args.no_autoplay,
65
+ audio_device=device, resume=not args.no_resume,
66
+ shift_floor_s=args.shift_floor_ms / 1000.0)
67
+ app.run()
68
+ return 0
@@ -0,0 +1,321 @@
1
+ from bisect import bisect_right
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from typing import Dict, List, Optional, Tuple
5
+
6
+ from cjm_context_graph_layer.grammar import OverlayRelations, SpineRelations
7
+ from cjm_context_graph_layer.ops import graph_task
8
+ from cjm_context_graph_primitives.query import NodeQuery, OrderBy, RelationPredicate
9
+ from cjm_substrate.core.manager import CapabilityManager
10
+ from cjm_substrate.core.queue import JobQueue
11
+ from cjm_transcript_correction_core.cli import load_capabilities
12
+ from cjm_transcript_correction_core.graph import (active_corrections, load_source_corrections,
13
+ load_source_segments, mark_anchor_segments,
14
+ open_marks, project_effective_spine,
15
+ resolve_source_renditions)
16
+ from cjm_transcript_correction_core.models import SpineSegment
17
+ from cjm_transcript_graph_schema.schema import TranscriptGraphLabels
18
+
19
+
20
+ @dataclass
21
+ class ChunkRef:
22
+ """Where one Segment's VAD-chunk audio lives: the model-input WAV + the chunk-local span.
23
+
24
+ The correction loop plays from the model-input rendition (what the model heard),
25
+ so the span is expressed LOCAL to that AudioSegment's WAV — Segment times are
26
+ source-coordinate on the graph; the join subtracts the owning AudioSegment's
27
+ start."""
28
+ wav_path: str # The AudioSegment's model-input WAV (16 kHz mono)
29
+ start_s: float # Chunk start, seconds, local to `wav_path`
30
+ end_s: float # Chunk end, seconds, local to `wav_path`
31
+
32
+
33
+ class SpineView:
34
+ """One Source's effective correction spine, cursor-windowed for the TUI.
35
+
36
+ The driver's single seat at the graph: bootstraps the substrate capability stack
37
+ (graph worker pointed at the shared transcription graph), resolves the rendition
38
+ chain, loads the fine Segment spine, applies the effective projection (layer-0 +
39
+ active corrections — the SAME read every downstream consumer gets), and joins
40
+ each Segment to its model-input WAV chunk. The TUI renders WINDOWS of this view
41
+ (`window(cursor, count)` — scrolling moves the CURSOR, slots re-bind around it;
42
+ there is no viewport state) and slices audio via `chunk(i)`. Open MARKS load
43
+ with the corrections (⚑ bookkeeping only — they never touch the projection).
44
+ Reads come through correction-core's operation vocabulary; writes too — the
45
+ TUI never touches the graph directly."""
46
+
47
+ def __init__(self, manager: CapabilityManager, queue: JobQueue, graph_id: str,
48
+ source_id: str, source_title: str):
49
+ self._manager = manager
50
+ self.queue = queue
51
+ self.graph_id = graph_id
52
+ self.source_id = source_id
53
+ self.source_title = source_title
54
+ self.segments: List[SpineSegment] = [] # Full-skeleton effective spine (text edits applied, prunes marked)
55
+ self.pruned_ids: set = set() # Segment ids a prune correction targets (card marks)
56
+ self._prune_corrections: List[dict] = [] # Active prune Corrections (unprune anchors)
57
+ self._open_marks: List[dict] = [] # OPEN mark Corrections (routed attention)
58
+ self.marked_ids: set = set() # Segment ids an open mark anchors (⚑ glyphs)
59
+ self.seen_mark_classes: List[str] = [] # DISTINCT classes journaled on this source (open or discharged)
60
+ self._aseg_starts: List[float] = [] # AudioSegment starts (sorted, for bisect)
61
+ self._aseg_audio: List[Optional[ChunkRef]] = [] # Parallel: (wav, aseg-start) join stubs
62
+
63
+ @classmethod
64
+ async def open(cls, graph_db_path: str, # The shared transcription graph db
65
+ *, source: Optional[str] = None, # Source node id OR a title substring
66
+ manifests_dir: str = ".cjm/manifests", # Capability manifests directory
67
+ graph_capability: str = "cjm-capability-graph-sqlite",
68
+ rendition: Optional[str] = None, # Rendition selector (None = auto)
69
+ ) -> "SpineView":
70
+ """Bootstrap the capability stack and load one Source's effective spine."""
71
+ manager = CapabilityManager(search_paths=[Path(manifests_dir)])
72
+ load_capabilities(manager, [graph_capability],
73
+ configs={graph_capability: {"db_path": str(graph_db_path)}})
74
+ queue = JobQueue(deps=manager)
75
+ await queue.start()
76
+ try:
77
+ sq = NodeQuery(label="Source", project=["title"])
78
+ res = await graph_task(queue, graph_capability, "query_nodes", query=sq.to_dict())
79
+ sources = [(r["id"], str(r.get("title") or "")) for r in (res.rows or [])]
80
+ if source:
81
+ picked = [(i, t) for i, t in sources
82
+ if i == source or source.lower() in t.lower()]
83
+ else:
84
+ picked = sources
85
+ if len(picked) != 1:
86
+ titles = "; ".join(t for _, t in sources)
87
+ raise ValueError(f"need exactly one Source (matched {len(picked)}) — "
88
+ f"pass `source=` an id or title substring; available: {titles}")
89
+ view = cls(manager, queue, graph_capability, picked[0][0], picked[0][1])
90
+ await view._load(rendition)
91
+ return view
92
+ except BaseException:
93
+ await queue.stop()
94
+ raise
95
+
96
+ async def _load(self, rendition: Optional[str]) -> None:
97
+ """Load spine + corrections + the audio join (one Source, one rendition chain)."""
98
+ segments = await load_source_segments(self.queue, self.graph_id, self.source_id,
99
+ rendition_selector=rendition)
100
+ corrections, superseded = await load_source_corrections(
101
+ self.queue, self.graph_id, self.source_id)
102
+ active = active_corrections(corrections, superseded)
103
+ # Open marks paint ⚑ in the walk; they NEVER touch the projection
104
+ # (corrections_to_edits has no arm for correction_type "mark" — DEC 2a231843).
105
+ self._open_marks = open_marks(corrections, superseded)
106
+ self._recompute_marked_ids()
107
+ # The correction surface walks the FULL VAD skeleton (the 1:1 invariant):
108
+ # prune corrections are NOT applied to this view — an "empty" chunk may hold
109
+ # speech that FA starved (the falsified D14 premise), and an empty chunk is
110
+ # exactly where a boundary-shift pulls mis-assigned text back. Pruned ids
111
+ # surface as card marks instead of disappearing from the walk.
112
+ self._prune_corrections = [
113
+ c for c in active
114
+ if c.get("correction_type") == "grouping"
115
+ and (c.get("payload") or {}).get("operation") == "prune_empty"]
116
+ self.pruned_ids = {
117
+ sid for c in self._prune_corrections
118
+ for sid in (c.get("payload") or {}).get("pruned_segment_ids") or []}
119
+ # Prunes are the ONLY corrections withheld from this view (their
120
+ # positions stay walkable, marked); boundary shifts and text edits
121
+ # APPLY, review verdicts map to no edit (58b2e0a0 residual fix).
122
+ prune_ids = {c.get("id") for c in self._prune_corrections}
123
+ projected = [c for c in active if c.get("id") not in prune_ids]
124
+ self.segments = project_effective_spine(segments, projected)
125
+ rend_ids = set(await resolve_source_renditions(
126
+ self.queue, self.graph_id, self.source_id, rendition))
127
+ aq = NodeQuery(label=TranscriptGraphLabels.AUDIO_SEGMENT,
128
+ related=RelationPredicate(SpineRelations.PART_OF, node_id=self.source_id),
129
+ order_by=OrderBy(prop="start"), project=["start", "end"])
130
+ ares = await graph_task(self.queue, self.graph_id, "query_nodes", query=aq.to_dict())
131
+ asegs = [(r["id"], float(r.get("start") or 0.0)) for r in (ares.rows or [])]
132
+ rq = NodeQuery(label=TranscriptGraphLabels.AUDIO_RENDITION,
133
+ related=RelationPredicate(OverlayRelations.DERIVED_FROM,
134
+ node_ids=[a[0] for a in asegs]),
135
+ project=["model_input_path", "audio_segment_id"])
136
+ rres = await graph_task(self.queue, self.graph_id, "query_nodes", query=rq.to_dict())
137
+ wav_by_aseg: Dict[str, str] = {
138
+ str(r.get("audio_segment_id")): str(r.get("model_input_path") or "")
139
+ for r in (rres.rows or []) if r["id"] in rend_ids}
140
+ self._aseg_starts = [start for _, start in asegs]
141
+ self._aseg_audio = [
142
+ ChunkRef(wav_by_aseg[aid], start, 0.0) if aid in wav_by_aseg else None
143
+ for aid, start in asegs]
144
+
145
+ @property
146
+ def size(self) -> int: # Total segments in the effective spine
147
+ return len(self.segments)
148
+
149
+ def window(self, cursor: int, count: int) -> List[SpineSegment]:
150
+ """The slot window around the cursor — clamped, cursor-parameterized, stateless."""
151
+ if not self.segments or count <= 0:
152
+ return []
153
+ half = count // 2
154
+ start = max(0, min(max(0, cursor - half), len(self.segments) - count))
155
+ return self.segments[start:start + count]
156
+
157
+ def aseg_index(self, index: int) -> Optional[int]:
158
+ """Which coarse AudioSegment (by position) a segment sits in — seam rendering."""
159
+ if not (0 <= index < len(self.segments)) or not self._aseg_starts:
160
+ return None
161
+ seg = self.segments[index]
162
+ if seg.start_time is None:
163
+ return None
164
+ return max(0, bisect_right(self._aseg_starts, float(seg.start_time)) - 1)
165
+
166
+ def chunk(self, index: int) -> Optional[ChunkRef]:
167
+ """The Segment's VAD-chunk audio ref (model-input WAV + chunk-local span), or None."""
168
+ if not (0 <= index < len(self.segments)) or not self._aseg_starts:
169
+ return None
170
+ seg = self.segments[index]
171
+ if seg.start_time is None or seg.end_time is None:
172
+ return None
173
+ i = max(0, bisect_right(self._aseg_starts, float(seg.start_time)) - 1)
174
+ stub = self._aseg_audio[i]
175
+ if stub is None or not stub.wav_path:
176
+ return None
177
+ return ChunkRef(stub.wav_path,
178
+ float(seg.start_time) - stub.start_s,
179
+ float(seg.end_time) - stub.start_s)
180
+
181
+ def prune_correction_for(self, segment_id: str) -> Optional[dict]:
182
+ """The active prune Correction covering a segment (the unprune anchor), or None."""
183
+ for c in self._prune_corrections:
184
+ if segment_id in ((c.get("payload") or {}).get("pruned_segment_ids") or []):
185
+ return c
186
+ return None
187
+
188
+ def unprune_local(self, prior_id: str, amended: dict) -> None:
189
+ """Local echo of a committed prune amendment (amended supersedes prior_id)."""
190
+ self._prune_corrections = [amended if c.get("id") == prior_id else c
191
+ for c in self._prune_corrections]
192
+ self.pruned_ids = {
193
+ sid for c in self._prune_corrections
194
+ for sid in (c.get("payload") or {}).get("pruned_segment_ids") or []}
195
+
196
+ def _recompute_marked_ids(self) -> None:
197
+ """Re-derive the ⚑ id set + observed class list from the OPEN marks
198
+ (load + local echoes). A class leaves the picker menu when its last
199
+ open mark on this source is discharged — junk classes clean up via
200
+ dismissal; proven classes persist by PROMOTION into the recommended
201
+ slate, never by haunting the menu from discharged marks."""
202
+ self.marked_ids = set()
203
+ for m in self._open_marks:
204
+ try:
205
+ self.marked_ids.update(mark_anchor_segments(
206
+ (m.get("payload") or {}).get("anchor") or {}))
207
+ except ValueError:
208
+ continue # malformed historical mark: skip its glyph, never break the walk
209
+ self.seen_mark_classes = sorted({
210
+ str((m.get("payload") or {}).get("mark_class"))
211
+ for m in self._open_marks
212
+ if (m.get("payload") or {}).get("mark_class")})
213
+
214
+ def marks_for(self, segment_id: str) -> List[dict]:
215
+ """The open marks anchored to a segment (oldest first) — dismissal targets."""
216
+ out = []
217
+ for m in self._open_marks:
218
+ try:
219
+ ids = mark_anchor_segments((m.get("payload") or {}).get("anchor") or {})
220
+ except ValueError:
221
+ continue
222
+ if segment_id in ids:
223
+ out.append(m)
224
+ return out
225
+
226
+ def add_mark_local(self, mark: dict) -> None:
227
+ """Local echo of a committed mark (the ⚑ paints without a reload)."""
228
+ self._open_marks.append(mark)
229
+ self._recompute_marked_ids()
230
+
231
+ def dismiss_mark_local(self, mark_id: str) -> None:
232
+ """Local echo of a mark dismissal."""
233
+ self._open_marks = [m for m in self._open_marks if m.get("id") != mark_id]
234
+ self._recompute_marked_ids()
235
+
236
+ async def close(self) -> None:
237
+ """Tear down the queue + capability stack (app exit)."""
238
+ await self.queue.stop()
239
+ try:
240
+ self._manager.unload_capability(self.graph_id)
241
+ except Exception:
242
+ pass
243
+
244
+
245
+ def plan_boundary_shift(
246
+ left_text: str, # The cursor segment's current effective text
247
+ right_text: str, # The next segment's current effective text
248
+ direction: str, # "push" (last word of left -> right) | "pull" (first word of right -> left)
249
+ ) -> Optional[Tuple[str, str, str]]: # (moved word, new left text, new right text); None = nothing to move
250
+ """Plan a ONE-WORD boundary shift (the [ / ] gesture unit).
251
+
252
+ Mirrors the layer's junction-normalizing semantics (DEC f83c6931) for the
253
+ local echo: single-space joins, vacated boundary whitespace collapses.
254
+ Repeat presses chain one-word corrections; the projection applies them in
255
+ created_at order over the evolving text, so the chain composes exactly.
256
+ """
257
+ if direction == "push":
258
+ words = left_text.split()
259
+ if not words:
260
+ return None
261
+ moved = words[-1]
262
+ base = left_text.rstrip()
263
+ new_left = base[: len(base) - len(moved)].rstrip()
264
+ rtext = right_text.lstrip()
265
+ new_right = f"{moved} {rtext}" if rtext else moved
266
+ else:
267
+ words = right_text.split()
268
+ if not words:
269
+ return None
270
+ moved = words[0]
271
+ base = right_text.lstrip()
272
+ new_right = base[len(moved):].lstrip()
273
+ ltext = left_text.rstrip()
274
+ new_left = f"{ltext} {moved}" if ltext else moved
275
+ return moved, new_left, new_right
276
+
277
+
278
+ def parse_mark_input(
279
+ raw: str, # The mark-editor submission: `class ["snippet"] [note...]`
280
+ segment_text: str, # The focused segment's current effective text (span lookup)
281
+ ) -> Optional[Tuple[str, Optional[Tuple[int, int, str]], Optional[str]]]: # (class, span, note); None = empty input
282
+ """Parse the M-editor mark grammar (pure; the DEC 2a231843 TUI gesture).
283
+
284
+ First token = the mark class (open vocabulary). An optional "double-quoted"
285
+ snippet that occurs in the segment text becomes a SPAN anchor (first
286
+ occurrence; offsets + verbatim snapshot). Everything else is the note.
287
+ A quoted snippet NOT found in the text stays part of the note — the mark
288
+ degrades to segment scope rather than recording a false span.
289
+ """
290
+ text = (raw or "").strip()
291
+ if not text:
292
+ return None
293
+ head, _, rest = text.partition(" ")
294
+ rest = rest.strip()
295
+ span = None
296
+ if rest.startswith('"') and '"' in rest[1:]:
297
+ snippet, _, tail = rest[1:].partition('"')
298
+ at = segment_text.find(snippet) if snippet else -1
299
+ if at != -1:
300
+ span = (at, at + len(snippet), snippet)
301
+ rest = tail.strip()
302
+ return head, span, (rest or None)
303
+
304
+
305
+ def resolve_mark_class_token(
306
+ raw: str, # The mark-editor submission (possibly `N ...`)
307
+ menu: List[str], # Selectable classes (recommended slate + observed)
308
+ ) -> Tuple[str, Optional[str]]: # (possibly-rewritten submission, error message or None)
309
+ """Resolve a leading digit token to its menu class (the M picker; pure).
310
+
311
+ `2 "snippet" note` becomes `<menu[1]> "snippet" note` — everything after
312
+ the digit is preserved VERBATIM (a snippet's inner spacing must survive).
313
+ Out-of-range numbers return an error instead of minting a numeric class.
314
+ """
315
+ head, _, rest = (raw or "").strip().partition(" ")
316
+ if not head.isdigit():
317
+ return raw, None
318
+ n = int(head)
319
+ if not (1 <= n <= len(menu)):
320
+ return raw, f"no class #{n} (menu is 1-{len(menu)})"
321
+ return f"{menu[n - 1]} {rest}".strip(), None
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: cjm-transcript-correction-tui
3
+ Version: 0.0.1
4
+ Summary: Transcript-correction TUI driver: keyboard-first terminal review surface (cursor-windowed segment walk, VAD-chunk audio auto-play, fidelity + boundary-shift corrections, agents-propose/humans-confirm triage) over cjm-transcript-correction-core's operation vocabulary. The first born-on-graph library of the self-hosting graph arc.
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-correction-tui
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: cjm-transcript-correction-core>=0.0.4
14
+ Requires-Dist: cjm-context-graph-layer>=0.0.11
15
+ Requires-Dist: cjm-substrate-tui-kit>=0.0.1
16
+ Requires-Dist: textual
17
+ Requires-Dist: numpy
18
+ Requires-Dist: sounddevice
19
+ Requires-Dist: soundfile
20
+ Dynamic: license-file
21
+
22
+ # cjm-transcript-correction-tui
23
+
24
+ <!-- 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) -->
25
+
26
+ The minimal presentation driver for cjm-transcript-correction-core: a document-order segment walk over the shared transcript graph with chunk audio playback, held-key boundary-shift gestures, inline text correction, and review markers — the human pass-1/pass-2 correction loop's UI. Kept a separate library from the headless core per the non-minimal-drivers-separate-libs rule; every correction write appends through to the workflow's sidecar journal.
27
+
28
+ ## Modules
29
+
30
+ - **`cjm_transcript_correction_tui.__init__`** — Keyboard-first TUI driver for the transcript-correction workflow — a presentation
31
+ - **`cjm_transcript_correction_tui.app`**
32
+ - **`cjm_transcript_correction_tui.audio`**
33
+ - **`cjm_transcript_correction_tui.cli`**
34
+ - **`cjm_transcript_correction_tui.spine`**
35
+
36
+ ## API
37
+
38
+ ### `cjm_transcript_correction_tui.app`
39
+
40
+ - `CorrectionApp` _class_ — The correction loop, v0 thinnest slice: document-order segment walk with
41
+ - `load_tui_state` _function_ — Read the per-graph TUI sidecar state (last-focused positions).
42
+ - `save_tui_state` _function_ — Merge one source's last-focused position into the sidecar state file.
43
+
44
+ ### `cjm_transcript_correction_tui.audio`
45
+
46
+ - `ChunkPlayer` _class_ — Persistent-output-stream VAD-chunk player — the focus-walk auto-play engine.
47
+ - `load_chunk` _function_ — Read one VAD chunk's samples from the model-input WAV — frame-sliced, sample-accurate.
48
+ - `stretch` _function_ — Pitch-preserving time-stretch (WSOLA, numpy-only) — the playback-speed engine.
49
+
50
+ ### `cjm_transcript_correction_tui.cli`
51
+
52
+ - `build_parser` _function_ — The TUI driver's argument surface (mirrors correction-core's run/review args).
53
+ - `main` _function_ — Parse args, run the correction loop (the app owns the event loop + teardown).
54
+
55
+ ### `cjm_transcript_correction_tui.spine`
56
+
57
+ - `ChunkRef` _class_ — Where one Segment's VAD-chunk audio lives: the model-input WAV + the chunk-local span.
58
+ - `SpineView` _class_ — One Source's effective correction spine, cursor-windowed for the TUI.
59
+ - `plan_boundary_shift` _function_ — Plan a ONE-WORD boundary shift (the [ / ] gesture unit).
60
+
61
+ ## Dependencies
62
+
63
+ **Depends on:** `cjm-context-graph-layer`, `cjm-transcript-correction-core`, `numpy`, `sounddevice`, `soundfile`, `textual`
@@ -0,0 +1,10 @@
1
+ cjm_transcript_correction_tui/__init__.py,sha256=pZkWcO1mKbzxwTLMYicmlI6EX6fJ3iPXnvwmTjznHPM,1117
2
+ cjm_transcript_correction_tui/app.py,sha256=jeOvYpFJcOjkot7FE8P3wg4yhVGX4OBafdETf_9znTg,31340
3
+ cjm_transcript_correction_tui/cli.py,sha256=GD6JiTK8BbEpr44ygkw0OgTZ3FptgzPiiTiF-rr75wI,3882
4
+ cjm_transcript_correction_tui/spine.py,sha256=OR0jBwLuWZkCJhipqJOd8mZI2_xQ3bkUCqKrqQjjKVA,16743
5
+ cjm_transcript_correction_tui-0.0.1.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
6
+ cjm_transcript_correction_tui-0.0.1.dist-info/METADATA,sha256=gBrE1DfBfz4UM8zaO7ZYshax194tS3HuR7683xjrqeQ,3488
7
+ cjm_transcript_correction_tui-0.0.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
8
+ cjm_transcript_correction_tui-0.0.1.dist-info/entry_points.txt,sha256=JA2wc74AOwVLsKg5TQn5rJZs1vl6TcHTQX17pWv_YMQ,89
9
+ cjm_transcript_correction_tui-0.0.1.dist-info/top_level.txt,sha256=oKp9N3nvV0nW3b7teWDQmMQze7u2c67Nn_KEqavzpnU,30
10
+ cjm_transcript_correction_tui-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cjm-transcript-correction-tui = cjm_transcript_correction_tui.cli:main