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.
Files changed (38) hide show
  1. {cjm_transcript_source_select-0.0.1/cjm_transcript_source_select.egg-info → cjm_transcript_source_select-0.0.3}/PKG-INFO +58 -25
  2. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/README.md +57 -24
  3. cjm_transcript_source_select-0.0.3/cjm_transcript_source_select/__init__.py +1 -0
  4. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/_modidx.py +6 -0
  5. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/components/selection_queue.py +1 -1
  6. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/components/source_browser.py +8 -3
  7. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/routes/core.py +46 -1
  8. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/routes/filtering.py +16 -2
  9. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/routes/queue.py +37 -10
  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
  11. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3/cjm_transcript_source_select.egg-info}/PKG-INFO +58 -25
  12. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/settings.ini +15 -23
  13. cjm_transcript_source_select-0.0.1/cjm_transcript_source_select/__init__.py +0 -1
  14. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/LICENSE +0 -0
  15. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/MANIFEST.in +0 -0
  16. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/components/__init__.py +0 -0
  17. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/components/helpers.py +0 -0
  18. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/components/local_files.py +0 -0
  19. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/components/preview_panel.py +0 -0
  20. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/components/step_renderer.py +0 -0
  21. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/html_ids.py +0 -0
  22. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/models.py +0 -0
  23. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/routes/__init__.py +0 -0
  24. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/routes/init.py +0 -0
  25. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/routes/local_files.py +0 -0
  26. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/routes/tabs.py +0 -0
  27. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/services/__init__.py +0 -0
  28. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/services/source.py +0 -0
  29. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select/utils.py +0 -0
  30. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select.egg-info/SOURCES.txt +0 -0
  31. {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
  32. {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
  33. {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
  34. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/cjm_transcript_source_select.egg-info/requires.txt +0 -0
  35. {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
  36. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/pyproject.toml +0 -0
  37. {cjm_transcript_source_select-0.0.1 → cjm_transcript_source_select-0.0.3}/setup.cfg +0 -0
  38. {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.1
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 --> components_helpers
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
- routes_core --> services_source
124
- routes_filtering --> routes_core
125
+ routes_filtering --> services_source
125
126
  routes_filtering --> services_source_utils
126
- routes_filtering --> models
127
+ routes_filtering --> routes_core
127
128
  routes_filtering --> components_source_browser
128
- routes_filtering --> services_source
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 --> routes_tabs
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
- routes_local_files --> components_local_files
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 --> components_local_files
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
- *50 cross-module dependencies detected*
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 --> components_helpers
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
- routes_core --> services_source
81
- routes_filtering --> routes_core
82
+ routes_filtering --> services_source
82
83
  routes_filtering --> services_source_utils
83
- routes_filtering --> models
84
+ routes_filtering --> routes_core
84
85
  routes_filtering --> components_source_browser
85
- routes_filtering --> services_source
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 --> routes_tabs
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
- routes_local_files --> components_local_files
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 --> components_local_files
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
- *50 cross-module dependencies detected*
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(record.get("record_id", ""), selected_sources)
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
- if not any(s.get("record_id") == record_id for s in selected_sources):
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 the item
66
- selected_sources = [s for s in selected_sources if s.get("record_id") != record_id]
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
- selected_sources = select_all_in_group(all_transcriptions, group_key, grouping_mode, selected_sources)
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', 'filter_transcriptions', 'select_all_in_group', 'toggle_source_selection',
8
- 'reorder_item', 'reorder_sources', 'calculate_next_tab', 'check_audio_exists', 'validate_browse_path']
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(s.get("record_id") == record_id for s in selected_sources)
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
- existing_record_ids = {s.get("record_id") for s in selected_sources}
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
- if record_id and record_id not in existing_record_ids:
130
- result.append({"record_id": record_id, "provider_id": t.get("provider_id", "")})
131
- existing_record_ids.add(record_id)
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 for s in selected_sources):
143
- return [s for s in selected_sources if s.get("record_id") != record_id]
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.1
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 --> components_helpers
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
- routes_core --> services_source
124
- routes_filtering --> routes_core
125
+ routes_filtering --> services_source
125
126
  routes_filtering --> services_source_utils
126
- routes_filtering --> models
127
+ routes_filtering --> routes_core
127
128
  routes_filtering --> components_source_browser
128
- routes_filtering --> services_source
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 --> routes_tabs
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
- routes_local_files --> components_local_files
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 --> components_local_files
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
- *50 cross-module dependencies detected*
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 = %(repo)s
8
- version = 0.0.1
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://%(user)s.github.io
26
- doc_baseurl = /%(repo)s
27
- git_url = https://github.com/%(user)s/%(repo)s
28
- title = %(lib_name)s
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, %(author)s
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"