cjm-transcript-segment-align 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.
- cjm_transcript_segment_align/__init__.py +1 -0
- cjm_transcript_segment_align/_modidx.py +101 -0
- cjm_transcript_segment_align/components/__init__.py +0 -0
- cjm_transcript_segment_align/components/handlers.py +331 -0
- cjm_transcript_segment_align/components/helpers.py +85 -0
- cjm_transcript_segment_align/components/keyboard_config.py +323 -0
- cjm_transcript_segment_align/components/step_renderer.py +624 -0
- cjm_transcript_segment_align/html_ids.py +44 -0
- cjm_transcript_segment_align/routes/__init__.py +0 -0
- cjm_transcript_segment_align/routes/chrome.py +169 -0
- cjm_transcript_segment_align/routes/forced_alignment.py +396 -0
- cjm_transcript_segment_align/services/__init__.py +0 -0
- cjm_transcript_segment_align/services/forced_alignment.py +301 -0
- cjm_transcript_segment_align-0.0.1.dist-info/LICENSE +201 -0
- cjm_transcript_segment_align-0.0.1.dist-info/METADATA +777 -0
- cjm_transcript_segment_align-0.0.1.dist-info/RECORD +19 -0
- cjm_transcript_segment_align-0.0.1.dist-info/WHEEL +5 -0
- cjm_transcript_segment_align-0.0.1.dist-info/entry_points.txt +2 -0
- cjm_transcript_segment_align-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
"""Phase 2 combined step renderer: dual-column layout for Segment & Align"""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/components/step_renderer.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['DEBUG_COMBINED_RENDER', 'render_seg_mini_stats_badge', 'render_align_mini_stats_badge',
|
|
7
|
+
'render_alignment_status_text', 'render_alignment_status', 'render_footer_inner_content',
|
|
8
|
+
'render_combined_step']
|
|
9
|
+
|
|
10
|
+
# %% ../../nbs/components/step_renderer.ipynb #c3d4e5f6
|
|
11
|
+
from typing import Any, List
|
|
12
|
+
|
|
13
|
+
from fasthtml.common import Div, H2, P, Span, Input, Button
|
|
14
|
+
|
|
15
|
+
# DaisyUI components
|
|
16
|
+
from cjm_fasthtml_daisyui.components.data_display.badge import badge, badge_styles, badge_sizes
|
|
17
|
+
from cjm_fasthtml_daisyui.components.feedback.alert import alert, alert_colors
|
|
18
|
+
from cjm_fasthtml_daisyui.utilities.semantic_colors import bg_dui, text_dui, border_dui, ring_dui
|
|
19
|
+
from cjm_fasthtml_daisyui.utilities.border_radius import border_radius
|
|
20
|
+
|
|
21
|
+
# Tailwind utilities
|
|
22
|
+
from cjm_fasthtml_tailwind.utilities.spacing import p, m
|
|
23
|
+
from cjm_fasthtml_tailwind.utilities.sizing import w, h, min_h
|
|
24
|
+
from cjm_fasthtml_tailwind.utilities.typography import font_size, font_weight, uppercase, tracking
|
|
25
|
+
from cjm_fasthtml_tailwind.utilities.layout import overflow, position, inset, display_tw
|
|
26
|
+
from cjm_fasthtml_tailwind.utilities.borders import border
|
|
27
|
+
from cjm_fasthtml_tailwind.utilities.effects import opacity, ring
|
|
28
|
+
from cjm_fasthtml_tailwind.utilities.transitions_and_animation import transition, duration
|
|
29
|
+
from cjm_fasthtml_tailwind.utilities.flexbox_and_grid import (
|
|
30
|
+
flex_display, flex_direction, justify, items, gap, grow
|
|
31
|
+
)
|
|
32
|
+
from cjm_fasthtml_tailwind.core.base import combine_classes
|
|
33
|
+
|
|
34
|
+
# Interaction context
|
|
35
|
+
from cjm_fasthtml_interactions.core.context import InteractionContext
|
|
36
|
+
|
|
37
|
+
# Card stack library
|
|
38
|
+
from cjm_fasthtml_card_stack.components.controls import render_width_slider
|
|
39
|
+
from cjm_fasthtml_card_stack.components.states import render_loading_state
|
|
40
|
+
from cjm_fasthtml_card_stack.core.constants import DEFAULT_VISIBLE_COUNT, DEFAULT_CARD_WIDTH
|
|
41
|
+
|
|
42
|
+
# Local imports
|
|
43
|
+
# HTML IDs (page-specific)
|
|
44
|
+
from ..html_ids import CombinedHtmlIds
|
|
45
|
+
from cjm_transcript_segmentation.html_ids import SegmentationHtmlIds
|
|
46
|
+
from cjm_transcript_segmentation.models import TextSegment, SegmentationUrls
|
|
47
|
+
from cjm_transcript_vad_align.models import VADChunk, AlignmentUrls
|
|
48
|
+
|
|
49
|
+
# State extraction helpers (combined-level)
|
|
50
|
+
from cjm_transcript_segment_align.components.helpers import (
|
|
51
|
+
extract_seg_state, extract_alignment_state,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Decomposition composable renderers
|
|
55
|
+
from cjm_transcript_segmentation.components.step_renderer import (
|
|
56
|
+
render_seg_column_body, render_toolbar, render_seg_stats,
|
|
57
|
+
render_seg_footer_content, render_seg_mini_stats_text,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Alignment composable renderers
|
|
61
|
+
from cjm_transcript_vad_align.components.step_renderer import (
|
|
62
|
+
render_align_column_body, render_align_mini_stats_text,
|
|
63
|
+
)
|
|
64
|
+
from cjm_transcript_vad_align.components.card_stack_config import (
|
|
65
|
+
ALIGN_CS_IDS,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Shared keyboard config (combined-level)
|
|
69
|
+
from cjm_transcript_segment_align.components.keyboard_config import (
|
|
70
|
+
build_combined_kb_system,
|
|
71
|
+
render_keyboard_hints_collapsible, generate_zone_change_js,
|
|
72
|
+
SWITCH_CHROME_BTN_ID,
|
|
73
|
+
)
|
|
74
|
+
from cjm_transcript_segmentation.components.card_stack_config import (
|
|
75
|
+
SEG_CS_CONFIG, SEG_CS_IDS,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Debug flag for combined step rendering tracing (set False in production)
|
|
79
|
+
DEBUG_COMBINED_RENDER = True
|
|
80
|
+
|
|
81
|
+
# %% ../../nbs/components/step_renderer.ipynb #e5f6a7b8
|
|
82
|
+
def _render_column_header(
|
|
83
|
+
title:str, # Column title (e.g., "Text Decomposition")
|
|
84
|
+
stats_id:str, # HTML ID for the mini-stats badge area
|
|
85
|
+
header_id:str, # HTML ID for the column header container
|
|
86
|
+
initial_text:str="--", # Initial text for the mini-stats badge
|
|
87
|
+
) -> Any: # Column header component
|
|
88
|
+
"""Render a column header with title and mini-stats badge."""
|
|
89
|
+
return Div(
|
|
90
|
+
Span(
|
|
91
|
+
title,
|
|
92
|
+
cls=combine_classes(
|
|
93
|
+
font_size.sm, font_weight.bold,
|
|
94
|
+
uppercase, tracking.wide,
|
|
95
|
+
text_dui.base_content.opacity(50)
|
|
96
|
+
)
|
|
97
|
+
),
|
|
98
|
+
Span(
|
|
99
|
+
initial_text,
|
|
100
|
+
id=stats_id,
|
|
101
|
+
cls=combine_classes(badge, badge_styles.ghost, badge_sizes.sm)
|
|
102
|
+
),
|
|
103
|
+
id=header_id,
|
|
104
|
+
cls=combine_classes(
|
|
105
|
+
flex_display, justify.between, items.center,
|
|
106
|
+
p(3), bg_dui.base_200,
|
|
107
|
+
border_dui.base_300, border.b()
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# %% ../../nbs/components/step_renderer.ipynb #zw8jc4n30b
|
|
112
|
+
def render_seg_mini_stats_badge(
|
|
113
|
+
segments:List[TextSegment], # Current segments
|
|
114
|
+
oob:bool=False, # Whether to render as OOB swap
|
|
115
|
+
) -> Any: # Mini-stats badge Span
|
|
116
|
+
"""Render the segmentation mini-stats badge for the column header."""
|
|
117
|
+
return Span(
|
|
118
|
+
render_seg_mini_stats_text(segments),
|
|
119
|
+
id=CombinedHtmlIds.SEG_MINI_STATS,
|
|
120
|
+
cls=combine_classes(badge, badge_styles.ghost, badge_sizes.sm),
|
|
121
|
+
hx_swap_oob="true" if oob else None,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# %% ../../nbs/components/step_renderer.ipynb #3x5ezeou2d3
|
|
125
|
+
def render_align_mini_stats_badge(
|
|
126
|
+
chunks:List[VADChunk], # Current VAD chunks
|
|
127
|
+
oob:bool=False, # Whether to render as OOB swap
|
|
128
|
+
) -> Any: # Mini-stats badge Span
|
|
129
|
+
"""Render the alignment mini-stats badge for the column header."""
|
|
130
|
+
return Span(
|
|
131
|
+
render_align_mini_stats_text(chunks),
|
|
132
|
+
id=CombinedHtmlIds.ALIGNMENT_MINI_STATS,
|
|
133
|
+
cls=combine_classes(badge, badge_styles.ghost, badge_sizes.sm),
|
|
134
|
+
hx_swap_oob="true" if oob else None,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# %% ../../nbs/components/step_renderer.ipynb #t247rxpafe
|
|
138
|
+
def render_alignment_status_text(
|
|
139
|
+
segment_count:int, # Number of text segments
|
|
140
|
+
chunk_count:int, # Number of VAD chunks
|
|
141
|
+
) -> str: # Status message text
|
|
142
|
+
"""Generate alignment status message based on segment and VAD chunk counts."""
|
|
143
|
+
if segment_count == 0 or chunk_count == 0:
|
|
144
|
+
return "Waiting for data..."
|
|
145
|
+
|
|
146
|
+
delta = segment_count - chunk_count
|
|
147
|
+
|
|
148
|
+
if delta == 0:
|
|
149
|
+
return f"Aligned ({segment_count})"
|
|
150
|
+
elif delta > 0:
|
|
151
|
+
return f"{segment_count} segments vs {chunk_count} VAD chunks (merge {delta})"
|
|
152
|
+
else:
|
|
153
|
+
return f"{segment_count} segments vs {chunk_count} VAD chunks (split {-delta})"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
#| export
|
|
157
|
+
def render_alignment_status(
|
|
158
|
+
segment_count:int, # Number of text segments
|
|
159
|
+
chunk_count:int, # Number of VAD chunks
|
|
160
|
+
oob:bool=False, # Whether to render as OOB swap
|
|
161
|
+
) -> Any: # Alignment status badge component
|
|
162
|
+
"""Render the alignment status indicator badge."""
|
|
163
|
+
is_aligned = segment_count > 0 and chunk_count > 0 and segment_count == chunk_count
|
|
164
|
+
|
|
165
|
+
# Choose badge style based on alignment state
|
|
166
|
+
if is_aligned:
|
|
167
|
+
badge_cls = combine_classes(badge, badge_sizes.sm, text_dui.success, font_weight.bold)
|
|
168
|
+
elif segment_count == 0 or chunk_count == 0:
|
|
169
|
+
badge_cls = combine_classes(badge, badge_styles.ghost, badge_sizes.sm)
|
|
170
|
+
else:
|
|
171
|
+
badge_cls = combine_classes(badge, badge_sizes.sm, text_dui.warning, font_weight.bold)
|
|
172
|
+
|
|
173
|
+
return Span(
|
|
174
|
+
render_alignment_status_text(segment_count, chunk_count),
|
|
175
|
+
id=CombinedHtmlIds.ALIGNMENT_STATUS,
|
|
176
|
+
cls=badge_cls,
|
|
177
|
+
hx_swap_oob="true" if oob else None,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Shared footer inner content styling
|
|
182
|
+
_FOOTER_INNER_CLS = combine_classes(flex_display, justify.between, items.center, w.full, gap(4))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
#| export
|
|
186
|
+
def render_footer_inner_content(
|
|
187
|
+
column_footer:Any, # Column-specific footer content (decomp or align)
|
|
188
|
+
segment_count:int, # Number of text segments
|
|
189
|
+
chunk_count:int, # Number of VAD chunks
|
|
190
|
+
) -> Any: # Styled wrapper div with column footer and alignment status
|
|
191
|
+
"""Render the footer inner content with consistent styling.
|
|
192
|
+
|
|
193
|
+
This ensures the footer layout (justify-between) is preserved across
|
|
194
|
+
all OOB swaps. Both the column-specific footer content and the
|
|
195
|
+
alignment status indicator are wrapped in a flex container.
|
|
196
|
+
"""
|
|
197
|
+
return Div(
|
|
198
|
+
column_footer,
|
|
199
|
+
render_alignment_status(segment_count, chunk_count),
|
|
200
|
+
cls=_FOOTER_INNER_CLS
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# %% ../../nbs/components/step_renderer.ipynb #a7b8c9d0
|
|
204
|
+
def _placeholder(
|
|
205
|
+
text:str, # Placeholder message
|
|
206
|
+
) -> Any: # Styled placeholder paragraph
|
|
207
|
+
"""Render a placeholder text element for uninitialized chrome containers."""
|
|
208
|
+
return P(text, cls=combine_classes(font_size.sm, text_dui.base_content.opacity(50)))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _render_shared_chrome(
|
|
212
|
+
seg_state:dict=None, # Extracted segmentation state (None = show placeholders)
|
|
213
|
+
align_state:dict=None, # Extracted alignment state (None = no VAD data yet)
|
|
214
|
+
urls:SegmentationUrls=None, # Segmentation URL bundle (required when seg_state provided)
|
|
215
|
+
kb_manager:Any=None, # Keyboard manager (required when seg_state provided)
|
|
216
|
+
) -> tuple: # (hints, toolbar, controls, footer)
|
|
217
|
+
"""Render shared chrome containers, populated with segmentation content when initialized.
|
|
218
|
+
|
|
219
|
+
Takes extracted state dicts from `extract_seg_state()` and `extract_alignment_state()`
|
|
220
|
+
which contain deserialized TextSegment and VADChunk objects.
|
|
221
|
+
"""
|
|
222
|
+
is_init = seg_state is not None and seg_state.get("is_initialized", False)
|
|
223
|
+
|
|
224
|
+
# --- Hints ---
|
|
225
|
+
if is_init and kb_manager:
|
|
226
|
+
hints_content = render_keyboard_hints_collapsible(kb_manager)
|
|
227
|
+
else:
|
|
228
|
+
hints_content = _placeholder("Keyboard hints will appear here when a column is active.")
|
|
229
|
+
|
|
230
|
+
hints = Div(
|
|
231
|
+
hints_content,
|
|
232
|
+
id=CombinedHtmlIds.SHARED_HINTS,
|
|
233
|
+
cls=str(p(2))
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# --- Toolbar ---
|
|
237
|
+
if is_init and urls:
|
|
238
|
+
segments = seg_state.get("segments", [])
|
|
239
|
+
history = seg_state.get("history", [])
|
|
240
|
+
visible_count = seg_state.get("visible_count", DEFAULT_VISIBLE_COUNT)
|
|
241
|
+
toolbar_content = render_toolbar(
|
|
242
|
+
reset_url=urls.reset,
|
|
243
|
+
ai_split_url=urls.ai_split,
|
|
244
|
+
undo_url=urls.undo,
|
|
245
|
+
can_undo=(len(history) > 0),
|
|
246
|
+
visible_count=visible_count,
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
toolbar_content = _placeholder("Toolbar actions will appear here based on the active column.")
|
|
250
|
+
|
|
251
|
+
toolbar = Div(
|
|
252
|
+
toolbar_content,
|
|
253
|
+
id=CombinedHtmlIds.SHARED_TOOLBAR,
|
|
254
|
+
cls=str(p(2))
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# --- Controls ---
|
|
258
|
+
if is_init:
|
|
259
|
+
card_width = seg_state.get("card_width", DEFAULT_CARD_WIDTH)
|
|
260
|
+
controls_content = render_width_slider(SEG_CS_CONFIG, SEG_CS_IDS, card_width=card_width)
|
|
261
|
+
else:
|
|
262
|
+
controls_content = _placeholder("Column-specific controls will appear here.")
|
|
263
|
+
|
|
264
|
+
controls = Div(
|
|
265
|
+
controls_content,
|
|
266
|
+
id=CombinedHtmlIds.SHARED_CONTROLS,
|
|
267
|
+
cls=str(p(2))
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# --- Footer with alignment status ---
|
|
271
|
+
segment_count = len(seg_state.get("segments", [])) if seg_state else 0
|
|
272
|
+
chunk_count = len(align_state.get("vad_chunks", [])) if align_state else 0
|
|
273
|
+
|
|
274
|
+
if is_init:
|
|
275
|
+
segments = seg_state.get("segments", [])
|
|
276
|
+
focused_index = seg_state.get("focused_index", 0)
|
|
277
|
+
column_footer = render_seg_footer_content(segments, focused_index)
|
|
278
|
+
else:
|
|
279
|
+
column_footer = _placeholder("Footer with progress and timestamp details will appear here.")
|
|
280
|
+
|
|
281
|
+
footer_inner = render_footer_inner_content(column_footer, segment_count, chunk_count)
|
|
282
|
+
|
|
283
|
+
footer = Div(
|
|
284
|
+
footer_inner,
|
|
285
|
+
id=CombinedHtmlIds.SHARED_FOOTER,
|
|
286
|
+
cls=combine_classes(
|
|
287
|
+
p(1), bg_dui.base_100,
|
|
288
|
+
border_dui.base_300, border.t(),
|
|
289
|
+
flex_display, justify.center, items.center
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return hints, toolbar, controls, footer
|
|
294
|
+
|
|
295
|
+
# %% ../../nbs/components/step_renderer.ipynb #c9d0e1f2
|
|
296
|
+
# Shared column styling (reused by init handler for outerHTML swap)
|
|
297
|
+
_SEG_COLUMN_CLS = combine_classes(
|
|
298
|
+
w.full, w('[60%]').lg,
|
|
299
|
+
min_h(0),
|
|
300
|
+
flex_display, flex_direction.col,
|
|
301
|
+
bg_dui.base_100, border_dui.base_300, border(1),
|
|
302
|
+
border_radius.box,
|
|
303
|
+
overflow.hidden,
|
|
304
|
+
transition.all, duration._200,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _render_seg_column(
|
|
309
|
+
is_active:bool=True, # Whether this column is initially active
|
|
310
|
+
column_body:Any=None, # Pre-rendered column body (None = not initialized)
|
|
311
|
+
mini_stats_text:str="--", # Mini-stats badge text
|
|
312
|
+
init_url:str="", # URL for auto-trigger initialization
|
|
313
|
+
) -> Any: # Left column component
|
|
314
|
+
"""Render the left segmentation column."""
|
|
315
|
+
active_cls = combine_classes(ring(1), ring_dui.primary) if is_active else str(opacity(70))
|
|
316
|
+
|
|
317
|
+
# Content area: initialized viewport or loading + auto-trigger
|
|
318
|
+
if column_body is not None:
|
|
319
|
+
content = column_body
|
|
320
|
+
else:
|
|
321
|
+
content = Div(
|
|
322
|
+
render_loading_state(SEG_CS_IDS, message="Initializing segments..."),
|
|
323
|
+
# Auto-trigger initialization on load
|
|
324
|
+
Div(
|
|
325
|
+
hx_post=init_url,
|
|
326
|
+
hx_trigger="load",
|
|
327
|
+
hx_target=CombinedHtmlIds.as_selector(
|
|
328
|
+
CombinedHtmlIds.SEG_COLUMN_CONTENT
|
|
329
|
+
),
|
|
330
|
+
hx_swap="outerHTML"
|
|
331
|
+
) if init_url else None,
|
|
332
|
+
id=CombinedHtmlIds.SEG_COLUMN_CONTENT,
|
|
333
|
+
cls=combine_classes(grow(), overflow.hidden, flex_display, flex_direction.col, p(4))
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return Div(
|
|
337
|
+
_render_column_header(
|
|
338
|
+
title="Text Segmentation",
|
|
339
|
+
stats_id=CombinedHtmlIds.SEG_MINI_STATS,
|
|
340
|
+
header_id=CombinedHtmlIds.SEG_COLUMN_HEADER,
|
|
341
|
+
initial_text=mini_stats_text,
|
|
342
|
+
),
|
|
343
|
+
content,
|
|
344
|
+
id=CombinedHtmlIds.SEG_COLUMN,
|
|
345
|
+
cls=combine_classes(_SEG_COLUMN_CLS, active_cls)
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# %% ../../nbs/components/step_renderer.ipynb #d0e1f2a3
|
|
349
|
+
# Shared alignment column styling (reused by init handler for outerHTML swap)
|
|
350
|
+
_ALIGNMENT_COLUMN_CLS = combine_classes(
|
|
351
|
+
w.full, w('[40%]').lg,
|
|
352
|
+
min_h(0),
|
|
353
|
+
flex_display, flex_direction.col,
|
|
354
|
+
bg_dui.base_100, border_dui.base_300, border(1),
|
|
355
|
+
border_radius.box,
|
|
356
|
+
overflow.hidden,
|
|
357
|
+
transition.all, duration._200,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _render_alignment_column(
|
|
362
|
+
is_active:bool=False, # Whether this column is initially active
|
|
363
|
+
column_body:Any=None, # Pre-rendered column body (None = not initialized)
|
|
364
|
+
mini_stats_text:str="--", # Mini-stats badge text
|
|
365
|
+
init_url:str="", # URL for auto-trigger initialization
|
|
366
|
+
) -> Any: # Right column component
|
|
367
|
+
"""Render the right alignment column."""
|
|
368
|
+
active_cls = combine_classes(ring(1), ring_dui.secondary) if is_active else str(opacity(70))
|
|
369
|
+
|
|
370
|
+
# Content area: initialized viewport or loading + auto-trigger
|
|
371
|
+
if column_body is not None:
|
|
372
|
+
content = column_body
|
|
373
|
+
else:
|
|
374
|
+
content = Div(
|
|
375
|
+
render_loading_state(ALIGN_CS_IDS, message="Analyzing audio..."),
|
|
376
|
+
# Auto-trigger initialization on load
|
|
377
|
+
Div(
|
|
378
|
+
hx_post=init_url,
|
|
379
|
+
hx_trigger="load",
|
|
380
|
+
hx_target=CombinedHtmlIds.as_selector(
|
|
381
|
+
CombinedHtmlIds.ALIGNMENT_COLUMN_CONTENT
|
|
382
|
+
),
|
|
383
|
+
hx_swap="outerHTML"
|
|
384
|
+
) if init_url else None,
|
|
385
|
+
id=CombinedHtmlIds.ALIGNMENT_COLUMN_CONTENT,
|
|
386
|
+
cls=combine_classes(grow(), min_h(0), overflow.hidden, flex_display, flex_direction.col, p(4))
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
return Div(
|
|
390
|
+
_render_column_header(
|
|
391
|
+
title="VAD Alignment",
|
|
392
|
+
stats_id=CombinedHtmlIds.ALIGNMENT_MINI_STATS,
|
|
393
|
+
header_id=CombinedHtmlIds.ALIGNMENT_COLUMN_HEADER,
|
|
394
|
+
initial_text=mini_stats_text,
|
|
395
|
+
),
|
|
396
|
+
content,
|
|
397
|
+
id=CombinedHtmlIds.ALIGNMENT_COLUMN,
|
|
398
|
+
cls=combine_classes(_ALIGNMENT_COLUMN_CLS, active_cls)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# %% ../../nbs/components/step_renderer.ipynb #1xws0u33lr
|
|
402
|
+
def _render_keyboard_system_container(
|
|
403
|
+
kb_system:Any=None, # Rendered keyboard system (None = empty container)
|
|
404
|
+
oob:bool=False, # Whether to render as OOB swap
|
|
405
|
+
) -> Any: # Div with id=KEYBOARD_SYSTEM containing KB elements
|
|
406
|
+
"""Render stable container for keyboard navigation system elements."""
|
|
407
|
+
if DEBUG_COMBINED_RENDER:
|
|
408
|
+
print(f"[COMBINED_RENDER] _render_keyboard_system_container called, kb_system={kb_system is not None}")
|
|
409
|
+
|
|
410
|
+
if kb_system is not None:
|
|
411
|
+
content = (kb_system.script, kb_system.hidden_inputs, kb_system.action_buttons)
|
|
412
|
+
else:
|
|
413
|
+
content = ()
|
|
414
|
+
|
|
415
|
+
return Div(
|
|
416
|
+
*content,
|
|
417
|
+
id=CombinedHtmlIds.KEYBOARD_SYSTEM,
|
|
418
|
+
hx_swap_oob="true" if oob else None,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# %% ../../nbs/components/step_renderer.ipynb #f2a3b4c5
|
|
422
|
+
def render_combined_step(
|
|
423
|
+
ctx:InteractionContext, # Interaction context with state and data
|
|
424
|
+
seg_urls:SegmentationUrls=None, # URL bundle for segmentation routes
|
|
425
|
+
align_urls:AlignmentUrls=None, # URL bundle for alignment routes
|
|
426
|
+
switch_chrome_url:str="", # URL for chrome switching route
|
|
427
|
+
) -> Any: # FastHTML component with full dual-column layout
|
|
428
|
+
"""Render Phase 2: Combined Segment & Align step with dual-column layout."""
|
|
429
|
+
if DEBUG_COMBINED_RENDER:
|
|
430
|
+
print("[COMBINED_RENDER] render_combined_step called")
|
|
431
|
+
|
|
432
|
+
seg_urls = seg_urls or SegmentationUrls()
|
|
433
|
+
|
|
434
|
+
# Extract state using combined helpers
|
|
435
|
+
seg_state = extract_seg_state(ctx)
|
|
436
|
+
align_state = extract_alignment_state(ctx)
|
|
437
|
+
|
|
438
|
+
is_seg_init = seg_state["is_initialized"]
|
|
439
|
+
is_align_init = align_state["is_initialized"]
|
|
440
|
+
|
|
441
|
+
if DEBUG_COMBINED_RENDER:
|
|
442
|
+
print(f"[COMBINED_RENDER] is_seg_init: {is_seg_init}")
|
|
443
|
+
print(f"[COMBINED_RENDER] is_align_init: {is_align_init}")
|
|
444
|
+
|
|
445
|
+
# Variables for KB system
|
|
446
|
+
kb_manager = None
|
|
447
|
+
kb_system = None
|
|
448
|
+
zone_change_js = None
|
|
449
|
+
|
|
450
|
+
if is_seg_init:
|
|
451
|
+
# Get values from extracted state
|
|
452
|
+
segments = seg_state["segments"]
|
|
453
|
+
focused_index = seg_state["focused_index"]
|
|
454
|
+
history = seg_state["history"]
|
|
455
|
+
visible_count = seg_state["visible_count"]
|
|
456
|
+
card_width = seg_state["card_width"]
|
|
457
|
+
|
|
458
|
+
# Always build combined KB system with both zones
|
|
459
|
+
# (alignment zone will have no items until alignment init completes)
|
|
460
|
+
if align_urls:
|
|
461
|
+
kb_manager, kb_system = build_combined_kb_system(seg_urls, align_urls)
|
|
462
|
+
zone_change_js = generate_zone_change_js(switch_chrome_url)
|
|
463
|
+
if DEBUG_COMBINED_RENDER:
|
|
464
|
+
print(f"[COMBINED_RENDER] built combined kb_system with both zones")
|
|
465
|
+
|
|
466
|
+
# Render column body with viewport (kb_system=None, KB is in stable container)
|
|
467
|
+
column_body = render_seg_column_body(
|
|
468
|
+
segments=segments,
|
|
469
|
+
focused_index=focused_index,
|
|
470
|
+
visible_count=visible_count,
|
|
471
|
+
card_width=card_width,
|
|
472
|
+
urls=seg_urls,
|
|
473
|
+
kb_system=None, # KB system is in stable container, not column body
|
|
474
|
+
)
|
|
475
|
+
mini_stats_text = render_seg_mini_stats_text(segments)
|
|
476
|
+
|
|
477
|
+
# Shared chrome with real segmentation content (includes alignment status)
|
|
478
|
+
hints, toolbar, controls, footer = _render_shared_chrome(
|
|
479
|
+
seg_state=seg_state,
|
|
480
|
+
align_state=align_state,
|
|
481
|
+
urls=seg_urls,
|
|
482
|
+
kb_manager=kb_manager,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Left column with real content
|
|
486
|
+
seg_col = _render_seg_column(
|
|
487
|
+
is_active=True,
|
|
488
|
+
column_body=column_body,
|
|
489
|
+
mini_stats_text=mini_stats_text,
|
|
490
|
+
)
|
|
491
|
+
else:
|
|
492
|
+
# Shared chrome with placeholders (includes alignment status)
|
|
493
|
+
hints, toolbar, controls, footer = _render_shared_chrome(
|
|
494
|
+
align_state=align_state,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# Left column with loading state + auto-trigger
|
|
498
|
+
seg_col = _render_seg_column(
|
|
499
|
+
is_active=True,
|
|
500
|
+
init_url=seg_urls.init,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# --- Alignment column ---
|
|
504
|
+
if is_align_init and align_urls:
|
|
505
|
+
# Get values from extracted state
|
|
506
|
+
chunks = align_state["vad_chunks"]
|
|
507
|
+
align_focused = align_state["focused_index"]
|
|
508
|
+
align_visible = align_state["visible_count"]
|
|
509
|
+
align_width = align_state["card_width"]
|
|
510
|
+
media_paths = align_state.get("media_paths", [])
|
|
511
|
+
audio_src_url = align_urls.audio_src if align_urls else ""
|
|
512
|
+
audio_urls = [f"{audio_src_url}?path={mp}" for mp in media_paths] if audio_src_url and media_paths else []
|
|
513
|
+
|
|
514
|
+
if DEBUG_COMBINED_RENDER:
|
|
515
|
+
print(f"[COMBINED_RENDER] media_paths: {len(media_paths)}, audio_urls: {len(audio_urls)}")
|
|
516
|
+
|
|
517
|
+
align_body = render_align_column_body(
|
|
518
|
+
chunks=chunks,
|
|
519
|
+
focused_index=align_focused,
|
|
520
|
+
visible_count=align_visible,
|
|
521
|
+
card_width=align_width,
|
|
522
|
+
urls=align_urls,
|
|
523
|
+
kb_system=None, # KB system is in stable container
|
|
524
|
+
audio_urls=audio_urls,
|
|
525
|
+
)
|
|
526
|
+
align_mini_text = render_align_mini_stats_text(chunks)
|
|
527
|
+
|
|
528
|
+
align_col = _render_alignment_column(
|
|
529
|
+
is_active=False,
|
|
530
|
+
column_body=align_body,
|
|
531
|
+
mini_stats_text=align_mini_text,
|
|
532
|
+
)
|
|
533
|
+
elif align_urls and align_urls.init:
|
|
534
|
+
# Show loading state with auto-trigger
|
|
535
|
+
align_col = _render_alignment_column(
|
|
536
|
+
is_active=False,
|
|
537
|
+
init_url=align_urls.init,
|
|
538
|
+
)
|
|
539
|
+
else:
|
|
540
|
+
# No alignment URLs — show column with default placeholder
|
|
541
|
+
align_col = _render_alignment_column(is_active=False)
|
|
542
|
+
|
|
543
|
+
# Hidden input tracking active column (seg or align)
|
|
544
|
+
active_column_input = Input(
|
|
545
|
+
type="hidden",
|
|
546
|
+
id=CombinedHtmlIds.ACTIVE_COLUMN_INPUT,
|
|
547
|
+
name="active_column",
|
|
548
|
+
value="seg",
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# Stable keyboard system container (outside columns)
|
|
552
|
+
kb_container = _render_keyboard_system_container(kb_system=kb_system)
|
|
553
|
+
|
|
554
|
+
# Hidden button for chrome swap HTMX trigger (clicked by zone change JS)
|
|
555
|
+
chrome_switch_btn = None
|
|
556
|
+
if switch_chrome_url and align_urls:
|
|
557
|
+
chrome_switch_btn = Button(
|
|
558
|
+
id=SWITCH_CHROME_BTN_ID,
|
|
559
|
+
cls=str(display_tw.hidden),
|
|
560
|
+
hx_post=switch_chrome_url,
|
|
561
|
+
hx_include=f"#{CombinedHtmlIds.ACTIVE_COLUMN_INPUT}",
|
|
562
|
+
hx_swap="none",
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
if DEBUG_COMBINED_RENDER:
|
|
566
|
+
print(f"[COMBINED_RENDER] kb_container rendered with kb_system={kb_system is not None}")
|
|
567
|
+
print(f"[COMBINED_RENDER] zone_change_js present: {zone_change_js is not None}")
|
|
568
|
+
print(f"[COMBINED_RENDER] chrome_switch_btn present: {chrome_switch_btn is not None}")
|
|
569
|
+
|
|
570
|
+
return Div(
|
|
571
|
+
# Header
|
|
572
|
+
Div(
|
|
573
|
+
H2("Segment & Align", cls=combine_classes(font_size._3xl, font_weight.bold)),
|
|
574
|
+
P(
|
|
575
|
+
"Decompose text into segments and align with audio timestamps.",
|
|
576
|
+
cls=combine_classes(text_dui.base_content.opacity(70), m.b(2))
|
|
577
|
+
),
|
|
578
|
+
),
|
|
579
|
+
|
|
580
|
+
# Shared keyboard hints
|
|
581
|
+
hints,
|
|
582
|
+
|
|
583
|
+
# Shared toolbar and controls
|
|
584
|
+
toolbar,
|
|
585
|
+
controls,
|
|
586
|
+
|
|
587
|
+
# Dual-column content area
|
|
588
|
+
Div(
|
|
589
|
+
seg_col,
|
|
590
|
+
align_col,
|
|
591
|
+
cls=combine_classes(
|
|
592
|
+
grow(),
|
|
593
|
+
min_h(0),
|
|
594
|
+
flex_display,
|
|
595
|
+
flex_direction.col,
|
|
596
|
+
flex_direction.row.lg,
|
|
597
|
+
gap(4),
|
|
598
|
+
overflow.hidden,
|
|
599
|
+
p(1),
|
|
600
|
+
)
|
|
601
|
+
),
|
|
602
|
+
|
|
603
|
+
# Shared footer
|
|
604
|
+
footer,
|
|
605
|
+
|
|
606
|
+
# Stable keyboard system container (outside columns, won't be swapped)
|
|
607
|
+
kb_container,
|
|
608
|
+
|
|
609
|
+
# Zone change JavaScript (when align_urls available)
|
|
610
|
+
zone_change_js,
|
|
611
|
+
|
|
612
|
+
# Hidden chrome switch button (for HTMX-triggered chrome swaps)
|
|
613
|
+
chrome_switch_btn,
|
|
614
|
+
|
|
615
|
+
# Hidden active column state
|
|
616
|
+
active_column_input,
|
|
617
|
+
|
|
618
|
+
id=SegmentationHtmlIds.SEG_CONTAINER,
|
|
619
|
+
cls=combine_classes(
|
|
620
|
+
w.full, h.full,
|
|
621
|
+
flex_display, flex_direction.col,
|
|
622
|
+
p(4), p.x(2), p.b(0)
|
|
623
|
+
)
|
|
624
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""HTML ID constants for Phase 2 Shell: Dual-Column Layout shared chrome"""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/html_ids.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['CombinedHtmlIds']
|
|
7
|
+
|
|
8
|
+
# %% ../nbs/html_ids.ipynb #c3d4e5f6
|
|
9
|
+
# Framework-agnostic — no external imports needed
|
|
10
|
+
|
|
11
|
+
class CombinedHtmlIds:
|
|
12
|
+
"""HTML ID constants for Phase 2 Shell: Dual-Column Layout shared chrome."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def as_selector(
|
|
16
|
+
id_str:str # The HTML ID to convert
|
|
17
|
+
) -> str: # CSS selector with # prefix
|
|
18
|
+
"""Convert an ID to a CSS selector format."""
|
|
19
|
+
return f"#{id_str}"
|
|
20
|
+
|
|
21
|
+
# Shared Chrome
|
|
22
|
+
SHARED_HINTS = "sd-shared-hints"
|
|
23
|
+
SHARED_TOOLBAR = "sd-shared-toolbar"
|
|
24
|
+
SHARED_CONTROLS = "sd-shared-controls"
|
|
25
|
+
SHARED_FOOTER = "sd-shared-footer"
|
|
26
|
+
ALIGNMENT_STATUS = "sd-alignment-status"
|
|
27
|
+
|
|
28
|
+
# Segmentation Column
|
|
29
|
+
SEG_COLUMN = "sd-seg-column"
|
|
30
|
+
SEG_COLUMN_HEADER = "sd-seg-column-header"
|
|
31
|
+
SEG_COLUMN_CONTENT = "sd-seg-column-content"
|
|
32
|
+
SEG_MINI_STATS = "sd-seg-mini-stats"
|
|
33
|
+
|
|
34
|
+
# Alignment Column
|
|
35
|
+
ALIGNMENT_COLUMN = "sd-align-column"
|
|
36
|
+
ALIGNMENT_COLUMN_HEADER = "sd-align-column-header"
|
|
37
|
+
ALIGNMENT_COLUMN_CONTENT = "sd-align-column-content"
|
|
38
|
+
ALIGNMENT_MINI_STATS = "sd-align-mini-stats"
|
|
39
|
+
|
|
40
|
+
# State Tracking
|
|
41
|
+
ACTIVE_COLUMN_INPUT = "sd-active-column"
|
|
42
|
+
|
|
43
|
+
# Keyboard System (stable container)
|
|
44
|
+
KEYBOARD_SYSTEM = "sd-keyboard-system"
|
|
File without changes
|