cjm-transcript-source-select 0.0.1__tar.gz → 0.0.3__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_source_select-0.0.1/cjm_transcript_source_select.egg-info → cjm_transcript_source_select-0.0.3}/PKG-INFO +58 -25
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/README.md +57 -24
- cjm_transcript_source_select-0.0.3/cjm_transcript_source_select/__init__.py +1 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/_modidx.py +6 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/components/selection_queue.py +1 -1
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/components/source_browser.py +8 -3
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/routes/core.py +46 -1
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/routes/filtering.py +16 -2
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/routes/queue.py +37 -10
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/services/source_utils.py +44 -13
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3/cjm_transcript_source_select.egg-info}/PKG-INFO +58 -25
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/settings.ini +15 -23
- cjm_transcript_source_select-0.0.1/cjm_transcript_source_select/__init__.py +0 -1
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/LICENSE +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/MANIFEST.in +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/components/__init__.py +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/components/helpers.py +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/components/local_files.py +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/components/preview_panel.py +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/components/step_renderer.py +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/html_ids.py +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/models.py +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/routes/__init__.py +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/routes/init.py +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/routes/local_files.py +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/routes/tabs.py +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/services/__init__.py +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/services/source.py +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/utils.py +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select.egg-info/SOURCES.txt +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select.egg-info/dependency_links.txt +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select.egg-info/entry_points.txt +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select.egg-info/not-zip-safe +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select.egg-info/requires.txt +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select.egg-info/top_level.txt +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/pyproject.toml +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/setup.cfg +0 -0
- {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: cjm-transcript-source-select
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: FastHTML source selection component for transcript decomposition workflows, with federated database browsing, drag-drop ordering, and keyboard navigation.
|
|
5
5
|
Home-page: https://github.com/cj-mills/cjm-transcript-source-select
|
|
6
6
|
Author: Christian J. Mills
|
|
@@ -105,54 +105,55 @@ graph LR
|
|
|
105
105
|
components_local_files --> html_ids
|
|
106
106
|
components_preview_panel --> html_ids
|
|
107
107
|
components_selection_queue --> html_ids
|
|
108
|
-
components_source_browser --> services_source_utils
|
|
109
108
|
components_source_browser --> utils
|
|
109
|
+
components_source_browser --> services_source_utils
|
|
110
110
|
components_source_browser --> html_ids
|
|
111
|
-
components_step_renderer -->
|
|
111
|
+
components_step_renderer --> components_selection_queue
|
|
112
|
+
components_step_renderer --> components_preview_panel
|
|
113
|
+
components_step_renderer --> components_local_files
|
|
112
114
|
components_step_renderer --> components_source_browser
|
|
115
|
+
components_step_renderer --> components_helpers
|
|
113
116
|
components_step_renderer --> utils
|
|
114
|
-
components_step_renderer --> components_preview_panel
|
|
115
117
|
components_step_renderer --> html_ids
|
|
116
|
-
components_step_renderer --> components_local_files
|
|
117
|
-
components_step_renderer --> components_selection_queue
|
|
118
118
|
components_step_renderer --> models
|
|
119
119
|
routes_core --> components_step_renderer
|
|
120
|
+
routes_core --> services_source
|
|
120
121
|
routes_core --> models
|
|
122
|
+
routes_core --> html_ids
|
|
121
123
|
routes_core --> components_selection_queue
|
|
122
124
|
routes_core --> components_source_browser
|
|
123
|
-
|
|
124
|
-
routes_filtering --> routes_core
|
|
125
|
+
routes_filtering --> services_source
|
|
125
126
|
routes_filtering --> services_source_utils
|
|
126
|
-
routes_filtering -->
|
|
127
|
+
routes_filtering --> routes_core
|
|
127
128
|
routes_filtering --> components_source_browser
|
|
128
|
-
routes_filtering -->
|
|
129
|
-
routes_init --> routes_queue
|
|
130
|
-
routes_init --> routes_local_files
|
|
131
|
-
routes_init --> models
|
|
129
|
+
routes_filtering --> models
|
|
132
130
|
routes_init --> routes_filtering
|
|
133
|
-
routes_init -->
|
|
131
|
+
routes_init --> routes_local_files
|
|
134
132
|
routes_init --> services_source
|
|
133
|
+
routes_init --> routes_tabs
|
|
134
|
+
routes_init --> models
|
|
135
135
|
routes_init --> routes_core
|
|
136
|
-
|
|
137
|
-
routes_local_files --> routes_core
|
|
136
|
+
routes_init --> routes_queue
|
|
138
137
|
routes_local_files --> services_source
|
|
138
|
+
routes_local_files --> routes_core
|
|
139
139
|
routes_local_files --> models
|
|
140
|
+
routes_local_files --> components_local_files
|
|
140
141
|
routes_queue --> routes_core
|
|
141
|
-
routes_queue --> services_source_utils
|
|
142
|
-
routes_queue --> models
|
|
143
142
|
routes_queue --> components_preview_panel
|
|
144
143
|
routes_queue --> services_source
|
|
144
|
+
routes_queue --> services_source_utils
|
|
145
|
+
routes_queue --> models
|
|
145
146
|
routes_tabs --> routes_local_files
|
|
146
|
-
routes_tabs -->
|
|
147
|
+
routes_tabs --> components_source_browser
|
|
147
148
|
routes_tabs --> services_source_utils
|
|
149
|
+
routes_tabs --> services_source
|
|
148
150
|
routes_tabs --> routes_core
|
|
149
|
-
routes_tabs --> components_source_browser
|
|
150
151
|
routes_tabs --> models
|
|
152
|
+
routes_tabs --> components_local_files
|
|
151
153
|
routes_tabs --> components_step_renderer
|
|
152
|
-
routes_tabs --> services_source
|
|
153
154
|
```
|
|
154
155
|
|
|
155
|
-
*
|
|
156
|
+
*51 cross-module dependencies detected*
|
|
156
157
|
|
|
157
158
|
## CLI Reference
|
|
158
159
|
|
|
@@ -186,6 +187,26 @@ def _get_step_state(
|
|
|
186
187
|
"Get the selection step state from the workflow state store."
|
|
187
188
|
```
|
|
188
189
|
|
|
190
|
+
``` python
|
|
191
|
+
def _find_duplicate_media_source(
|
|
192
|
+
source_service: SourceService, # Source service for lookups
|
|
193
|
+
record_id: str, # Candidate record ID
|
|
194
|
+
provider_id: str, # Candidate provider ID
|
|
195
|
+
selected_sources: List[Dict[str, str]], # Current selections
|
|
196
|
+
) -> Optional[Dict[str, str]]: # Conflicting source dict or None
|
|
197
|
+
"Find an already-selected source that shares the same audio file."
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
``` python
|
|
201
|
+
def _render_duplicate_flash(
|
|
202
|
+
candidate_record_id: str, # Record ID of the row the user clicked
|
|
203
|
+
candidate_provider_id: str, # Provider ID of the row the user clicked
|
|
204
|
+
existing_record_id: str, # Record ID of the conflicting selected row
|
|
205
|
+
existing_provider_id: str, # Provider ID of the conflicting selected row
|
|
206
|
+
) -> Script: # OOB script element for flash animation
|
|
207
|
+
"Render a self-removing Script that briefly flashes two source rows with error color."
|
|
208
|
+
```
|
|
209
|
+
|
|
189
210
|
``` python
|
|
190
211
|
def _get_active_source_tab(
|
|
191
212
|
state_store: WorkflowStateStore, # The workflow state store
|
|
@@ -658,6 +679,7 @@ def _handle_selection_remove(
|
|
|
658
679
|
request, # FastHTML request object
|
|
659
680
|
sess, # FastHTML session object
|
|
660
681
|
record_id: str, # Job ID to remove
|
|
682
|
+
provider_id: str, # Plugin name for the source
|
|
661
683
|
urls: SelectionUrls, # URL bundle for rendering
|
|
662
684
|
): # Queue component with OOB stats, optionally with OOB source list
|
|
663
685
|
"Remove a source from the selection queue."
|
|
@@ -698,7 +720,7 @@ def _handle_selection_select_all(
|
|
|
698
720
|
grouping_mode: str, # Current grouping mode: "media_path" or "batch_id"
|
|
699
721
|
urls: SelectionUrls, # URL bundle for rendering
|
|
700
722
|
): # Queue component with OOB stats, optionally with OOB source list
|
|
701
|
-
"Select all transcriptions for a given group."
|
|
723
|
+
"Select all transcriptions for a given group, skipping duplicate audio sources."
|
|
702
724
|
```
|
|
703
725
|
|
|
704
726
|
``` python
|
|
@@ -1088,6 +1110,7 @@ from cjm_transcript_source_select.services.source_utils import (
|
|
|
1088
1110
|
group_transcriptions,
|
|
1089
1111
|
group_transcriptions_by_audio,
|
|
1090
1112
|
is_source_selected,
|
|
1113
|
+
get_selected_media_paths,
|
|
1091
1114
|
filter_transcriptions,
|
|
1092
1115
|
select_all_in_group,
|
|
1093
1116
|
toggle_source_selection,
|
|
@@ -1133,9 +1156,18 @@ def group_transcriptions_by_audio(
|
|
|
1133
1156
|
``` python
|
|
1134
1157
|
def is_source_selected(
|
|
1135
1158
|
record_id: str, # Job ID to check
|
|
1159
|
+
provider_id: str, # Provider ID to check
|
|
1136
1160
|
selected_sources: List[Dict[str, str]] # List of selected sources
|
|
1137
1161
|
) -> bool: # True if source is selected
|
|
1138
|
-
"Check if a source is in the selected list."
|
|
1162
|
+
"Check if a source is in the selected list by (record_id, provider_id) pair."
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
``` python
|
|
1166
|
+
def get_selected_media_paths(
|
|
1167
|
+
selected_sources: List[Dict[str, str]], # Current selections (record_id, provider_id)
|
|
1168
|
+
all_transcriptions: List[Dict[str, Any]], # All available transcription records
|
|
1169
|
+
) -> Set[str]: # Media paths already represented in selections
|
|
1170
|
+
"Get the set of media_paths for currently selected sources."
|
|
1139
1171
|
```
|
|
1140
1172
|
|
|
1141
1173
|
``` python
|
|
@@ -1152,6 +1184,7 @@ def select_all_in_group(
|
|
|
1152
1184
|
group_key: str, # Group key to match against
|
|
1153
1185
|
grouping_mode: str, # Grouping mode: "media_path" or "batch_id"
|
|
1154
1186
|
selected_sources: List[Dict[str, str]], # Current selections
|
|
1187
|
+
excluded_media_paths: Optional[Set[str]] = None, # Media paths to skip (already selected)
|
|
1155
1188
|
) -> List[Dict[str, str]]: # Updated selections with new items appended
|
|
1156
1189
|
"Add all transcriptions matching a group key to the selection list, skipping duplicates."
|
|
1157
1190
|
```
|
|
@@ -1162,7 +1195,7 @@ def toggle_source_selection(
|
|
|
1162
1195
|
provider_id: str, # Plugin name for the source
|
|
1163
1196
|
selected_sources: List[Dict[str, str]], # Current selections
|
|
1164
1197
|
) -> List[Dict[str, str]]: # Updated selections
|
|
1165
|
-
"Toggle a source in or out of the selection list."
|
|
1198
|
+
"Toggle a source in or out of the selection list by (record_id, provider_id) pair."
|
|
1166
1199
|
```
|
|
1167
1200
|
|
|
1168
1201
|
``` python
|
|
@@ -62,54 +62,55 @@ graph LR
|
|
|
62
62
|
components_local_files --> html_ids
|
|
63
63
|
components_preview_panel --> html_ids
|
|
64
64
|
components_selection_queue --> html_ids
|
|
65
|
-
components_source_browser --> services_source_utils
|
|
66
65
|
components_source_browser --> utils
|
|
66
|
+
components_source_browser --> services_source_utils
|
|
67
67
|
components_source_browser --> html_ids
|
|
68
|
-
components_step_renderer -->
|
|
68
|
+
components_step_renderer --> components_selection_queue
|
|
69
|
+
components_step_renderer --> components_preview_panel
|
|
70
|
+
components_step_renderer --> components_local_files
|
|
69
71
|
components_step_renderer --> components_source_browser
|
|
72
|
+
components_step_renderer --> components_helpers
|
|
70
73
|
components_step_renderer --> utils
|
|
71
|
-
components_step_renderer --> components_preview_panel
|
|
72
74
|
components_step_renderer --> html_ids
|
|
73
|
-
components_step_renderer --> components_local_files
|
|
74
|
-
components_step_renderer --> components_selection_queue
|
|
75
75
|
components_step_renderer --> models
|
|
76
76
|
routes_core --> components_step_renderer
|
|
77
|
+
routes_core --> services_source
|
|
77
78
|
routes_core --> models
|
|
79
|
+
routes_core --> html_ids
|
|
78
80
|
routes_core --> components_selection_queue
|
|
79
81
|
routes_core --> components_source_browser
|
|
80
|
-
|
|
81
|
-
routes_filtering --> routes_core
|
|
82
|
+
routes_filtering --> services_source
|
|
82
83
|
routes_filtering --> services_source_utils
|
|
83
|
-
routes_filtering -->
|
|
84
|
+
routes_filtering --> routes_core
|
|
84
85
|
routes_filtering --> components_source_browser
|
|
85
|
-
routes_filtering -->
|
|
86
|
-
routes_init --> routes_queue
|
|
87
|
-
routes_init --> routes_local_files
|
|
88
|
-
routes_init --> models
|
|
86
|
+
routes_filtering --> models
|
|
89
87
|
routes_init --> routes_filtering
|
|
90
|
-
routes_init -->
|
|
88
|
+
routes_init --> routes_local_files
|
|
91
89
|
routes_init --> services_source
|
|
90
|
+
routes_init --> routes_tabs
|
|
91
|
+
routes_init --> models
|
|
92
92
|
routes_init --> routes_core
|
|
93
|
-
|
|
94
|
-
routes_local_files --> routes_core
|
|
93
|
+
routes_init --> routes_queue
|
|
95
94
|
routes_local_files --> services_source
|
|
95
|
+
routes_local_files --> routes_core
|
|
96
96
|
routes_local_files --> models
|
|
97
|
+
routes_local_files --> components_local_files
|
|
97
98
|
routes_queue --> routes_core
|
|
98
|
-
routes_queue --> services_source_utils
|
|
99
|
-
routes_queue --> models
|
|
100
99
|
routes_queue --> components_preview_panel
|
|
101
100
|
routes_queue --> services_source
|
|
101
|
+
routes_queue --> services_source_utils
|
|
102
|
+
routes_queue --> models
|
|
102
103
|
routes_tabs --> routes_local_files
|
|
103
|
-
routes_tabs -->
|
|
104
|
+
routes_tabs --> components_source_browser
|
|
104
105
|
routes_tabs --> services_source_utils
|
|
106
|
+
routes_tabs --> services_source
|
|
105
107
|
routes_tabs --> routes_core
|
|
106
|
-
routes_tabs --> components_source_browser
|
|
107
108
|
routes_tabs --> models
|
|
109
|
+
routes_tabs --> components_local_files
|
|
108
110
|
routes_tabs --> components_step_renderer
|
|
109
|
-
routes_tabs --> services_source
|
|
110
111
|
```
|
|
111
112
|
|
|
112
|
-
*
|
|
113
|
+
*51 cross-module dependencies detected*
|
|
113
114
|
|
|
114
115
|
## CLI Reference
|
|
115
116
|
|
|
@@ -143,6 +144,26 @@ def _get_step_state(
|
|
|
143
144
|
"Get the selection step state from the workflow state store."
|
|
144
145
|
```
|
|
145
146
|
|
|
147
|
+
``` python
|
|
148
|
+
def _find_duplicate_media_source(
|
|
149
|
+
source_service: SourceService, # Source service for lookups
|
|
150
|
+
record_id: str, # Candidate record ID
|
|
151
|
+
provider_id: str, # Candidate provider ID
|
|
152
|
+
selected_sources: List[Dict[str, str]], # Current selections
|
|
153
|
+
) -> Optional[Dict[str, str]]: # Conflicting source dict or None
|
|
154
|
+
"Find an already-selected source that shares the same audio file."
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
``` python
|
|
158
|
+
def _render_duplicate_flash(
|
|
159
|
+
candidate_record_id: str, # Record ID of the row the user clicked
|
|
160
|
+
candidate_provider_id: str, # Provider ID of the row the user clicked
|
|
161
|
+
existing_record_id: str, # Record ID of the conflicting selected row
|
|
162
|
+
existing_provider_id: str, # Provider ID of the conflicting selected row
|
|
163
|
+
) -> Script: # OOB script element for flash animation
|
|
164
|
+
"Render a self-removing Script that briefly flashes two source rows with error color."
|
|
165
|
+
```
|
|
166
|
+
|
|
146
167
|
``` python
|
|
147
168
|
def _get_active_source_tab(
|
|
148
169
|
state_store: WorkflowStateStore, # The workflow state store
|
|
@@ -615,6 +636,7 @@ def _handle_selection_remove(
|
|
|
615
636
|
request, # FastHTML request object
|
|
616
637
|
sess, # FastHTML session object
|
|
617
638
|
record_id: str, # Job ID to remove
|
|
639
|
+
provider_id: str, # Plugin name for the source
|
|
618
640
|
urls: SelectionUrls, # URL bundle for rendering
|
|
619
641
|
): # Queue component with OOB stats, optionally with OOB source list
|
|
620
642
|
"Remove a source from the selection queue."
|
|
@@ -655,7 +677,7 @@ def _handle_selection_select_all(
|
|
|
655
677
|
grouping_mode: str, # Current grouping mode: "media_path" or "batch_id"
|
|
656
678
|
urls: SelectionUrls, # URL bundle for rendering
|
|
657
679
|
): # Queue component with OOB stats, optionally with OOB source list
|
|
658
|
-
"Select all transcriptions for a given group."
|
|
680
|
+
"Select all transcriptions for a given group, skipping duplicate audio sources."
|
|
659
681
|
```
|
|
660
682
|
|
|
661
683
|
``` python
|
|
@@ -1045,6 +1067,7 @@ from cjm_transcript_source_select.services.source_utils import (
|
|
|
1045
1067
|
group_transcriptions,
|
|
1046
1068
|
group_transcriptions_by_audio,
|
|
1047
1069
|
is_source_selected,
|
|
1070
|
+
get_selected_media_paths,
|
|
1048
1071
|
filter_transcriptions,
|
|
1049
1072
|
select_all_in_group,
|
|
1050
1073
|
toggle_source_selection,
|
|
@@ -1090,9 +1113,18 @@ def group_transcriptions_by_audio(
|
|
|
1090
1113
|
``` python
|
|
1091
1114
|
def is_source_selected(
|
|
1092
1115
|
record_id: str, # Job ID to check
|
|
1116
|
+
provider_id: str, # Provider ID to check
|
|
1093
1117
|
selected_sources: List[Dict[str, str]] # List of selected sources
|
|
1094
1118
|
) -> bool: # True if source is selected
|
|
1095
|
-
"Check if a source is in the selected list."
|
|
1119
|
+
"Check if a source is in the selected list by (record_id, provider_id) pair."
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
``` python
|
|
1123
|
+
def get_selected_media_paths(
|
|
1124
|
+
selected_sources: List[Dict[str, str]], # Current selections (record_id, provider_id)
|
|
1125
|
+
all_transcriptions: List[Dict[str, Any]], # All available transcription records
|
|
1126
|
+
) -> Set[str]: # Media paths already represented in selections
|
|
1127
|
+
"Get the set of media_paths for currently selected sources."
|
|
1096
1128
|
```
|
|
1097
1129
|
|
|
1098
1130
|
``` python
|
|
@@ -1109,6 +1141,7 @@ def select_all_in_group(
|
|
|
1109
1141
|
group_key: str, # Group key to match against
|
|
1110
1142
|
grouping_mode: str, # Grouping mode: "media_path" or "batch_id"
|
|
1111
1143
|
selected_sources: List[Dict[str, str]], # Current selections
|
|
1144
|
+
excluded_media_paths: Optional[Set[str]] = None, # Media paths to skip (already selected)
|
|
1112
1145
|
) -> List[Dict[str, str]]: # Updated selections with new items appended
|
|
1113
1146
|
"Add all transcriptions matching a group key to the selection list, skipping duplicates."
|
|
1114
1147
|
```
|
|
@@ -1119,7 +1152,7 @@ def toggle_source_selection(
|
|
|
1119
1152
|
provider_id: str, # Plugin name for the source
|
|
1120
1153
|
selected_sources: List[Dict[str, str]], # Current selections
|
|
1121
1154
|
) -> List[Dict[str, str]]: # Updated selections
|
|
1122
|
-
"Toggle a source in or out of the selection list."
|
|
1155
|
+
"Toggle a source in or out of the selection list by (record_id, provider_id) pair."
|
|
1123
1156
|
```
|
|
1124
1157
|
|
|
1125
1158
|
``` python
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.3"
|
|
@@ -79,10 +79,14 @@ d = { 'settings': { 'branch': 'main',
|
|
|
79
79
|
'cjm_transcript_source_select/models.py')},
|
|
80
80
|
'cjm_transcript_source_select.routes.core': { 'cjm_transcript_source_select.routes.core._build_queue_response': ( 'routes/core.html#_build_queue_response',
|
|
81
81
|
'cjm_transcript_source_select/routes/core.py'),
|
|
82
|
+
'cjm_transcript_source_select.routes.core._find_duplicate_media_source': ( 'routes/core.html#_find_duplicate_media_source',
|
|
83
|
+
'cjm_transcript_source_select/routes/core.py'),
|
|
82
84
|
'cjm_transcript_source_select.routes.core._get_active_source_tab': ( 'routes/core.html#_get_active_source_tab',
|
|
83
85
|
'cjm_transcript_source_select/routes/core.py'),
|
|
84
86
|
'cjm_transcript_source_select.routes.core._get_step_state': ( 'routes/core.html#_get_step_state',
|
|
85
87
|
'cjm_transcript_source_select/routes/core.py'),
|
|
88
|
+
'cjm_transcript_source_select.routes.core._render_duplicate_flash': ( 'routes/core.html#_render_duplicate_flash',
|
|
89
|
+
'cjm_transcript_source_select/routes/core.py'),
|
|
86
90
|
'cjm_transcript_source_select.routes.core._update_step_state': ( 'routes/core.html#_update_step_state',
|
|
87
91
|
'cjm_transcript_source_select/routes/core.py')},
|
|
88
92
|
'cjm_transcript_source_select.routes.filtering': { 'cjm_transcript_source_select.routes.filtering._handle_grouping_change': ( 'routes/filtering.html#_handle_grouping_change',
|
|
@@ -197,6 +201,8 @@ d = { 'settings': { 'branch': 'main',
|
|
|
197
201
|
'cjm_transcript_source_select/services/source_utils.py'),
|
|
198
202
|
'cjm_transcript_source_select.services.source_utils.filter_transcriptions': ( 'services/source_utils.html#filter_transcriptions',
|
|
199
203
|
'cjm_transcript_source_select/services/source_utils.py'),
|
|
204
|
+
'cjm_transcript_source_select.services.source_utils.get_selected_media_paths': ( 'services/source_utils.html#get_selected_media_paths',
|
|
205
|
+
'cjm_transcript_source_select/services/source_utils.py'),
|
|
200
206
|
'cjm_transcript_source_select.services.source_utils.group_transcriptions': ( 'services/source_utils.html#group_transcriptions',
|
|
201
207
|
'cjm_transcript_source_select/services/source_utils.py'),
|
|
202
208
|
'cjm_transcript_source_select.services.source_utils.group_transcriptions_by_audio': ( 'services/source_utils.html#group_transcriptions_by_audio',
|
|
@@ -73,7 +73,7 @@ def _render_queue_item(
|
|
|
73
73
|
lucide_icon("x", size=4, cls=str(text_dui.base_content.opacity(60))),
|
|
74
74
|
cls=combine_classes(btn, btn_styles.ghost, btn_sizes.xs, m.l(1)),
|
|
75
75
|
hx_post=remove_url,
|
|
76
|
-
hx_vals=json.dumps({"record_id": record_id}),
|
|
76
|
+
hx_vals=json.dumps({"record_id": record_id, "provider_id": provider_id}),
|
|
77
77
|
hx_target=SelectionHtmlIds.as_selector(SelectionHtmlIds.QUEUE_CONTAINER),
|
|
78
78
|
hx_swap="outerHTML",
|
|
79
79
|
data_action="remove",
|
|
@@ -39,6 +39,7 @@ from cjm_fasthtml_tailwind.utilities.interactivity import cursor
|
|
|
39
39
|
from cjm_fasthtml_tailwind.utilities.flexbox_and_grid import (
|
|
40
40
|
flex_display, flex_direction, justify, items, gap, grow
|
|
41
41
|
)
|
|
42
|
+
from cjm_fasthtml_tailwind.utilities.transitions_and_animation import transition, duration
|
|
42
43
|
from cjm_fasthtml_tailwind.core.base import combine_classes
|
|
43
44
|
|
|
44
45
|
# Local imports
|
|
@@ -154,7 +155,7 @@ def _render_source_row(
|
|
|
154
155
|
hx_vals=json.dumps({"record_id": record_id, "provider_id": provider_id}),
|
|
155
156
|
hx_target=SelectionHtmlIds.as_selector(SelectionHtmlIds.QUEUE_CONTAINER),
|
|
156
157
|
hx_swap="outerHTML",
|
|
157
|
-
name=f"source_{record_id}"
|
|
158
|
+
name=f"source_{provider_id}_{record_id}"
|
|
158
159
|
),
|
|
159
160
|
cls=str(w(12))
|
|
160
161
|
),
|
|
@@ -177,7 +178,9 @@ def _render_source_row(
|
|
|
177
178
|
cls=combine_classes(
|
|
178
179
|
bg_dui.primary.opacity(10) if is_selected else "",
|
|
179
180
|
bg_dui.base_200.hover,
|
|
180
|
-
cursor.pointer
|
|
181
|
+
cursor.pointer,
|
|
182
|
+
transition.colors,
|
|
183
|
+
duration(200)
|
|
181
184
|
),
|
|
182
185
|
tabindex="0",
|
|
183
186
|
data_selectable="true",
|
|
@@ -260,7 +263,9 @@ def _render_source_list(
|
|
|
260
263
|
|
|
261
264
|
# Add record rows
|
|
262
265
|
for record in records:
|
|
263
|
-
is_selected = is_source_selected(
|
|
266
|
+
is_selected = is_source_selected(
|
|
267
|
+
record.get("record_id", ""), record.get("provider_id", ""), selected_sources
|
|
268
|
+
)
|
|
264
269
|
table_rows.append(_render_source_row(
|
|
265
270
|
record, is_selected, add_url, remove_url, preview_url,
|
|
266
271
|
is_first=is_first_record
|
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
__all__ = ['DEBUG_SELECTION_STATE', 'WorkflowStateStore']
|
|
7
7
|
|
|
8
8
|
# %% ../../nbs/routes/core.ipynb #sel-core-imports
|
|
9
|
-
from typing import List, Dict, Any, Tuple, Union
|
|
9
|
+
from typing import List, Dict, Any, Optional, Tuple, Union
|
|
10
|
+
|
|
11
|
+
from fasthtml.common import Script
|
|
10
12
|
|
|
11
13
|
from cjm_workflow_state.state_store import SQLiteWorkflowStateStore
|
|
12
14
|
|
|
13
15
|
from ..models import SelectionUrls
|
|
16
|
+
from ..html_ids import SelectionHtmlIds
|
|
14
17
|
from cjm_transcript_source_select.components.source_browser import (
|
|
15
18
|
_render_source_list
|
|
16
19
|
)
|
|
@@ -39,6 +42,48 @@ def _get_step_state(
|
|
|
39
42
|
step_states = workflow_state.get("step_states", {})
|
|
40
43
|
return step_states.get("selection", {})
|
|
41
44
|
|
|
45
|
+
def _find_duplicate_media_source(
|
|
46
|
+
source_service: SourceService, # Source service for lookups
|
|
47
|
+
record_id: str, # Candidate record ID
|
|
48
|
+
provider_id: str, # Candidate provider ID
|
|
49
|
+
selected_sources: List[Dict[str, str]], # Current selections
|
|
50
|
+
) -> Optional[Dict[str, str]]: # Conflicting source dict or None
|
|
51
|
+
"""Find an already-selected source that shares the same audio file."""
|
|
52
|
+
candidate = source_service.get_transcription_by_id(record_id, provider_id)
|
|
53
|
+
if not candidate or not candidate.media_path:
|
|
54
|
+
return None
|
|
55
|
+
for s in selected_sources:
|
|
56
|
+
existing = source_service.get_transcription_by_id(s["record_id"], s["provider_id"])
|
|
57
|
+
if existing and existing.media_path == candidate.media_path:
|
|
58
|
+
return s
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def _render_duplicate_flash(
|
|
62
|
+
candidate_record_id: str, # Record ID of the row the user clicked
|
|
63
|
+
candidate_provider_id: str, # Provider ID of the row the user clicked
|
|
64
|
+
existing_record_id: str, # Record ID of the conflicting selected row
|
|
65
|
+
existing_provider_id: str, # Provider ID of the conflicting selected row
|
|
66
|
+
) -> Script: # OOB script element for flash animation
|
|
67
|
+
"""Render a self-removing Script that briefly flashes two source rows with error color."""
|
|
68
|
+
row1 = SelectionHtmlIds.source_row(candidate_record_id, candidate_provider_id)
|
|
69
|
+
row2 = SelectionHtmlIds.source_row(existing_record_id, existing_provider_id)
|
|
70
|
+
return Script(f"""
|
|
71
|
+
(function() {{
|
|
72
|
+
var s = document.currentScript;
|
|
73
|
+
var r1 = document.getElementById('{row1}');
|
|
74
|
+
var r2 = document.getElementById('{row2}');
|
|
75
|
+
[r1, r2].forEach(function(r) {{
|
|
76
|
+
if (r) {{ r.classList.add('bg-error'); }}
|
|
77
|
+
}});
|
|
78
|
+
setTimeout(function() {{
|
|
79
|
+
[r1, r2].forEach(function(r) {{
|
|
80
|
+
if (r) {{ r.classList.remove('bg-error'); }}
|
|
81
|
+
}});
|
|
82
|
+
if (s) {{ s.remove(); }}
|
|
83
|
+
}}, 400);
|
|
84
|
+
}})();
|
|
85
|
+
""")
|
|
86
|
+
|
|
42
87
|
# %% ../../nbs/routes/core.ipynb #3zll5oy1hsc
|
|
43
88
|
def _get_active_source_tab(
|
|
44
89
|
state_store: WorkflowStateStore, # The workflow state store
|
|
@@ -14,14 +14,15 @@ from cjm_fasthtml_interactions.core.state_store import get_session_id
|
|
|
14
14
|
|
|
15
15
|
from ..models import SelectionUrls
|
|
16
16
|
from cjm_transcript_source_select.routes.core import (
|
|
17
|
-
WorkflowStateStore, _get_step_state, _update_step_state, _build_queue_response
|
|
17
|
+
WorkflowStateStore, _get_step_state, _update_step_state, _build_queue_response,
|
|
18
|
+
_find_duplicate_media_source, _render_duplicate_flash
|
|
18
19
|
)
|
|
19
20
|
from cjm_transcript_source_select.components.source_browser import (
|
|
20
21
|
_render_source_list
|
|
21
22
|
)
|
|
22
23
|
from ..services.source import SourceService
|
|
23
24
|
from cjm_transcript_source_select.services.source_utils import (
|
|
24
|
-
filter_transcriptions, toggle_source_selection, reorder_item
|
|
25
|
+
filter_transcriptions, toggle_source_selection, reorder_item, is_source_selected
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
# %% ../../nbs/routes/filtering.ipynb #c4457084
|
|
@@ -100,6 +101,19 @@ def _handle_selection_toggle_focused(
|
|
|
100
101
|
step_state = _get_step_state(state_store, workflow_id, session_id)
|
|
101
102
|
selected_sources = step_state.get("selected_sources", [])
|
|
102
103
|
|
|
104
|
+
# Only check for duplicate media_path when adding (not removing)
|
|
105
|
+
if not is_source_selected(record_id, provider_id, selected_sources):
|
|
106
|
+
conflict = _find_duplicate_media_source(source_service, record_id, provider_id, selected_sources)
|
|
107
|
+
if conflict:
|
|
108
|
+
response = _build_queue_response(
|
|
109
|
+
state_store, workflow_id, source_service, session_id, selected_sources, urls
|
|
110
|
+
)
|
|
111
|
+
flash = _render_duplicate_flash(
|
|
112
|
+
record_id, provider_id, conflict["record_id"], conflict["provider_id"]
|
|
113
|
+
)
|
|
114
|
+
parts = response if isinstance(response, tuple) else (response,)
|
|
115
|
+
return (*parts, flash)
|
|
116
|
+
|
|
103
117
|
selected_sources = toggle_source_selection(record_id, provider_id, selected_sources)
|
|
104
118
|
_update_step_state(state_store, workflow_id, session_id, selected_sources)
|
|
105
119
|
|
|
@@ -14,14 +14,15 @@ from cjm_fasthtml_interactions.core.state_store import get_session_id
|
|
|
14
14
|
|
|
15
15
|
from ..models import SelectionUrls
|
|
16
16
|
from cjm_transcript_source_select.routes.core import (
|
|
17
|
-
WorkflowStateStore, _get_step_state, _update_step_state, _build_queue_response
|
|
17
|
+
WorkflowStateStore, _get_step_state, _update_step_state, _build_queue_response,
|
|
18
|
+
_find_duplicate_media_source, _render_duplicate_flash
|
|
18
19
|
)
|
|
19
20
|
from cjm_transcript_source_select.components.preview_panel import (
|
|
20
21
|
_render_preview_panel
|
|
21
22
|
)
|
|
22
23
|
from ..services.source import SourceService
|
|
23
24
|
from cjm_transcript_source_select.services.source_utils import (
|
|
24
|
-
select_all_in_group, reorder_sources
|
|
25
|
+
select_all_in_group, reorder_sources, get_selected_media_paths
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
# %% ../../nbs/routes/queue.ipynb #a5934339
|
|
@@ -40,8 +41,26 @@ def _handle_selection_add(
|
|
|
40
41
|
step_state = _get_step_state(state_store, workflow_id, session_id)
|
|
41
42
|
selected_sources = step_state.get("selected_sources", [])
|
|
42
43
|
|
|
43
|
-
# Check if already selected
|
|
44
|
-
|
|
44
|
+
# Check if already selected by (record_id, provider_id) pair
|
|
45
|
+
already_selected = any(
|
|
46
|
+
s.get("record_id") == record_id and s.get("provider_id") == provider_id
|
|
47
|
+
for s in selected_sources
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if not already_selected:
|
|
51
|
+
# Check for duplicate audio source
|
|
52
|
+
conflict = _find_duplicate_media_source(source_service, record_id, provider_id, selected_sources)
|
|
53
|
+
if conflict:
|
|
54
|
+
# Reject with flash feedback on both rows
|
|
55
|
+
response = _build_queue_response(
|
|
56
|
+
state_store, workflow_id, source_service, session_id, selected_sources, urls
|
|
57
|
+
)
|
|
58
|
+
flash = _render_duplicate_flash(
|
|
59
|
+
record_id, provider_id, conflict["record_id"], conflict["provider_id"]
|
|
60
|
+
)
|
|
61
|
+
parts = response if isinstance(response, tuple) else (response,)
|
|
62
|
+
return (*parts, flash)
|
|
63
|
+
|
|
45
64
|
selected_sources.append({"record_id": record_id, "provider_id": provider_id})
|
|
46
65
|
_update_step_state(state_store, workflow_id, session_id, selected_sources)
|
|
47
66
|
|
|
@@ -55,6 +74,7 @@ def _handle_selection_remove(
|
|
|
55
74
|
request, # FastHTML request object
|
|
56
75
|
sess, # FastHTML session object
|
|
57
76
|
record_id: str, # Job ID to remove
|
|
77
|
+
provider_id: str, # Plugin name for the source
|
|
58
78
|
urls: SelectionUrls, # URL bundle for rendering
|
|
59
79
|
): # Queue component with OOB stats, optionally with OOB source list
|
|
60
80
|
"""Remove a source from the selection queue."""
|
|
@@ -62,8 +82,11 @@ def _handle_selection_remove(
|
|
|
62
82
|
step_state = _get_step_state(state_store, workflow_id, session_id)
|
|
63
83
|
selected_sources = step_state.get("selected_sources", [])
|
|
64
84
|
|
|
65
|
-
# Remove
|
|
66
|
-
selected_sources = [
|
|
85
|
+
# Remove by (record_id, provider_id) pair
|
|
86
|
+
selected_sources = [
|
|
87
|
+
s for s in selected_sources
|
|
88
|
+
if not (s.get("record_id") == record_id and s.get("provider_id") == provider_id)
|
|
89
|
+
]
|
|
67
90
|
_update_step_state(state_store, workflow_id, session_id, selected_sources)
|
|
68
91
|
|
|
69
92
|
return _build_queue_response(state_store, workflow_id, source_service, session_id, selected_sources, urls)
|
|
@@ -120,13 +143,17 @@ def _handle_selection_select_all(
|
|
|
120
143
|
grouping_mode: str, # Current grouping mode: "media_path" or "batch_id"
|
|
121
144
|
urls: SelectionUrls, # URL bundle for rendering
|
|
122
145
|
): # Queue component with OOB stats, optionally with OOB source list
|
|
123
|
-
"""Select all transcriptions for a given group."""
|
|
146
|
+
"""Select all transcriptions for a given group, skipping duplicate audio sources."""
|
|
124
147
|
session_id = get_session_id(sess)
|
|
125
148
|
step_state = _get_step_state(state_store, workflow_id, session_id)
|
|
126
149
|
selected_sources = step_state.get("selected_sources", [])
|
|
127
150
|
|
|
128
151
|
all_transcriptions = source_service.query_transcriptions(limit=500)
|
|
129
|
-
|
|
152
|
+
excluded = get_selected_media_paths(selected_sources, all_transcriptions)
|
|
153
|
+
selected_sources = select_all_in_group(
|
|
154
|
+
all_transcriptions, group_key, grouping_mode, selected_sources,
|
|
155
|
+
excluded_media_paths=excluded,
|
|
156
|
+
)
|
|
130
157
|
|
|
131
158
|
_update_step_state(state_store, workflow_id, session_id, selected_sources=selected_sources)
|
|
132
159
|
|
|
@@ -180,11 +207,11 @@ def init_queue_router(
|
|
|
180
207
|
)
|
|
181
208
|
|
|
182
209
|
@router
|
|
183
|
-
def remove(request, sess, record_id: str):
|
|
210
|
+
def remove(request, sess, record_id: str, provider_id: str):
|
|
184
211
|
"""Remove a source from the selection queue."""
|
|
185
212
|
return _handle_selection_remove(
|
|
186
213
|
state_store, workflow_id, source_service,
|
|
187
|
-
request, sess, record_id, urls=urls,
|
|
214
|
+
request, sess, record_id, provider_id, urls=urls,
|
|
188
215
|
)
|
|
189
216
|
|
|
190
217
|
@router
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
# %% auto #0
|
|
6
6
|
__all__ = ['extract_batch_id', 'extract_model_name', 'group_transcriptions', 'group_transcriptions_by_audio',
|
|
7
|
-
'is_source_selected', '
|
|
8
|
-
'reorder_item', 'reorder_sources', 'calculate_next_tab', 'check_audio_exists',
|
|
7
|
+
'is_source_selected', 'get_selected_media_paths', 'filter_transcriptions', 'select_all_in_group',
|
|
8
|
+
'toggle_source_selection', 'reorder_item', 'reorder_sources', 'calculate_next_tab', 'check_audio_exists',
|
|
9
|
+
'validate_browse_path']
|
|
9
10
|
|
|
10
11
|
# %% ../../nbs/services/source_utils.ipynb #su-imports
|
|
11
|
-
from typing import Any, List, Dict
|
|
12
|
+
from typing import Any, List, Dict, Optional, Set
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
import json
|
|
14
15
|
|
|
@@ -85,10 +86,27 @@ def group_transcriptions_by_audio(
|
|
|
85
86
|
# %% ../../nbs/services/source_utils.ipynb #su-is-source-selected
|
|
86
87
|
def is_source_selected(
|
|
87
88
|
record_id: str, # Job ID to check
|
|
89
|
+
provider_id: str, # Provider ID to check
|
|
88
90
|
selected_sources: List[Dict[str, str]] # List of selected sources
|
|
89
91
|
) -> bool: # True if source is selected
|
|
90
|
-
"""Check if a source is in the selected list."""
|
|
91
|
-
return any(
|
|
92
|
+
"""Check if a source is in the selected list by (record_id, provider_id) pair."""
|
|
93
|
+
return any(
|
|
94
|
+
s.get("record_id") == record_id and s.get("provider_id") == provider_id
|
|
95
|
+
for s in selected_sources
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# %% ../../nbs/services/source_utils.ipynb #yt3azuiiy3g
|
|
99
|
+
def get_selected_media_paths(
|
|
100
|
+
selected_sources: List[Dict[str, str]], # Current selections (record_id, provider_id)
|
|
101
|
+
all_transcriptions: List[Dict[str, Any]], # All available transcription records
|
|
102
|
+
) -> Set[str]: # Media paths already represented in selections
|
|
103
|
+
"""Get the set of media_paths for currently selected sources."""
|
|
104
|
+
selected_keys = {(s.get("record_id"), s.get("provider_id")) for s in selected_sources}
|
|
105
|
+
return {
|
|
106
|
+
t.get("media_path") for t in all_transcriptions
|
|
107
|
+
if (t.get("record_id"), t.get("provider_id")) in selected_keys
|
|
108
|
+
and t.get("media_path")
|
|
109
|
+
}
|
|
92
110
|
|
|
93
111
|
# %% ../../nbs/services/source_utils.ipynb #tg25xqgkaa
|
|
94
112
|
def filter_transcriptions(
|
|
@@ -113,6 +131,7 @@ def select_all_in_group(
|
|
|
113
131
|
group_key: str, # Group key to match against
|
|
114
132
|
grouping_mode: str, # Grouping mode: "media_path" or "batch_id"
|
|
115
133
|
selected_sources: List[Dict[str, str]], # Current selections
|
|
134
|
+
excluded_media_paths: Optional[Set[str]] = None, # Media paths to skip (already selected)
|
|
116
135
|
) -> List[Dict[str, str]]: # Updated selections with new items appended
|
|
117
136
|
"""Add all transcriptions matching a group key to the selection list, skipping duplicates."""
|
|
118
137
|
# Filter transcriptions by group key
|
|
@@ -121,14 +140,24 @@ def select_all_in_group(
|
|
|
121
140
|
else:
|
|
122
141
|
matching = [t for t in transcriptions if t.get("media_path") == group_key]
|
|
123
142
|
|
|
124
|
-
# Deduplicate against existing selections
|
|
125
|
-
|
|
143
|
+
# Deduplicate against existing selections using (record_id, provider_id) pairs
|
|
144
|
+
existing_keys = {(s.get("record_id"), s.get("provider_id")) for s in selected_sources}
|
|
145
|
+
used_paths = set(excluded_media_paths) if excluded_media_paths else set()
|
|
126
146
|
result = list(selected_sources)
|
|
127
147
|
for t in matching:
|
|
128
148
|
record_id = t.get("record_id")
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
149
|
+
provider_id = t.get("provider_id", "")
|
|
150
|
+
media_path = t.get("media_path")
|
|
151
|
+
key = (record_id, provider_id)
|
|
152
|
+
if not record_id or key in existing_keys:
|
|
153
|
+
continue
|
|
154
|
+
# Skip if media_path already represented
|
|
155
|
+
if excluded_media_paths is not None and media_path and media_path in used_paths:
|
|
156
|
+
continue
|
|
157
|
+
result.append({"record_id": record_id, "provider_id": provider_id})
|
|
158
|
+
existing_keys.add(key)
|
|
159
|
+
if media_path:
|
|
160
|
+
used_paths.add(media_path)
|
|
132
161
|
|
|
133
162
|
return result
|
|
134
163
|
|
|
@@ -138,9 +167,11 @@ def toggle_source_selection(
|
|
|
138
167
|
provider_id: str, # Plugin name for the source
|
|
139
168
|
selected_sources: List[Dict[str, str]], # Current selections
|
|
140
169
|
) -> List[Dict[str, str]]: # Updated selections
|
|
141
|
-
"""Toggle a source in or out of the selection list."""
|
|
142
|
-
if any(s.get("record_id") == record_id
|
|
143
|
-
|
|
170
|
+
"""Toggle a source in or out of the selection list by (record_id, provider_id) pair."""
|
|
171
|
+
if any(s.get("record_id") == record_id and s.get("provider_id") == provider_id
|
|
172
|
+
for s in selected_sources):
|
|
173
|
+
return [s for s in selected_sources
|
|
174
|
+
if not (s.get("record_id") == record_id and s.get("provider_id") == provider_id)]
|
|
144
175
|
else:
|
|
145
176
|
return selected_sources + [{"record_id": record_id, "provider_id": provider_id}]
|
|
146
177
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: cjm-transcript-source-select
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: FastHTML source selection component for transcript decomposition workflows, with federated database browsing, drag-drop ordering, and keyboard navigation.
|
|
5
5
|
Home-page: https://github.com/cj-mills/cjm-transcript-source-select
|
|
6
6
|
Author: Christian J. Mills
|
|
@@ -105,54 +105,55 @@ graph LR
|
|
|
105
105
|
components_local_files --> html_ids
|
|
106
106
|
components_preview_panel --> html_ids
|
|
107
107
|
components_selection_queue --> html_ids
|
|
108
|
-
components_source_browser --> services_source_utils
|
|
109
108
|
components_source_browser --> utils
|
|
109
|
+
components_source_browser --> services_source_utils
|
|
110
110
|
components_source_browser --> html_ids
|
|
111
|
-
components_step_renderer -->
|
|
111
|
+
components_step_renderer --> components_selection_queue
|
|
112
|
+
components_step_renderer --> components_preview_panel
|
|
113
|
+
components_step_renderer --> components_local_files
|
|
112
114
|
components_step_renderer --> components_source_browser
|
|
115
|
+
components_step_renderer --> components_helpers
|
|
113
116
|
components_step_renderer --> utils
|
|
114
|
-
components_step_renderer --> components_preview_panel
|
|
115
117
|
components_step_renderer --> html_ids
|
|
116
|
-
components_step_renderer --> components_local_files
|
|
117
|
-
components_step_renderer --> components_selection_queue
|
|
118
118
|
components_step_renderer --> models
|
|
119
119
|
routes_core --> components_step_renderer
|
|
120
|
+
routes_core --> services_source
|
|
120
121
|
routes_core --> models
|
|
122
|
+
routes_core --> html_ids
|
|
121
123
|
routes_core --> components_selection_queue
|
|
122
124
|
routes_core --> components_source_browser
|
|
123
|
-
|
|
124
|
-
routes_filtering --> routes_core
|
|
125
|
+
routes_filtering --> services_source
|
|
125
126
|
routes_filtering --> services_source_utils
|
|
126
|
-
routes_filtering -->
|
|
127
|
+
routes_filtering --> routes_core
|
|
127
128
|
routes_filtering --> components_source_browser
|
|
128
|
-
routes_filtering -->
|
|
129
|
-
routes_init --> routes_queue
|
|
130
|
-
routes_init --> routes_local_files
|
|
131
|
-
routes_init --> models
|
|
129
|
+
routes_filtering --> models
|
|
132
130
|
routes_init --> routes_filtering
|
|
133
|
-
routes_init -->
|
|
131
|
+
routes_init --> routes_local_files
|
|
134
132
|
routes_init --> services_source
|
|
133
|
+
routes_init --> routes_tabs
|
|
134
|
+
routes_init --> models
|
|
135
135
|
routes_init --> routes_core
|
|
136
|
-
|
|
137
|
-
routes_local_files --> routes_core
|
|
136
|
+
routes_init --> routes_queue
|
|
138
137
|
routes_local_files --> services_source
|
|
138
|
+
routes_local_files --> routes_core
|
|
139
139
|
routes_local_files --> models
|
|
140
|
+
routes_local_files --> components_local_files
|
|
140
141
|
routes_queue --> routes_core
|
|
141
|
-
routes_queue --> services_source_utils
|
|
142
|
-
routes_queue --> models
|
|
143
142
|
routes_queue --> components_preview_panel
|
|
144
143
|
routes_queue --> services_source
|
|
144
|
+
routes_queue --> services_source_utils
|
|
145
|
+
routes_queue --> models
|
|
145
146
|
routes_tabs --> routes_local_files
|
|
146
|
-
routes_tabs -->
|
|
147
|
+
routes_tabs --> components_source_browser
|
|
147
148
|
routes_tabs --> services_source_utils
|
|
149
|
+
routes_tabs --> services_source
|
|
148
150
|
routes_tabs --> routes_core
|
|
149
|
-
routes_tabs --> components_source_browser
|
|
150
151
|
routes_tabs --> models
|
|
152
|
+
routes_tabs --> components_local_files
|
|
151
153
|
routes_tabs --> components_step_renderer
|
|
152
|
-
routes_tabs --> services_source
|
|
153
154
|
```
|
|
154
155
|
|
|
155
|
-
*
|
|
156
|
+
*51 cross-module dependencies detected*
|
|
156
157
|
|
|
157
158
|
## CLI Reference
|
|
158
159
|
|
|
@@ -186,6 +187,26 @@ def _get_step_state(
|
|
|
186
187
|
"Get the selection step state from the workflow state store."
|
|
187
188
|
```
|
|
188
189
|
|
|
190
|
+
``` python
|
|
191
|
+
def _find_duplicate_media_source(
|
|
192
|
+
source_service: SourceService, # Source service for lookups
|
|
193
|
+
record_id: str, # Candidate record ID
|
|
194
|
+
provider_id: str, # Candidate provider ID
|
|
195
|
+
selected_sources: List[Dict[str, str]], # Current selections
|
|
196
|
+
) -> Optional[Dict[str, str]]: # Conflicting source dict or None
|
|
197
|
+
"Find an already-selected source that shares the same audio file."
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
``` python
|
|
201
|
+
def _render_duplicate_flash(
|
|
202
|
+
candidate_record_id: str, # Record ID of the row the user clicked
|
|
203
|
+
candidate_provider_id: str, # Provider ID of the row the user clicked
|
|
204
|
+
existing_record_id: str, # Record ID of the conflicting selected row
|
|
205
|
+
existing_provider_id: str, # Provider ID of the conflicting selected row
|
|
206
|
+
) -> Script: # OOB script element for flash animation
|
|
207
|
+
"Render a self-removing Script that briefly flashes two source rows with error color."
|
|
208
|
+
```
|
|
209
|
+
|
|
189
210
|
``` python
|
|
190
211
|
def _get_active_source_tab(
|
|
191
212
|
state_store: WorkflowStateStore, # The workflow state store
|
|
@@ -658,6 +679,7 @@ def _handle_selection_remove(
|
|
|
658
679
|
request, # FastHTML request object
|
|
659
680
|
sess, # FastHTML session object
|
|
660
681
|
record_id: str, # Job ID to remove
|
|
682
|
+
provider_id: str, # Plugin name for the source
|
|
661
683
|
urls: SelectionUrls, # URL bundle for rendering
|
|
662
684
|
): # Queue component with OOB stats, optionally with OOB source list
|
|
663
685
|
"Remove a source from the selection queue."
|
|
@@ -698,7 +720,7 @@ def _handle_selection_select_all(
|
|
|
698
720
|
grouping_mode: str, # Current grouping mode: "media_path" or "batch_id"
|
|
699
721
|
urls: SelectionUrls, # URL bundle for rendering
|
|
700
722
|
): # Queue component with OOB stats, optionally with OOB source list
|
|
701
|
-
"Select all transcriptions for a given group."
|
|
723
|
+
"Select all transcriptions for a given group, skipping duplicate audio sources."
|
|
702
724
|
```
|
|
703
725
|
|
|
704
726
|
``` python
|
|
@@ -1088,6 +1110,7 @@ from cjm_transcript_source_select.services.source_utils import (
|
|
|
1088
1110
|
group_transcriptions,
|
|
1089
1111
|
group_transcriptions_by_audio,
|
|
1090
1112
|
is_source_selected,
|
|
1113
|
+
get_selected_media_paths,
|
|
1091
1114
|
filter_transcriptions,
|
|
1092
1115
|
select_all_in_group,
|
|
1093
1116
|
toggle_source_selection,
|
|
@@ -1133,9 +1156,18 @@ def group_transcriptions_by_audio(
|
|
|
1133
1156
|
``` python
|
|
1134
1157
|
def is_source_selected(
|
|
1135
1158
|
record_id: str, # Job ID to check
|
|
1159
|
+
provider_id: str, # Provider ID to check
|
|
1136
1160
|
selected_sources: List[Dict[str, str]] # List of selected sources
|
|
1137
1161
|
) -> bool: # True if source is selected
|
|
1138
|
-
"Check if a source is in the selected list."
|
|
1162
|
+
"Check if a source is in the selected list by (record_id, provider_id) pair."
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
``` python
|
|
1166
|
+
def get_selected_media_paths(
|
|
1167
|
+
selected_sources: List[Dict[str, str]], # Current selections (record_id, provider_id)
|
|
1168
|
+
all_transcriptions: List[Dict[str, Any]], # All available transcription records
|
|
1169
|
+
) -> Set[str]: # Media paths already represented in selections
|
|
1170
|
+
"Get the set of media_paths for currently selected sources."
|
|
1139
1171
|
```
|
|
1140
1172
|
|
|
1141
1173
|
``` python
|
|
@@ -1152,6 +1184,7 @@ def select_all_in_group(
|
|
|
1152
1184
|
group_key: str, # Group key to match against
|
|
1153
1185
|
grouping_mode: str, # Grouping mode: "media_path" or "batch_id"
|
|
1154
1186
|
selected_sources: List[Dict[str, str]], # Current selections
|
|
1187
|
+
excluded_media_paths: Optional[Set[str]] = None, # Media paths to skip (already selected)
|
|
1155
1188
|
) -> List[Dict[str, str]]: # Updated selections with new items appended
|
|
1156
1189
|
"Add all transcriptions matching a group key to the selection list, skipping duplicates."
|
|
1157
1190
|
```
|
|
@@ -1162,7 +1195,7 @@ def toggle_source_selection(
|
|
|
1162
1195
|
provider_id: str, # Plugin name for the source
|
|
1163
1196
|
selected_sources: List[Dict[str, str]], # Current selections
|
|
1164
1197
|
) -> List[Dict[str, str]]: # Updated selections
|
|
1165
|
-
"Toggle a source in or out of the selection list."
|
|
1198
|
+
"Toggle a source in or out of the selection list by (record_id, provider_id) pair."
|
|
1166
1199
|
```
|
|
1167
1200
|
|
|
1168
1201
|
``` python
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
[DEFAULT]
|
|
2
|
-
# All sections below are required unless otherwise specified.
|
|
3
|
-
# See https://github.com/AnswerDotAI/nbdev/blob/main/settings.ini for examples.
|
|
4
|
-
|
|
5
|
-
### Python library ###
|
|
6
2
|
repo = cjm-transcript-source-select
|
|
7
|
-
lib_name =
|
|
8
|
-
version = 0.0.
|
|
3
|
+
lib_name = cjm-transcript-source-select
|
|
4
|
+
version = 0.0.3
|
|
9
5
|
min_python = 3.12
|
|
10
6
|
license = apache2
|
|
11
7
|
black_formatting = False
|
|
12
|
-
|
|
13
|
-
### nbdev ###
|
|
14
8
|
doc_path = _docs
|
|
15
9
|
lib_path = cjm_transcript_source_select
|
|
16
10
|
nbs_path = nbs
|
|
@@ -18,29 +12,27 @@ recursive = True
|
|
|
18
12
|
tst_flags = notest
|
|
19
13
|
put_version_in_init = True
|
|
20
14
|
update_pyproject = True
|
|
21
|
-
|
|
22
|
-
### Docs ###
|
|
23
15
|
branch = main
|
|
24
16
|
custom_sidebar = False
|
|
25
|
-
doc_host = https
|
|
26
|
-
doc_baseurl =
|
|
27
|
-
git_url = https://github.com
|
|
28
|
-
title =
|
|
29
|
-
|
|
30
|
-
### PyPI ###
|
|
17
|
+
doc_host = https://cj-mills.github.io
|
|
18
|
+
doc_baseurl = /cjm-transcript-source-select
|
|
19
|
+
git_url = https://github.com/cj-mills/cjm-transcript-source-select
|
|
20
|
+
title = cjm-transcript-source-select
|
|
31
21
|
audience = Developers
|
|
32
22
|
author = Christian J. Mills
|
|
33
23
|
author_email = 9126128+cj-mills@users.noreply.github.com
|
|
34
|
-
copyright = 2026 onwards,
|
|
24
|
+
copyright = 2026 onwards, Christian J. Mills
|
|
35
25
|
description = FastHTML source selection component for transcript decomposition workflows, with federated database browsing, drag-drop ordering, and keyboard navigation.
|
|
36
26
|
keywords = nbdev jupyter notebook python
|
|
37
27
|
language = English
|
|
38
28
|
status = 3
|
|
39
29
|
user = cj-mills
|
|
30
|
+
requirements = cjm-plugin-system cjm-transcription-plugin-system cjm-fasthtml-app-core cjm-fasthtml-daisyui cjm_fasthtml_lucide_icons cjm_fasthtml_file_browser cjm_fasthtml_keyboard_navigation duckdb pandas cjm_workflow_state cjm_source_provider cjm_fasthtml_interactions
|
|
31
|
+
readme_nb = index.ipynb
|
|
32
|
+
allowed_metadata_keys =
|
|
33
|
+
allowed_cell_metadata_keys =
|
|
34
|
+
jupyter_hooks = False
|
|
35
|
+
clean_ids = True
|
|
36
|
+
clear_all = False
|
|
37
|
+
skip_procs =
|
|
40
38
|
|
|
41
|
-
### Optional ###
|
|
42
|
-
requirements = cjm-plugin-system cjm-transcription-plugin-system cjm-fasthtml-app-core cjm-fasthtml-daisyui cjm_fasthtml_lucide_icons cjm_fasthtml_file_browser cjm_fasthtml_keyboard_navigation duckdb pandas cjm_workflow_state cjm_source_provider cjm_fasthtml_interactions
|
|
43
|
-
# dev_requirements =
|
|
44
|
-
# console_scripts =
|
|
45
|
-
# conda_user =
|
|
46
|
-
# package_data =
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|