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,169 @@
|
|
|
1
|
+
"""Shared chrome switching route handlers for the combined Phase 2 step"""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/routes/chrome.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['DEBUG_SWITCH_CHROME', 'init_chrome_router']
|
|
7
|
+
|
|
8
|
+
# %% ../../nbs/routes/chrome.ipynb #c3d4e5f6
|
|
9
|
+
from typing import Tuple, Dict, Callable
|
|
10
|
+
|
|
11
|
+
from fasthtml.common import APIRouter, Div
|
|
12
|
+
|
|
13
|
+
from cjm_fasthtml_card_stack.components.controls import render_width_slider
|
|
14
|
+
from cjm_fasthtml_card_stack.core.constants import DEFAULT_VISIBLE_COUNT, DEFAULT_CARD_WIDTH
|
|
15
|
+
|
|
16
|
+
from cjm_fasthtml_interactions.core.state_store import get_session_id
|
|
17
|
+
|
|
18
|
+
from cjm_workflow_state.state_store import SQLiteWorkflowStateStore
|
|
19
|
+
|
|
20
|
+
from ..html_ids import CombinedHtmlIds
|
|
21
|
+
from cjm_transcript_segmentation.models import TextSegment, SegmentationUrls
|
|
22
|
+
from cjm_transcript_vad_align.models import VADChunk, AlignmentUrls
|
|
23
|
+
|
|
24
|
+
# Segmentation renderers
|
|
25
|
+
from cjm_transcript_segmentation.components.step_renderer import (
|
|
26
|
+
render_toolbar as render_seg_toolbar,
|
|
27
|
+
render_seg_footer_content,
|
|
28
|
+
)
|
|
29
|
+
from cjm_transcript_segmentation.components.card_stack_config import (
|
|
30
|
+
SEG_CS_CONFIG, SEG_CS_IDS,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Alignment renderers
|
|
34
|
+
from cjm_transcript_vad_align.components.step_renderer import (
|
|
35
|
+
render_align_toolbar, render_align_footer_content,
|
|
36
|
+
)
|
|
37
|
+
from cjm_transcript_vad_align.components.card_stack_config import (
|
|
38
|
+
ALIGN_CS_CONFIG, ALIGN_CS_IDS,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Footer helper
|
|
42
|
+
from cjm_transcript_segment_align.components.step_renderer import (
|
|
43
|
+
render_footer_inner_content,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Keyboard config
|
|
47
|
+
from cjm_transcript_segment_align.components.keyboard_config import (
|
|
48
|
+
build_combined_kb_system, render_keyboard_hints_collapsible,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
DEBUG_SWITCH_CHROME = False
|
|
52
|
+
|
|
53
|
+
# %% ../../nbs/routes/chrome.ipynb #e5f6a7b8
|
|
54
|
+
async def _handle_switch_chrome(
|
|
55
|
+
state_store:SQLiteWorkflowStateStore, # State store instance
|
|
56
|
+
workflow_id:str, # Workflow identifier
|
|
57
|
+
request, # FastHTML request object
|
|
58
|
+
sess, # FastHTML session object
|
|
59
|
+
seg_urls:SegmentationUrls, # URL bundle for segmentation routes
|
|
60
|
+
align_urls:AlignmentUrls, # URL bundle for alignment routes
|
|
61
|
+
) -> tuple: # OOB swaps for shared chrome containers
|
|
62
|
+
"""Switch shared chrome content based on active column."""
|
|
63
|
+
form = await request.form()
|
|
64
|
+
active_column = form.get("active_column", "seg")
|
|
65
|
+
|
|
66
|
+
if DEBUG_SWITCH_CHROME:
|
|
67
|
+
print(f"[SWITCH_CHROME] active_column: {active_column}")
|
|
68
|
+
|
|
69
|
+
session_id = get_session_id(sess)
|
|
70
|
+
state = state_store.get_state(workflow_id, session_id)
|
|
71
|
+
step_states = state.get("step_states", {})
|
|
72
|
+
seg_state = step_states.get("segmentation", {})
|
|
73
|
+
align_state = step_states.get("alignment", {})
|
|
74
|
+
|
|
75
|
+
# Get counts for alignment status
|
|
76
|
+
segment_count = len(seg_state.get("segments", []))
|
|
77
|
+
chunk_count = len(align_state.get("vad_chunks", []))
|
|
78
|
+
|
|
79
|
+
# Build combined KB manager for hints
|
|
80
|
+
kb_manager, _ = build_combined_kb_system(seg_urls, align_urls)
|
|
81
|
+
|
|
82
|
+
if active_column == "seg":
|
|
83
|
+
# Segmentation chrome
|
|
84
|
+
segments = [TextSegment.from_dict(s) for s in seg_state.get("segments", [])]
|
|
85
|
+
history = seg_state.get("history", [])
|
|
86
|
+
focused_index = seg_state.get("focused_index", 0)
|
|
87
|
+
visible_count = seg_state.get("visible_count", DEFAULT_VISIBLE_COUNT)
|
|
88
|
+
is_auto_mode = seg_state.get("is_auto_mode", False)
|
|
89
|
+
card_width = seg_state.get("card_width", DEFAULT_CARD_WIDTH)
|
|
90
|
+
|
|
91
|
+
hints_content = render_keyboard_hints_collapsible(kb_manager, include_zone_switch=True)
|
|
92
|
+
toolbar_content = render_seg_toolbar(
|
|
93
|
+
reset_url=seg_urls.reset,
|
|
94
|
+
ai_split_url=seg_urls.ai_split,
|
|
95
|
+
undo_url=seg_urls.undo,
|
|
96
|
+
can_undo=(len(history) > 0),
|
|
97
|
+
visible_count=visible_count,
|
|
98
|
+
is_auto_mode=is_auto_mode,
|
|
99
|
+
)
|
|
100
|
+
controls_content = render_width_slider(SEG_CS_CONFIG, SEG_CS_IDS, card_width=card_width)
|
|
101
|
+
column_footer = render_seg_footer_content(segments, focused_index)
|
|
102
|
+
else:
|
|
103
|
+
# Alignment chrome
|
|
104
|
+
chunks = [VADChunk.from_dict(c) for c in align_state.get("vad_chunks", [])]
|
|
105
|
+
focused_index = align_state.get("focused_chunk_index", 0)
|
|
106
|
+
visible_count = align_state.get("visible_count", 5)
|
|
107
|
+
is_auto_mode = align_state.get("is_auto_mode", False)
|
|
108
|
+
card_width = align_state.get("card_width", 40)
|
|
109
|
+
|
|
110
|
+
hints_content = render_keyboard_hints_collapsible(kb_manager, include_zone_switch=True)
|
|
111
|
+
toolbar_content = render_align_toolbar(
|
|
112
|
+
visible_count=visible_count,
|
|
113
|
+
is_auto_mode=is_auto_mode,
|
|
114
|
+
)
|
|
115
|
+
controls_content = render_width_slider(ALIGN_CS_CONFIG, ALIGN_CS_IDS, card_width=card_width)
|
|
116
|
+
column_footer = render_align_footer_content(chunks, focused_index)
|
|
117
|
+
|
|
118
|
+
if DEBUG_SWITCH_CHROME:
|
|
119
|
+
print(f"[SWITCH_CHROME] returning OOB swaps for {active_column}")
|
|
120
|
+
|
|
121
|
+
# Return OOB swaps
|
|
122
|
+
hints_oob = Div(
|
|
123
|
+
hints_content,
|
|
124
|
+
id=CombinedHtmlIds.SHARED_HINTS,
|
|
125
|
+
hx_swap_oob="innerHTML"
|
|
126
|
+
)
|
|
127
|
+
toolbar_oob = Div(
|
|
128
|
+
toolbar_content,
|
|
129
|
+
id=CombinedHtmlIds.SHARED_TOOLBAR,
|
|
130
|
+
hx_swap_oob="innerHTML"
|
|
131
|
+
)
|
|
132
|
+
controls_oob = Div(
|
|
133
|
+
controls_content,
|
|
134
|
+
id=CombinedHtmlIds.SHARED_CONTROLS,
|
|
135
|
+
hx_swap_oob="innerHTML"
|
|
136
|
+
)
|
|
137
|
+
footer_oob = Div(
|
|
138
|
+
render_footer_inner_content(column_footer, segment_count, chunk_count),
|
|
139
|
+
id=CombinedHtmlIds.SHARED_FOOTER,
|
|
140
|
+
hx_swap_oob="innerHTML"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return (hints_oob, toolbar_oob, controls_oob, footer_oob)
|
|
144
|
+
|
|
145
|
+
# %% ../../nbs/routes/chrome.ipynb #g7b8c9d0
|
|
146
|
+
def init_chrome_router(
|
|
147
|
+
state_store: SQLiteWorkflowStateStore, # State store instance
|
|
148
|
+
workflow_id: str, # Workflow identifier
|
|
149
|
+
seg_urls: SegmentationUrls, # URL bundle for segmentation routes
|
|
150
|
+
align_urls: AlignmentUrls, # URL bundle for alignment routes
|
|
151
|
+
prefix: str, # Route prefix (e.g., "/workflow/core/chrome")
|
|
152
|
+
) -> Tuple[APIRouter, Dict[str, Callable]]: # (router, route_dict)
|
|
153
|
+
"""Initialize chrome switching routes."""
|
|
154
|
+
router = APIRouter(prefix=prefix)
|
|
155
|
+
|
|
156
|
+
@router
|
|
157
|
+
async def switch_chrome(request, sess):
|
|
158
|
+
"""Switch shared chrome content based on active column."""
|
|
159
|
+
return await _handle_switch_chrome(
|
|
160
|
+
state_store, workflow_id, request, sess,
|
|
161
|
+
seg_urls=seg_urls,
|
|
162
|
+
align_urls=align_urls,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
routes = {
|
|
166
|
+
"switch_chrome": switch_chrome,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return router, routes
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Routes for triggering forced alignment, polling progress, and toggling between NLTK and force-aligned pre-splits"""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/routes/forced_alignment.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['FA_CONTAINER_ID', 'FA_STATUS_ID', 'render_fa_trigger_button', 'render_fa_progress', 'render_fa_toggle',
|
|
7
|
+
'render_fa_controls', 'init_forced_alignment_routers']
|
|
8
|
+
|
|
9
|
+
# %% ../../nbs/routes/forced_alignment.ipynb #cell-imports
|
|
10
|
+
from typing import Any, Dict, List, Tuple, Callable, Optional
|
|
11
|
+
from dataclasses import asdict
|
|
12
|
+
|
|
13
|
+
from fasthtml.common import Div, Span, Button, Progress, APIRouter
|
|
14
|
+
|
|
15
|
+
from cjm_fasthtml_interactions.core.state_store import get_session_id
|
|
16
|
+
from cjm_fasthtml_daisyui.components.actions.button import btn, btn_colors, btn_sizes, btn_styles
|
|
17
|
+
from cjm_fasthtml_daisyui.components.feedback.loading import loading, loading_styles, loading_sizes
|
|
18
|
+
from cjm_fasthtml_daisyui.components.feedback.progress import progress as progress_cls, progress_colors
|
|
19
|
+
from cjm_fasthtml_daisyui.components.layout.join import join, join_item
|
|
20
|
+
from cjm_fasthtml_daisyui.components.data_display.badge import badge, badge_colors, badge_sizes
|
|
21
|
+
from cjm_fasthtml_tailwind.utilities.spacing import p, m
|
|
22
|
+
from cjm_fasthtml_tailwind.utilities.typography import font_size, font_weight
|
|
23
|
+
from cjm_fasthtml_tailwind.utilities.flexbox_and_grid import flex_display, items, gap, justify
|
|
24
|
+
from cjm_fasthtml_tailwind.core.base import combine_classes
|
|
25
|
+
from cjm_fasthtml_lucide_icons.factory import lucide_icon
|
|
26
|
+
|
|
27
|
+
from cjm_workflow_state.state_store import SQLiteWorkflowStateStore
|
|
28
|
+
from cjm_transcript_segmentation.models import TextSegment, SegmentationUrls
|
|
29
|
+
from cjm_transcript_segmentation.components.step_renderer import render_seg_column_body
|
|
30
|
+
from cjm_transcript_segmentation.components.card_stack_config import SEG_CS_CONFIG, SEG_CS_IDS
|
|
31
|
+
from cjm_transcript_vad_align.models import VADChunk
|
|
32
|
+
from cjm_transcript_source_select.services.source import SourceService
|
|
33
|
+
|
|
34
|
+
from ..html_ids import CombinedHtmlIds
|
|
35
|
+
from cjm_transcript_segment_align.components.step_renderer import (
|
|
36
|
+
render_alignment_status, render_seg_mini_stats_badge,
|
|
37
|
+
)
|
|
38
|
+
from ..services.forced_alignment import ForcedAlignmentService
|
|
39
|
+
|
|
40
|
+
# %% ../../nbs/routes/forced_alignment.ipynb #cell-ids
|
|
41
|
+
# Stable HTML IDs for forced alignment UI elements
|
|
42
|
+
FA_CONTAINER_ID = "sd-fa-controls"
|
|
43
|
+
FA_STATUS_ID = "sd-fa-status"
|
|
44
|
+
|
|
45
|
+
# %% ../../nbs/routes/forced_alignment.ipynb #cell-render-button
|
|
46
|
+
def render_fa_trigger_button(
|
|
47
|
+
trigger_url: str, # URL for forced alignment trigger route
|
|
48
|
+
disabled: bool = False, # Whether button is disabled
|
|
49
|
+
) -> Any: # Force Align trigger button
|
|
50
|
+
"""Render the Force Align trigger button."""
|
|
51
|
+
return Button(
|
|
52
|
+
lucide_icon("audio-waveform", size=4, cls=str(m.r(2))),
|
|
53
|
+
"Force Align",
|
|
54
|
+
cls=combine_classes(btn, btn_colors.primary, btn_sizes.sm),
|
|
55
|
+
disabled="disabled" if disabled else None,
|
|
56
|
+
hx_post=trigger_url,
|
|
57
|
+
hx_swap="none",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# %% ../../nbs/routes/forced_alignment.ipynb #cell-render-progress
|
|
61
|
+
def render_fa_progress(
|
|
62
|
+
progress_val: float, # Progress value 0.0-1.0
|
|
63
|
+
message: str, # Progress stage message
|
|
64
|
+
progress_url: str, # URL for progress polling
|
|
65
|
+
) -> Any: # Progress indicator with polling
|
|
66
|
+
"""Render forced alignment progress indicator with HTMX polling."""
|
|
67
|
+
pct = int(progress_val * 100)
|
|
68
|
+
return Div(
|
|
69
|
+
Div(
|
|
70
|
+
Span(cls=combine_classes(loading, loading_styles.spinner, loading_sizes.sm)),
|
|
71
|
+
Span(
|
|
72
|
+
f"{message} ({pct}%)",
|
|
73
|
+
cls=combine_classes(font_size.sm, m.l(2))
|
|
74
|
+
),
|
|
75
|
+
cls=combine_classes(flex_display, items.center)
|
|
76
|
+
),
|
|
77
|
+
Progress(
|
|
78
|
+
value=str(pct), max="100",
|
|
79
|
+
cls=combine_classes(progress_cls, progress_colors.primary, "w-48")
|
|
80
|
+
),
|
|
81
|
+
hx_get=progress_url,
|
|
82
|
+
hx_trigger="every 1s",
|
|
83
|
+
hx_target=f"#{FA_CONTAINER_ID}",
|
|
84
|
+
hx_swap="innerHTML",
|
|
85
|
+
cls=combine_classes(flex_display, items.center, gap(3)),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# %% ../../nbs/routes/forced_alignment.ipynb #cell-render-toggle
|
|
89
|
+
def render_fa_toggle(
|
|
90
|
+
active_presplit: str, # "nltk" or "forced_alignment"
|
|
91
|
+
toggle_url: str, # URL for toggle route
|
|
92
|
+
) -> Any: # Toggle button group
|
|
93
|
+
"""Render the NLTK / Force Aligned toggle button group."""
|
|
94
|
+
nltk_active = active_presplit == "nltk"
|
|
95
|
+
fa_active = active_presplit == "forced_alignment"
|
|
96
|
+
|
|
97
|
+
nltk_cls = combine_classes(
|
|
98
|
+
btn, btn_sizes.sm, join_item,
|
|
99
|
+
btn_colors.primary if nltk_active else btn_styles.outline
|
|
100
|
+
)
|
|
101
|
+
fa_cls = combine_classes(
|
|
102
|
+
btn, btn_sizes.sm, join_item,
|
|
103
|
+
btn_colors.primary if fa_active else btn_styles.outline
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return Div(
|
|
107
|
+
Span("Pre-splits:", cls=combine_classes(font_size.sm, m.r(2))),
|
|
108
|
+
Div(
|
|
109
|
+
Button(
|
|
110
|
+
lucide_icon("text-cursor-input", size=4, cls=str(m.r(1))),
|
|
111
|
+
"NLTK",
|
|
112
|
+
cls=nltk_cls,
|
|
113
|
+
hx_post=toggle_url,
|
|
114
|
+
hx_vals='{"mode": "nltk"}',
|
|
115
|
+
hx_swap="none",
|
|
116
|
+
disabled="disabled" if nltk_active else None,
|
|
117
|
+
),
|
|
118
|
+
Button(
|
|
119
|
+
lucide_icon("audio-waveform", size=4, cls=str(m.r(1))),
|
|
120
|
+
"Force Aligned",
|
|
121
|
+
cls=fa_cls,
|
|
122
|
+
hx_post=toggle_url,
|
|
123
|
+
hx_vals='{"mode": "forced_alignment"}',
|
|
124
|
+
hx_swap="none",
|
|
125
|
+
disabled="disabled" if fa_active else None,
|
|
126
|
+
),
|
|
127
|
+
cls=join,
|
|
128
|
+
),
|
|
129
|
+
cls=combine_classes(flex_display, items.center),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# %% ../../nbs/routes/forced_alignment.ipynb #cell-render-container
|
|
133
|
+
def render_fa_controls(
|
|
134
|
+
trigger_url: str = "", # URL for trigger route
|
|
135
|
+
toggle_url: str = "", # URL for toggle route
|
|
136
|
+
active_presplit: Optional[str] = None, # Current active mode (None = no FA done yet)
|
|
137
|
+
fa_available: bool = False, # Whether FA plugin is available
|
|
138
|
+
oob: bool = False, # Whether to render as OOB swap
|
|
139
|
+
) -> Any: # FA controls container
|
|
140
|
+
"""Render the forced alignment controls container.
|
|
141
|
+
|
|
142
|
+
Shows either:
|
|
143
|
+
- Trigger button (if FA not yet run)
|
|
144
|
+
- Toggle (if FA has been run)
|
|
145
|
+
- Nothing (if FA plugin not available)
|
|
146
|
+
"""
|
|
147
|
+
if not fa_available:
|
|
148
|
+
content = None
|
|
149
|
+
elif active_presplit is not None:
|
|
150
|
+
# FA has been run — show toggle
|
|
151
|
+
content = render_fa_toggle(active_presplit, toggle_url)
|
|
152
|
+
else:
|
|
153
|
+
# FA available but not yet run — show trigger button
|
|
154
|
+
content = render_fa_trigger_button(trigger_url)
|
|
155
|
+
|
|
156
|
+
return Div(
|
|
157
|
+
content,
|
|
158
|
+
id=FA_CONTAINER_ID,
|
|
159
|
+
cls=combine_classes(flex_display, items.center, gap(2)),
|
|
160
|
+
hx_swap_oob="true" if oob else None,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# %% ../../nbs/routes/forced_alignment.ipynb #cell-handler-trigger
|
|
164
|
+
async def _handle_fa_trigger(
|
|
165
|
+
state_store: SQLiteWorkflowStateStore,
|
|
166
|
+
workflow_id: str,
|
|
167
|
+
fa_service: ForcedAlignmentService,
|
|
168
|
+
source_service: SourceService,
|
|
169
|
+
request: Any,
|
|
170
|
+
sess: Any,
|
|
171
|
+
seg_urls: SegmentationUrls,
|
|
172
|
+
progress_url: str,
|
|
173
|
+
toggle_url: str,
|
|
174
|
+
) -> Any: # OOB updates for card stack, alignment status, FA controls, mini-stats
|
|
175
|
+
"""Trigger forced alignment and replace working segments."""
|
|
176
|
+
session_id = get_session_id(sess)
|
|
177
|
+
state = state_store.get_state(workflow_id, session_id)
|
|
178
|
+
step_states = state.get("step_states", {})
|
|
179
|
+
seg_state = step_states.get("segmentation", {})
|
|
180
|
+
align_state = step_states.get("alignment", {})
|
|
181
|
+
selection_state = step_states.get("selection", {})
|
|
182
|
+
|
|
183
|
+
# Get VAD chunks (must be initialized)
|
|
184
|
+
vad_chunk_dicts = align_state.get("vad_chunks", [])
|
|
185
|
+
if not vad_chunk_dicts:
|
|
186
|
+
return render_fa_controls(
|
|
187
|
+
trigger_url="", fa_available=True, oob=True
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
vad_chunks = [VADChunk.from_dict(c) for c in vad_chunk_dicts]
|
|
191
|
+
|
|
192
|
+
# Get source info for audio paths and text
|
|
193
|
+
selected_sources = selection_state.get("selected_sources", [])
|
|
194
|
+
source_blocks = source_service.get_source_blocks(selected_sources)
|
|
195
|
+
|
|
196
|
+
# Build audio paths from source blocks
|
|
197
|
+
audio_paths = []
|
|
198
|
+
for src in selected_sources:
|
|
199
|
+
block = source_service.get_transcription_by_id(src["record_id"], src["provider_id"])
|
|
200
|
+
audio_paths.append(block.media_path)
|
|
201
|
+
|
|
202
|
+
# Group VAD chunks by source (using audio_file_index if available)
|
|
203
|
+
if len(source_blocks) == 1:
|
|
204
|
+
vad_chunks_by_source = [vad_chunks]
|
|
205
|
+
else:
|
|
206
|
+
chunks_by_idx: Dict[int, List[VADChunk]] = {}
|
|
207
|
+
for chunk in vad_chunks:
|
|
208
|
+
idx = getattr(chunk, 'audio_file_index', 0)
|
|
209
|
+
if idx not in chunks_by_idx:
|
|
210
|
+
chunks_by_idx[idx] = []
|
|
211
|
+
chunks_by_idx[idx].append(chunk)
|
|
212
|
+
vad_chunks_by_source = [chunks_by_idx.get(i, []) for i in range(len(source_blocks))]
|
|
213
|
+
|
|
214
|
+
# Run forced alignment
|
|
215
|
+
fa_segments = await fa_service.align_and_split_combined_async(
|
|
216
|
+
source_blocks=source_blocks,
|
|
217
|
+
audio_paths=audio_paths,
|
|
218
|
+
vad_chunks_by_source=vad_chunks_by_source,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Save NLTK segments before replacing (preserve for toggle)
|
|
222
|
+
if "nltk_segments" not in seg_state:
|
|
223
|
+
seg_state["nltk_segments"] = seg_state.get("segments", [])
|
|
224
|
+
seg_state["nltk_initial_segments"] = seg_state.get("initial_segments", [])
|
|
225
|
+
|
|
226
|
+
# Store FA segments and set as active
|
|
227
|
+
fa_seg_dicts = [asdict(s) for s in fa_segments]
|
|
228
|
+
seg_state["fa_segments"] = fa_seg_dicts
|
|
229
|
+
seg_state["fa_initial_segments"] = fa_seg_dicts[:]
|
|
230
|
+
seg_state["segments"] = fa_seg_dicts
|
|
231
|
+
seg_state["initial_segments"] = fa_seg_dicts[:]
|
|
232
|
+
seg_state["active_presplit"] = "forced_alignment"
|
|
233
|
+
seg_state["focused_index"] = 0
|
|
234
|
+
seg_state["history"] = []
|
|
235
|
+
|
|
236
|
+
step_states["segmentation"] = seg_state
|
|
237
|
+
state["step_states"] = step_states
|
|
238
|
+
state_store.update_state(workflow_id, session_id, state)
|
|
239
|
+
|
|
240
|
+
# Re-render card stack and add OOB outerHTML for column body swap
|
|
241
|
+
column_body = render_seg_column_body(
|
|
242
|
+
segments=fa_segments,
|
|
243
|
+
focused_index=0,
|
|
244
|
+
visible_count=seg_state.get("visible_count", 3),
|
|
245
|
+
card_width=seg_state.get("card_width", 80),
|
|
246
|
+
urls=seg_urls,
|
|
247
|
+
kb_system=None,
|
|
248
|
+
)
|
|
249
|
+
column_body.attrs['hx-swap-oob'] = 'outerHTML'
|
|
250
|
+
|
|
251
|
+
# Build OOB updates
|
|
252
|
+
segment_count = len(fa_segments)
|
|
253
|
+
chunk_count = len(vad_chunk_dicts)
|
|
254
|
+
|
|
255
|
+
alignment_status_oob = render_alignment_status(segment_count, chunk_count, oob=True)
|
|
256
|
+
mini_stats_oob = render_seg_mini_stats_badge(fa_segments, oob=True)
|
|
257
|
+
fa_controls_oob = render_fa_controls(
|
|
258
|
+
toggle_url=toggle_url,
|
|
259
|
+
active_presplit="forced_alignment",
|
|
260
|
+
fa_available=True,
|
|
261
|
+
oob=True,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return (column_body, alignment_status_oob, mini_stats_oob, fa_controls_oob)
|
|
265
|
+
|
|
266
|
+
# %% ../../nbs/routes/forced_alignment.ipynb #cell-handler-toggle
|
|
267
|
+
async def _handle_fa_toggle(
|
|
268
|
+
state_store: SQLiteWorkflowStateStore,
|
|
269
|
+
workflow_id: str,
|
|
270
|
+
request: Any,
|
|
271
|
+
sess: Any,
|
|
272
|
+
seg_urls: SegmentationUrls,
|
|
273
|
+
toggle_url: str,
|
|
274
|
+
) -> Any: # OOB updates for card stack, alignment status, FA controls, mini-stats
|
|
275
|
+
"""Toggle between NLTK and force-aligned pre-splits."""
|
|
276
|
+
form = await request.form()
|
|
277
|
+
target_mode = form.get("mode", "nltk")
|
|
278
|
+
|
|
279
|
+
session_id = get_session_id(sess)
|
|
280
|
+
state = state_store.get_state(workflow_id, session_id)
|
|
281
|
+
step_states = state.get("step_states", {})
|
|
282
|
+
seg_state = step_states.get("segmentation", {})
|
|
283
|
+
align_state = step_states.get("alignment", {})
|
|
284
|
+
|
|
285
|
+
current_mode = seg_state.get("active_presplit", "nltk")
|
|
286
|
+
|
|
287
|
+
if target_mode == current_mode:
|
|
288
|
+
return # No-op
|
|
289
|
+
|
|
290
|
+
# Save current working segments to the current mode's storage
|
|
291
|
+
if current_mode == "nltk":
|
|
292
|
+
seg_state["nltk_segments"] = seg_state.get("segments", [])
|
|
293
|
+
else:
|
|
294
|
+
seg_state["fa_segments"] = seg_state.get("segments", [])
|
|
295
|
+
|
|
296
|
+
# Restore target mode's segments
|
|
297
|
+
if target_mode == "nltk":
|
|
298
|
+
seg_state["segments"] = seg_state.get("nltk_segments", seg_state.get("initial_segments", []))
|
|
299
|
+
seg_state["initial_segments"] = seg_state.get("nltk_initial_segments", seg_state.get("initial_segments", []))
|
|
300
|
+
else:
|
|
301
|
+
seg_state["segments"] = seg_state.get("fa_segments", [])
|
|
302
|
+
seg_state["initial_segments"] = seg_state.get("fa_initial_segments", [])
|
|
303
|
+
|
|
304
|
+
seg_state["active_presplit"] = target_mode
|
|
305
|
+
seg_state["focused_index"] = 0
|
|
306
|
+
seg_state["history"] = []
|
|
307
|
+
|
|
308
|
+
step_states["segmentation"] = seg_state
|
|
309
|
+
state["step_states"] = step_states
|
|
310
|
+
state_store.update_state(workflow_id, session_id, state)
|
|
311
|
+
|
|
312
|
+
# Re-render card stack with new segments and add OOB outerHTML
|
|
313
|
+
segments = [TextSegment.from_dict(s) for s in seg_state["segments"]]
|
|
314
|
+
|
|
315
|
+
column_body = render_seg_column_body(
|
|
316
|
+
segments=segments,
|
|
317
|
+
focused_index=0,
|
|
318
|
+
visible_count=seg_state.get("visible_count", 3),
|
|
319
|
+
card_width=seg_state.get("card_width", 80),
|
|
320
|
+
urls=seg_urls,
|
|
321
|
+
kb_system=None,
|
|
322
|
+
)
|
|
323
|
+
column_body.attrs['hx-swap-oob'] = 'outerHTML'
|
|
324
|
+
|
|
325
|
+
# Build OOB updates
|
|
326
|
+
chunk_count = len(align_state.get("vad_chunks", []))
|
|
327
|
+
|
|
328
|
+
alignment_status_oob = render_alignment_status(len(segments), chunk_count, oob=True)
|
|
329
|
+
mini_stats_oob = render_seg_mini_stats_badge(segments, oob=True)
|
|
330
|
+
fa_controls_oob = render_fa_controls(
|
|
331
|
+
toggle_url=toggle_url,
|
|
332
|
+
active_presplit=target_mode,
|
|
333
|
+
fa_available=True,
|
|
334
|
+
oob=True,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return (column_body, alignment_status_oob, mini_stats_oob, fa_controls_oob)
|
|
338
|
+
|
|
339
|
+
# %% ../../nbs/routes/forced_alignment.ipynb #cell-router
|
|
340
|
+
def init_forced_alignment_routers(
|
|
341
|
+
state_store: SQLiteWorkflowStateStore, # State store instance
|
|
342
|
+
workflow_id: str, # Workflow identifier
|
|
343
|
+
fa_service: ForcedAlignmentService, # Forced alignment service
|
|
344
|
+
source_service: SourceService, # Source service for audio paths/text
|
|
345
|
+
seg_urls: SegmentationUrls, # Segmentation URL bundle
|
|
346
|
+
prefix: str, # Route prefix (e.g., "/fa")
|
|
347
|
+
) -> Tuple[APIRouter, Dict[str, Callable]]: # (router, route_dict)
|
|
348
|
+
"""Initialize forced alignment routes."""
|
|
349
|
+
router = APIRouter(prefix=prefix)
|
|
350
|
+
|
|
351
|
+
@router
|
|
352
|
+
async def trigger(request, sess):
|
|
353
|
+
"""Trigger forced alignment."""
|
|
354
|
+
return await _handle_fa_trigger(
|
|
355
|
+
state_store, workflow_id, fa_service, source_service,
|
|
356
|
+
request, sess, seg_urls,
|
|
357
|
+
progress_url=progress.to(),
|
|
358
|
+
toggle_url=toggle.to(),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
@router
|
|
362
|
+
async def progress(request, sess):
|
|
363
|
+
"""Poll forced alignment progress."""
|
|
364
|
+
# Check if FA is still running by checking plugin progress
|
|
365
|
+
proxy = fa_service._manager.get_plugin(fa_service._plugin_name)
|
|
366
|
+
if proxy:
|
|
367
|
+
try:
|
|
368
|
+
prog = await proxy.get_progress_async()
|
|
369
|
+
if prog and prog.get("progress", 0) < 1.0:
|
|
370
|
+
return render_fa_progress(
|
|
371
|
+
progress_val=prog["progress"],
|
|
372
|
+
message=prog.get("message", "Processing..."),
|
|
373
|
+
progress_url=progress.to(),
|
|
374
|
+
)
|
|
375
|
+
except Exception:
|
|
376
|
+
pass
|
|
377
|
+
|
|
378
|
+
# FA is done or not running — show trigger button (will be replaced by toggle after trigger completes)
|
|
379
|
+
return render_fa_trigger_button(trigger.to())
|
|
380
|
+
|
|
381
|
+
@router
|
|
382
|
+
async def toggle(request, sess):
|
|
383
|
+
"""Toggle between NLTK and force-aligned pre-splits."""
|
|
384
|
+
return await _handle_fa_toggle(
|
|
385
|
+
state_store, workflow_id,
|
|
386
|
+
request, sess, seg_urls,
|
|
387
|
+
toggle_url=toggle.to(),
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
routes = {
|
|
391
|
+
"trigger": trigger,
|
|
392
|
+
"progress": progress,
|
|
393
|
+
"toggle": toggle,
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return router, routes
|
|
File without changes
|