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.
- {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/PKG-INFO +8 -9
- {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/README.md +7 -8
- {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui/__init__.py +1 -1
- {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui/app.py +91 -5
- {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui/cli.py +10 -6
- {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui/spine.py +93 -18
- {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui.egg-info/PKG-INFO +8 -9
- {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/pyproject.toml +1 -1
- {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/tests/test_gestures.py +14 -0
- {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/LICENSE +0 -0
- {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui.egg-info/SOURCES.txt +0 -0
- {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
- {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
- {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/cjm_transcript_correction_tui.egg-info/requires.txt +0 -0
- {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
- {cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/setup.cfg +0 -0
- {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.
|
|
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.
|
|
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
|
|
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,
|
|
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
|
|
91
|
-
|
|
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.
|
|
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",
|
|
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
|
|
19
|
-
"
|
|
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
|
|
27
|
-
"
|
|
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,
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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.
|
|
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.
|
|
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"
|
{cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/tests/test_gestures.py
RENAMED
|
@@ -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") == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cjm_transcript_correction_tui-0.0.1 → cjm_transcript_correction_tui-0.0.2}/tests/test_state.py
RENAMED
|
File without changes
|