cjm-transcript-correction-tui 0.0.1__tar.gz → 0.0.2__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 (17) hide show
  1. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/PKG-INFO +8 -9
  2. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/README.md +7 -8
  3. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui/__init__.py +1 -1
  4. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui/app.py +91 -5
  5. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui/cli.py +10 -6
  6. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui/spine.py +93 -18
  7. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui.egg-info/PKG-INFO +8 -9
  8. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/pyproject.toml +1 -1
  9. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/tests/test_gestures.py +14 -0
  10. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/LICENSE +0 -0
  11. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui.egg-info/SOURCES.txt +0 -0
  12. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui.egg-info/dependency_links.txt +0 -0
  13. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui.egg-info/entry_points.txt +0 -0
  14. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui.egg-info/requires.txt +0 -0
  15. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui.egg-info/top_level.txt +0 -0
  16. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/setup.cfg +0 -0
  17. {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/tests/test_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-transcript-correction-tui
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
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
5
  Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
6
6
  License: Apache-2.0
@@ -29,7 +29,6 @@ The minimal presentation driver for cjm-transcript-correction-core: a document-o
29
29
 
30
30
  - **`cjm_transcript_correction_tui.__init__`** — Keyboard-first TUI driver for the transcript-correction workflow — a presentation
31
31
  - **`cjm_transcript_correction_tui.app`**
32
- - **`cjm_transcript_correction_tui.audio`**
33
32
  - **`cjm_transcript_correction_tui.cli`**
34
33
  - **`cjm_transcript_correction_tui.spine`**
35
34
 
@@ -41,12 +40,6 @@ The minimal presentation driver for cjm-transcript-correction-core: a document-o
41
40
  - `load_tui_state` _function_ — Read the per-graph TUI sidecar state (last-focused positions).
42
41
  - `save_tui_state` _function_ — Merge one source's last-focused position into the sidecar state file.
43
42
 
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
43
  ### `cjm_transcript_correction_tui.cli`
51
44
 
52
45
  - `build_parser` _function_ — The TUI driver's argument surface (mirrors correction-core's run/review args).
@@ -56,8 +49,14 @@ The minimal presentation driver for cjm-transcript-correction-core: a document-o
56
49
 
57
50
  - `ChunkRef` _class_ — Where one Segment's VAD-chunk audio lives: the model-input WAV + the chunk-local span.
58
51
  - `SpineView` _class_ — One Source's effective correction spine, cursor-windowed for the TUI.
52
+ - `list_sources` _function_ — Enumerate the graph's Source nodes (the discovery corpus, 2ce81638).
53
+ - `match_sources` _function_ — The --source selector (pure; shared by direct open and the picker's seed).
54
+ - `open_stack` _function_ — Bootstrap the graph capability stack, resolving the db path (2ce81638).
55
+ - `parse_mark_input` _function_ — Parse the M-editor mark grammar (pure; the DEC 2a231843 TUI gesture).
59
56
  - `plan_boundary_shift` _function_ — Plan a ONE-WORD boundary shift (the [ / ] gesture unit).
57
+ - `resolve_mark_class_token` _function_ — Resolve a leading digit token to its menu class (the M picker; pure).
58
+ - `source_status` _function_ — Correction-status-at-a-glance for one Source (the picker's detail row).
60
59
 
61
60
  ## Dependencies
62
61
 
63
- **Depends on:** `cjm-context-graph-layer`, `cjm-transcript-correction-core`, `numpy`, `sounddevice`, `soundfile`, `textual`
62
+ **Depends on:** `cjm-context-graph-layer`, `cjm-substrate-tui-kit`, `cjm-transcript-correction-core`, `numpy`, `sounddevice`, `soundfile`, `textual`
@@ -8,7 +8,6 @@ The minimal presentation driver for cjm-transcript-correction-core: a document-o
8
8
 
9
9
  - **`cjm_transcript_correction_tui.__init__`** — Keyboard-first TUI driver for the transcript-correction workflow — a presentation
10
10
  - **`cjm_transcript_correction_tui.app`**
11
- - **`cjm_transcript_correction_tui.audio`**
12
11
  - **`cjm_transcript_correction_tui.cli`**
13
12
  - **`cjm_transcript_correction_tui.spine`**
14
13
 
@@ -20,12 +19,6 @@ The minimal presentation driver for cjm-transcript-correction-core: a document-o
20
19
  - `load_tui_state` _function_ — Read the per-graph TUI sidecar state (last-focused positions).
21
20
  - `save_tui_state` _function_ — Merge one source's last-focused position into the sidecar state file.
22
21
 
23
- ### `cjm_transcript_correction_tui.audio`
24
-
25
- - `ChunkPlayer` _class_ — Persistent-output-stream VAD-chunk player — the focus-walk auto-play engine.
26
- - `load_chunk` _function_ — Read one VAD chunk's samples from the model-input WAV — frame-sliced, sample-accurate.
27
- - `stretch` _function_ — Pitch-preserving time-stretch (WSOLA, numpy-only) — the playback-speed engine.
28
-
29
22
  ### `cjm_transcript_correction_tui.cli`
30
23
 
31
24
  - `build_parser` _function_ — The TUI driver's argument surface (mirrors correction-core's run/review args).
@@ -35,8 +28,14 @@ The minimal presentation driver for cjm-transcript-correction-core: a document-o
35
28
 
36
29
  - `ChunkRef` _class_ — Where one Segment's VAD-chunk audio lives: the model-input WAV + the chunk-local span.
37
30
  - `SpineView` _class_ — One Source's effective correction spine, cursor-windowed for the TUI.
31
+ - `list_sources` _function_ — Enumerate the graph's Source nodes (the discovery corpus, 2ce81638).
32
+ - `match_sources` _function_ — The --source selector (pure; shared by direct open and the picker's seed).
33
+ - `open_stack` _function_ — Bootstrap the graph capability stack, resolving the db path (2ce81638).
34
+ - `parse_mark_input` _function_ — Parse the M-editor mark grammar (pure; the DEC 2a231843 TUI gesture).
38
35
  - `plan_boundary_shift` _function_ — Plan a ONE-WORD boundary shift (the [ / ] gesture unit).
36
+ - `resolve_mark_class_token` _function_ — Resolve a leading digit token to its menu class (the M picker; pure).
37
+ - `source_status` _function_ — Correction-status-at-a-glance for one Source (the picker's detail row).
39
38
 
40
39
  ## Dependencies
41
40
 
42
- **Depends on:** `cjm-context-graph-layer`, `cjm-transcript-correction-core`, `numpy`, `sounddevice`, `soundfile`, `textual`
41
+ **Depends on:** `cjm-context-graph-layer`, `cjm-substrate-tui-kit`, `cjm-transcript-correction-core`, `numpy`, `sounddevice`, `soundfile`, `textual`
@@ -5,7 +5,7 @@ this package is authored as graph nodes and projected to `.py`."""
5
5
 
6
6
  import os
7
7
 
8
- __version__ = "0.0.1"
8
+ __version__ = "0.0.2"
9
9
 
10
10
  # PipeWire routing: conda-forge PortAudio ships an ALSA that cannot see the system
11
11
  # pipewire/default PCM (hw-only enumeration -> audio lands on the wrong sink, e.g. a
@@ -16,7 +16,8 @@ from textual.app import App, ComposeResult
16
16
  from textual.binding import Binding
17
17
  from textual.widgets import Input, Static
18
18
 
19
- from .spine import parse_mark_input, plan_boundary_shift, resolve_mark_class_token, SpineView
19
+ from .spine import (list_sources, match_sources, open_stack, parse_mark_input, plan_boundary_shift,
20
+ resolve_mark_class_token, source_status, SpineView)
20
21
 
21
22
 
22
23
  class CorrectionApp(App):
@@ -70,11 +71,12 @@ class CorrectionApp(App):
70
71
  Binding("N", "prev_mark", "prev mark"),
71
72
  Binding("p", "next_prune", "next prune"),
72
73
  Binding("P", "prev_prune", "prev prune"),
74
+ Binding("enter", "open_source", "open", show=False),
73
75
  Binding("escape", "cancel", "cancel/stop", show=False, priority=True),
74
76
  Binding("q", "quit_app", "quit"),
75
77
  ]
76
78
 
77
- def __init__(self, graph_db_path: str, # The shared transcription graph db
79
+ def __init__(self, graph_db_path: Optional[str] = None, # The shared transcription graph db (None = workspace-resolved, 2ce81638)
78
80
  *, source: Optional[str] = None, # Source id or title substring
79
81
  manifests_dir: str = ".cjm/manifests", # Capability manifests directory
80
82
  rendition: Optional[str] = None, # Rendition selector (None = auto)
@@ -87,8 +89,16 @@ class CorrectionApp(App):
87
89
  self._open_kwargs = dict(source=source, manifests_dir=manifests_dir,
88
90
  rendition=rendition)
89
91
  self._graph_db_path = graph_db_path
90
- # Every correction write appends through to the db's sidecar journal (DEC ccbab9f5).
91
- self._journal_path = sidecar_journal_path(graph_db_path)
92
+ # Every correction write appends through to the db's sidecar journal (DEC
93
+ # ccbab9f5); the path derives from the EFFECTIVE db at mount (may be
94
+ # workspace-resolved, so it cannot be computed here).
95
+ self._journal_path: Optional[object] = None
96
+ self.stage = "select" # "select" (source picker) -> "correct" (the walk)
97
+ self._graph_cap = "cjm-capability-graph-sqlite"
98
+ self._manager = None # the open stack; view.close() owns teardown once a spine opens
99
+ self._queue = None
100
+ self._sources: List[Tuple[str, str]] = [] # [(source_id, title)] the picker walks
101
+ self._status: Dict[str, Dict[str, int]] = {} # source_id -> status-at-a-glance
92
102
  self.view: Optional[SpineView] = None
93
103
  self.player: Optional[ChunkPlayer] = None
94
104
  self.cursor = 0
@@ -114,8 +124,31 @@ class CorrectionApp(App):
114
124
  yield editor
115
125
 
116
126
  async def on_mount(self) -> None:
117
- self.view = await SpineView.open(self._graph_db_path, **self._open_kwargs)
127
+ self._manager, self._queue, db = await open_stack(
128
+ self._graph_db_path, manifests_dir=self._open_kwargs["manifests_dir"],
129
+ graph_capability=self._graph_cap)
130
+ self._graph_db_path = db
131
+ self._journal_path = sidecar_journal_path(db)
118
132
  self.player = ChunkPlayer(device=self.audio_device)
133
+ sources = await list_sources(self._queue, self._graph_cap)
134
+ picked = match_sources(sources, self._open_kwargs["source"])
135
+ if len(picked) == 1:
136
+ await self._open_source(*picked[0])
137
+ return
138
+ # 2ce81638 discovery: no unique --source -> browse the graph's Sources
139
+ # (a bad needle widens to ALL of them, never a dead-end error).
140
+ self._sources = picked if len(picked) > 1 else sources
141
+ for sid, _ in self._sources:
142
+ self._status[sid] = await source_status(self._queue, self._graph_cap, sid)
143
+ self.cursor = 0
144
+ self._render()
145
+
146
+ async def _open_source(self, source_id: str, title: str) -> None:
147
+ """Open one Source's spine on the already-open stack and enter the walk."""
148
+ self.view = await SpineView.open_on(self._manager, self._queue, self._graph_cap,
149
+ source_id, title,
150
+ rendition=self._open_kwargs["rendition"])
151
+ self.stage = "correct"
119
152
  sess = await start_session(self.view.queue, self.view.graph_id,
120
153
  [self.view.source_id],
121
154
  journal_path=self._journal_path)
@@ -128,6 +161,7 @@ class CorrectionApp(App):
128
161
  self.speed = 1.0
129
162
  mc = str(state.get("_mark_class") or "suspect")
130
163
  self._mark_class = mc if mc[:1].isalnum() else "suspect" # heal a junk-class sidecar
164
+ self.cursor = 0 # the picker borrowed the cursor
131
165
  if self.resume:
132
166
  saved = state.get(self.view.source_id)
133
167
  if saved and self.view.size:
@@ -202,6 +236,9 @@ class CorrectionApp(App):
202
236
  is pinned to the vertical center of the card area; neighbor cards stack
203
237
  outward from it (one blank separator row) and absorb the height variance,
204
238
  clipping at the screen edges. The pin never moves — the spine flows past it."""
239
+ if self.stage == "select":
240
+ self._render_picker()
241
+ return
205
242
  view = self.view
206
243
  if not view.size:
207
244
  self.query_one("#status", Static).update(f"{view.source_title} · empty spine")
@@ -239,6 +276,48 @@ class CorrectionApp(App):
239
276
  f" · j/k·w/s walk · ←→/a/d shift · r replay · \\[/] speed · e edit · y copy"
240
277
  f" · space/u ±reviewed · m/b/M ⚑mark · n/N⚑ p/P✂ jump · q quit")
241
278
 
279
+ def _render_picker(self) -> None:
280
+ """The 2ce81638 discovery stage: the graph's Sources with correction
281
+ status at a glance; same key vocabulary as the walk (j/k, enter opens).
282
+ Spans only — no base row styles (7aca1117)."""
283
+ width = max(20, self.size.width)
284
+ lines: List[Text] = [Text("")]
285
+ if not self._sources:
286
+ lines.append(Text(" no Source nodes on this graph", style="dim"))
287
+ for i, (sid, title) in enumerate(self._sources):
288
+ st = self._status.get(sid) or {}
289
+ focused = (i == self.cursor)
290
+ row = Text("")
291
+ row.append(" > " if focused else " ")
292
+ row.append(title or sid[:12], style="bold" if focused else "")
293
+ row.append(f" {st.get('segments', 0)} segs", style="dim")
294
+ row.append(f" · {st.get('corrections', 0)} corrections", style="dim")
295
+ marks = st.get("marks", 0)
296
+ if marks:
297
+ row.append(f" · {marks} ⚑", style="yellow")
298
+ row.truncate(width)
299
+ lines.append(row)
300
+ self.query_one("#cards", Static).update(Text("\n").join(lines))
301
+ tail = str(self._graph_db_path or "")
302
+ tail = tail if len(tail) <= 40 else "…" + tail[-39:]
303
+ self.query_one("#status", Static).update(
304
+ f"pick a source ({len(self._sources)}) · @{tail}"
305
+ f" · j/k walk · enter open · q quit")
306
+
307
+ def check_action(self, action: str, parameters) -> bool:
308
+ """Stage gate: during the picker only walk/open/quit act — the whole
309
+ correction vocabulary stays inert until a spine is open (view-None
310
+ crash guard, one gate instead of twenty)."""
311
+ if self.stage == "select":
312
+ return action in ("next", "prev", "open_source", "quit_app")
313
+ return True
314
+
315
+ async def action_open_source(self) -> None:
316
+ if self.stage != "select" or not self._sources:
317
+ return
318
+ sid, title = self._sources[self.cursor]
319
+ await self._open_source(sid, title)
320
+
242
321
  def _play_cursor(self) -> None:
243
322
  c = self.view.chunk(self.cursor)
244
323
  if c is None:
@@ -247,6 +326,11 @@ class CorrectionApp(App):
247
326
  self.player.play(load_chunk(c.wav_path, c.start_s, c.end_s, speed=self.speed))
248
327
 
249
328
  def _move(self, delta: int) -> None:
329
+ if self.stage == "select": # the picker walks the source list
330
+ if self._sources:
331
+ self.cursor = max(0, min(len(self._sources) - 1, self.cursor + delta))
332
+ self._render()
333
+ return
250
334
  new = max(0, min(self.view.size - 1, self.cursor + delta))
251
335
  if new == self.cursor:
252
336
  return
@@ -607,6 +691,8 @@ class CorrectionApp(App):
607
691
  self.player.close()
608
692
  if self.view is not None:
609
693
  await self.view.close()
694
+ elif self._queue is not None:
695
+ await self._queue.stop() # picker-stage quit: the stack is open, no view owns it yet
610
696
  self.exit()
611
697
 
612
698
 
@@ -12,19 +12,23 @@ def build_parser() -> argparse.ArgumentParser: # Configured CLI parser
12
12
  prog="cjm-transcript-correction-tui",
13
13
  description="Keyboard-first correction loop over a transcription context graph "
14
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)")
15
+ p.add_argument("--graph-db-path", default=None,
16
+ help="The shared transcription graph db (the committed spine); "
17
+ "default: the graph capability's persisted config — under an "
18
+ "active workspace the config store is workspace-scoped, so "
19
+ "the workspace names the db (2ce81638)")
17
20
  p.add_argument("--source", default=None,
18
- help="Source node id or title substring (required when the graph "
19
- "holds more than one Source)")
21
+ help="Source node id or title substring; omitted or ambiguous -> "
22
+ "the in-TUI source picker (correction status at a glance)")
20
23
  p.add_argument("--manifests-dir", default=None,
21
24
  help="Capability manifests directory (default: the workspace's "
22
25
  ".cjm/manifests when one is active, else .cjm/manifests under the cwd)")
23
26
  p.add_argument("--workspace", default=None,
24
27
  help="Workspace root (5daadfc4; default: CJM_WORKSPACE env, else upward walk "
25
28
  "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")
29
+ "workers resolve workspace-scoped paths and the config store "
30
+ "supplies the graph db default (2ce81638 discovery is built: "
31
+ "no --source -> in-TUI picker)")
28
32
  p.add_argument("--rendition", default=None,
29
33
  help="AudioRendition selector when a source has more than one "
30
34
  "(\"raw\" or a preprocessing substring); default: auto-select")
@@ -61,38 +61,44 @@ class SpineView:
61
61
  self._aseg_audio: List[Optional[ChunkRef]] = [] # Parallel: (wav, aseg-start) join stubs
62
62
 
63
63
  @classmethod
64
- async def open(cls, graph_db_path: str, # The shared transcription graph db
64
+ async def open(cls, graph_db_path: Optional[str], # The shared transcription graph db (None = workspace-resolved)
65
65
  *, source: Optional[str] = None, # Source node id OR a title substring
66
66
  manifests_dir: str = ".cjm/manifests", # Capability manifests directory
67
67
  graph_capability: str = "cjm-capability-graph-sqlite",
68
68
  rendition: Optional[str] = None, # Rendition selector (None = auto)
69
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()
70
+ """Bootstrap the capability stack and load one Source's effective spine
71
+ (the direct/scripted launch; the app's picker composes the same rungs —
72
+ open_stack / list_sources / open_on — around a selection stage)."""
73
+ manager, queue, _ = await open_stack(graph_db_path, manifests_dir=manifests_dir,
74
+ graph_capability=graph_capability)
76
75
  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
76
+ sources = await list_sources(queue, graph_capability)
77
+ picked = match_sources(sources, source)
85
78
  if len(picked) != 1:
86
79
  titles = "; ".join(t for _, t in sources)
87
80
  raise ValueError(f"need exactly one Source (matched {len(picked)}) — "
88
81
  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
82
+ return await cls.open_on(manager, queue, graph_capability,
83
+ picked[0][0], picked[0][1], rendition=rendition)
92
84
  except BaseException:
93
85
  await queue.stop()
94
86
  raise
95
87
 
88
+ @classmethod
89
+ async def open_on(cls, manager: CapabilityManager, # The open stack's manager
90
+ queue: JobQueue, # Started queue (teardown stays with the caller until the view owns it)
91
+ graph_id: str, # The graph capability name
92
+ source_id: str, # The picked Source node id
93
+ source_title: str, # Its display title
94
+ *, rendition: Optional[str] = None, # Rendition selector (None = auto)
95
+ ) -> "SpineView":
96
+ """Load one Source's effective spine on an ALREADY-open stack (the
97
+ picker's open rung — discovery browsed the stack first, 2ce81638)."""
98
+ view = cls(manager, queue, graph_id, source_id, source_title)
99
+ await view._load(rendition)
100
+ return view
101
+
96
102
  async def _load(self, rendition: Optional[str]) -> None:
97
103
  """Load spine + corrections + the audio join (one Source, one rendition chain)."""
98
104
  segments = await load_source_segments(self.queue, self.graph_id, self.source_id,
@@ -319,3 +325,72 @@ def resolve_mark_class_token(
319
325
  if not (1 <= n <= len(menu)):
320
326
  return raw, f"no class #{n} (menu is 1-{len(menu)})"
321
327
  return f"{menu[n - 1]} {rest}".strip(), None
328
+
329
+
330
+ async def open_stack(
331
+ graph_db_path: Optional[str], # Explicit graph db, or None = the workspace answers
332
+ *, manifests_dir: str = ".cjm/manifests", # Capability manifests directory
333
+ graph_capability: str = "cjm-capability-graph-sqlite",
334
+ ) -> Tuple[CapabilityManager, JobQueue, str]: # (manager, started queue, effective db path)
335
+ """Bootstrap the graph capability stack, resolving the db path (2ce81638).
336
+
337
+ With an explicit path the capability loads against it (today's hand-carried
338
+ launch). With None, the capability loads on its PERSISTED config — under
339
+ CJM_WORKSPACE the substrate config store is workspace-scoped (5daadfc4), so
340
+ the workspace itself names the graph db; the effective path reads back off
341
+ the loaded instance (CR-2 applies persisted config when the caller sends
342
+ none). No db path anywhere = loud refusal naming both outs."""
343
+ manager = CapabilityManager(search_paths=[Path(manifests_dir)])
344
+ configs = ({graph_capability: {"db_path": str(graph_db_path)}}
345
+ if graph_db_path else None)
346
+ load_capabilities(manager, [graph_capability], configs=configs)
347
+ effective = graph_db_path or (
348
+ (manager.instances[graph_capability].config or {}).get("db_path"))
349
+ if not effective:
350
+ raise ValueError(
351
+ f"no graph db path: pass --graph-db-path, or persist one on "
352
+ f"{graph_capability} in the active workspace's config store")
353
+ queue = JobQueue(deps=manager)
354
+ await queue.start()
355
+ return manager, queue, str(effective)
356
+
357
+
358
+ async def list_sources(
359
+ queue: JobQueue, # Started queue over the loaded graph capability
360
+ graph_id: str, # The graph capability name
361
+ ) -> List[Tuple[str, str]]: # [(source_id, title)] in query order
362
+ """Enumerate the graph's Source nodes (the discovery corpus, 2ce81638)."""
363
+ sq = NodeQuery(label="Source", project=["title"])
364
+ res = await graph_task(queue, graph_id, "query_nodes", query=sq.to_dict())
365
+ return [(r["id"], str(r.get("title") or "")) for r in (res.rows or [])]
366
+
367
+
368
+ def match_sources(
369
+ sources: List[Tuple[str, str]], # [(source_id, title)] as enumerated
370
+ needle: Optional[str], # Source node id OR a title substring; None = all
371
+ ) -> List[Tuple[str, str]]: # The subset the needle selects (all when None)
372
+ """The --source selector (pure; shared by direct open and the picker's seed)."""
373
+ if not needle:
374
+ return list(sources)
375
+ return [(i, t) for i, t in sources
376
+ if i == needle or needle.lower() in t.lower()]
377
+
378
+
379
+ async def source_status(
380
+ queue: JobQueue, # Started queue over the loaded graph capability
381
+ graph_id: str, # The graph capability name
382
+ source_id: str, # Source whose correction status to summarize
383
+ ) -> Dict[str, int]: # {"segments": VAD-chunk count, "corrections": active, "marks": open}
384
+ """Correction-status-at-a-glance for one Source (the picker's detail row).
385
+
386
+ Segments count the VAD skeleton (AudioSegment PART_OF source — the same
387
+ full-skeleton walk the correction surface presents); corrections count the
388
+ ACTIVE set (supersession applied), marks the OPEN ⚑ set."""
389
+ aq = NodeQuery(label=TranscriptGraphLabels.AUDIO_SEGMENT,
390
+ related=RelationPredicate(SpineRelations.PART_OF, node_id=source_id),
391
+ project=["id"])
392
+ ares = await graph_task(queue, graph_id, "query_nodes", query=aq.to_dict())
393
+ corrections, superseded = await load_source_corrections(queue, graph_id, source_id)
394
+ return {"segments": len(ares.rows or []),
395
+ "corrections": len(active_corrections(corrections, superseded)),
396
+ "marks": len(open_marks(corrections, superseded))}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-transcript-correction-tui
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
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
5
  Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
6
6
  License: Apache-2.0
@@ -29,7 +29,6 @@ The minimal presentation driver for cjm-transcript-correction-core: a document-o
29
29
 
30
30
  - **`cjm_transcript_correction_tui.__init__`** — Keyboard-first TUI driver for the transcript-correction workflow — a presentation
31
31
  - **`cjm_transcript_correction_tui.app`**
32
- - **`cjm_transcript_correction_tui.audio`**
33
32
  - **`cjm_transcript_correction_tui.cli`**
34
33
  - **`cjm_transcript_correction_tui.spine`**
35
34
 
@@ -41,12 +40,6 @@ The minimal presentation driver for cjm-transcript-correction-core: a document-o
41
40
  - `load_tui_state` _function_ — Read the per-graph TUI sidecar state (last-focused positions).
42
41
  - `save_tui_state` _function_ — Merge one source's last-focused position into the sidecar state file.
43
42
 
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
43
  ### `cjm_transcript_correction_tui.cli`
51
44
 
52
45
  - `build_parser` _function_ — The TUI driver's argument surface (mirrors correction-core's run/review args).
@@ -56,8 +49,14 @@ The minimal presentation driver for cjm-transcript-correction-core: a document-o
56
49
 
57
50
  - `ChunkRef` _class_ — Where one Segment's VAD-chunk audio lives: the model-input WAV + the chunk-local span.
58
51
  - `SpineView` _class_ — One Source's effective correction spine, cursor-windowed for the TUI.
52
+ - `list_sources` _function_ — Enumerate the graph's Source nodes (the discovery corpus, 2ce81638).
53
+ - `match_sources` _function_ — The --source selector (pure; shared by direct open and the picker's seed).
54
+ - `open_stack` _function_ — Bootstrap the graph capability stack, resolving the db path (2ce81638).
55
+ - `parse_mark_input` _function_ — Parse the M-editor mark grammar (pure; the DEC 2a231843 TUI gesture).
59
56
  - `plan_boundary_shift` _function_ — Plan a ONE-WORD boundary shift (the [ / ] gesture unit).
57
+ - `resolve_mark_class_token` _function_ — Resolve a leading digit token to its menu class (the M picker; pure).
58
+ - `source_status` _function_ — Correction-status-at-a-glance for one Source (the picker's detail row).
60
59
 
61
60
  ## Dependencies
62
61
 
63
- **Depends on:** `cjm-context-graph-layer`, `cjm-transcript-correction-core`, `numpy`, `sounddevice`, `soundfile`, `textual`
62
+ **Depends on:** `cjm-context-graph-layer`, `cjm-substrate-tui-kit`, `cjm-transcript-correction-core`, `numpy`, `sounddevice`, `soundfile`, `textual`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cjm-transcript-correction-tui"
7
- version = "0.0.1"
7
+ version = "0.0.2"
8
8
  description = "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."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -112,3 +112,17 @@ def test_resolve_mark_class_token():
112
112
  assert raw == "9" and err is not None
113
113
  raw, err = resolve_mark_class_token("0 note", menu)
114
114
  assert raw == "0 note" and err is not None
115
+
116
+
117
+ def test_match_sources_selector_arms():
118
+ """2ce81638 discovery: the --source selector is pure and shared by direct
119
+ open and the picker seed — exact-id wins, title substring is case-blind,
120
+ None selects all, a miss selects none (the app widens a miss to the full
121
+ picker instead of dead-ending)."""
122
+ from cjm_transcript_correction_tui.spine import match_sources
123
+ sources = [("id-a", "Intro꞉ Learning Games"), ("id-b", "Chapter One")]
124
+ assert match_sources(sources, None) == sources
125
+ assert match_sources(sources, "id-a") == [sources[0]]
126
+ assert match_sources(sources, "chapter") == [sources[1]]
127
+ assert match_sources(sources, "LEARNING") == [sources[0]]
128
+ assert match_sources(sources, "zzz") == []