karaoke-gen 0.86.7__py3-none-any.whl → 0.96.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. backend/.coveragerc +20 -0
  2. backend/.gitignore +37 -0
  3. backend/Dockerfile +43 -0
  4. backend/Dockerfile.base +74 -0
  5. backend/README.md +242 -0
  6. backend/__init__.py +0 -0
  7. backend/api/__init__.py +0 -0
  8. backend/api/dependencies.py +457 -0
  9. backend/api/routes/__init__.py +0 -0
  10. backend/api/routes/admin.py +742 -0
  11. backend/api/routes/audio_search.py +903 -0
  12. backend/api/routes/auth.py +348 -0
  13. backend/api/routes/file_upload.py +2076 -0
  14. backend/api/routes/health.py +344 -0
  15. backend/api/routes/internal.py +435 -0
  16. backend/api/routes/jobs.py +1610 -0
  17. backend/api/routes/review.py +652 -0
  18. backend/api/routes/themes.py +162 -0
  19. backend/api/routes/users.py +1014 -0
  20. backend/config.py +172 -0
  21. backend/main.py +133 -0
  22. backend/middleware/__init__.py +5 -0
  23. backend/middleware/audit_logging.py +124 -0
  24. backend/models/__init__.py +0 -0
  25. backend/models/job.py +519 -0
  26. backend/models/requests.py +123 -0
  27. backend/models/theme.py +153 -0
  28. backend/models/user.py +254 -0
  29. backend/models/worker_log.py +164 -0
  30. backend/pyproject.toml +29 -0
  31. backend/quick-check.sh +93 -0
  32. backend/requirements.txt +29 -0
  33. backend/run_tests.sh +60 -0
  34. backend/services/__init__.py +0 -0
  35. backend/services/audio_analysis_service.py +243 -0
  36. backend/services/audio_editing_service.py +278 -0
  37. backend/services/audio_search_service.py +702 -0
  38. backend/services/auth_service.py +630 -0
  39. backend/services/credential_manager.py +792 -0
  40. backend/services/discord_service.py +172 -0
  41. backend/services/dropbox_service.py +301 -0
  42. backend/services/email_service.py +1093 -0
  43. backend/services/encoding_interface.py +454 -0
  44. backend/services/encoding_service.py +405 -0
  45. backend/services/firestore_service.py +512 -0
  46. backend/services/flacfetch_client.py +573 -0
  47. backend/services/gce_encoding/README.md +72 -0
  48. backend/services/gce_encoding/__init__.py +22 -0
  49. backend/services/gce_encoding/main.py +589 -0
  50. backend/services/gce_encoding/requirements.txt +16 -0
  51. backend/services/gdrive_service.py +356 -0
  52. backend/services/job_logging.py +258 -0
  53. backend/services/job_manager.py +842 -0
  54. backend/services/job_notification_service.py +271 -0
  55. backend/services/local_encoding_service.py +590 -0
  56. backend/services/local_preview_encoding_service.py +407 -0
  57. backend/services/lyrics_cache_service.py +216 -0
  58. backend/services/metrics.py +413 -0
  59. backend/services/packaging_service.py +287 -0
  60. backend/services/rclone_service.py +106 -0
  61. backend/services/storage_service.py +209 -0
  62. backend/services/stripe_service.py +275 -0
  63. backend/services/structured_logging.py +254 -0
  64. backend/services/template_service.py +330 -0
  65. backend/services/theme_service.py +469 -0
  66. backend/services/tracing.py +543 -0
  67. backend/services/user_service.py +721 -0
  68. backend/services/worker_service.py +558 -0
  69. backend/services/youtube_service.py +112 -0
  70. backend/services/youtube_upload_service.py +445 -0
  71. backend/tests/__init__.py +4 -0
  72. backend/tests/conftest.py +224 -0
  73. backend/tests/emulator/__init__.py +7 -0
  74. backend/tests/emulator/conftest.py +88 -0
  75. backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
  76. backend/tests/emulator/test_emulator_integration.py +356 -0
  77. backend/tests/emulator/test_style_loading_direct.py +436 -0
  78. backend/tests/emulator/test_worker_logs_direct.py +229 -0
  79. backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
  80. backend/tests/requirements-test.txt +10 -0
  81. backend/tests/requirements.txt +6 -0
  82. backend/tests/test_admin_email_endpoints.py +411 -0
  83. backend/tests/test_api_integration.py +460 -0
  84. backend/tests/test_api_routes.py +93 -0
  85. backend/tests/test_audio_analysis_service.py +294 -0
  86. backend/tests/test_audio_editing_service.py +386 -0
  87. backend/tests/test_audio_search.py +1398 -0
  88. backend/tests/test_audio_services.py +378 -0
  89. backend/tests/test_auth_firestore.py +231 -0
  90. backend/tests/test_config_extended.py +68 -0
  91. backend/tests/test_credential_manager.py +377 -0
  92. backend/tests/test_dependencies.py +54 -0
  93. backend/tests/test_discord_service.py +244 -0
  94. backend/tests/test_distribution_services.py +820 -0
  95. backend/tests/test_dropbox_service.py +472 -0
  96. backend/tests/test_email_service.py +492 -0
  97. backend/tests/test_emulator_integration.py +322 -0
  98. backend/tests/test_encoding_interface.py +412 -0
  99. backend/tests/test_file_upload.py +1739 -0
  100. backend/tests/test_flacfetch_client.py +632 -0
  101. backend/tests/test_gdrive_service.py +524 -0
  102. backend/tests/test_instrumental_api.py +431 -0
  103. backend/tests/test_internal_api.py +343 -0
  104. backend/tests/test_job_creation_regression.py +583 -0
  105. backend/tests/test_job_manager.py +339 -0
  106. backend/tests/test_job_manager_notifications.py +329 -0
  107. backend/tests/test_job_notification_service.py +443 -0
  108. backend/tests/test_jobs_api.py +273 -0
  109. backend/tests/test_local_encoding_service.py +423 -0
  110. backend/tests/test_local_preview_encoding_service.py +567 -0
  111. backend/tests/test_main.py +87 -0
  112. backend/tests/test_models.py +918 -0
  113. backend/tests/test_packaging_service.py +382 -0
  114. backend/tests/test_requests.py +201 -0
  115. backend/tests/test_routes_jobs.py +282 -0
  116. backend/tests/test_routes_review.py +337 -0
  117. backend/tests/test_services.py +556 -0
  118. backend/tests/test_services_extended.py +112 -0
  119. backend/tests/test_storage_service.py +448 -0
  120. backend/tests/test_style_upload.py +261 -0
  121. backend/tests/test_template_service.py +295 -0
  122. backend/tests/test_theme_service.py +516 -0
  123. backend/tests/test_unicode_sanitization.py +522 -0
  124. backend/tests/test_upload_api.py +256 -0
  125. backend/tests/test_validate.py +156 -0
  126. backend/tests/test_video_worker_orchestrator.py +847 -0
  127. backend/tests/test_worker_log_subcollection.py +509 -0
  128. backend/tests/test_worker_logging.py +365 -0
  129. backend/tests/test_workers.py +1116 -0
  130. backend/tests/test_workers_extended.py +178 -0
  131. backend/tests/test_youtube_service.py +247 -0
  132. backend/tests/test_youtube_upload_service.py +568 -0
  133. backend/validate.py +173 -0
  134. backend/version.py +27 -0
  135. backend/workers/README.md +597 -0
  136. backend/workers/__init__.py +11 -0
  137. backend/workers/audio_worker.py +618 -0
  138. backend/workers/lyrics_worker.py +683 -0
  139. backend/workers/render_video_worker.py +483 -0
  140. backend/workers/screens_worker.py +525 -0
  141. backend/workers/style_helper.py +198 -0
  142. backend/workers/video_worker.py +1277 -0
  143. backend/workers/video_worker_orchestrator.py +701 -0
  144. backend/workers/worker_logging.py +278 -0
  145. karaoke_gen/instrumental_review/static/index.html +7 -4
  146. karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
  147. karaoke_gen/style_loader.py +3 -1
  148. karaoke_gen/utils/__init__.py +163 -8
  149. karaoke_gen/video_background_processor.py +9 -4
  150. {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/METADATA +2 -1
  151. {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/RECORD +187 -42
  152. lyrics_transcriber/correction/agentic/providers/config.py +9 -5
  153. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +1 -51
  154. lyrics_transcriber/correction/corrector.py +192 -130
  155. lyrics_transcriber/correction/operations.py +24 -9
  156. lyrics_transcriber/frontend/package-lock.json +2 -2
  157. lyrics_transcriber/frontend/package.json +1 -1
  158. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
  159. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
  160. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
  161. lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
  162. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
  163. lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
  164. lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
  165. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
  166. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
  167. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
  168. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
  169. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
  170. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
  171. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
  172. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
  173. lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
  174. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
  175. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
  176. lyrics_transcriber/frontend/src/theme.ts +42 -15
  177. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  178. lyrics_transcriber/frontend/vite.config.js +5 -0
  179. lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
  180. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
  181. lyrics_transcriber/frontend/web_assets/index.html +6 -2
  182. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
  183. lyrics_transcriber/output/generator.py +17 -3
  184. lyrics_transcriber/output/video.py +60 -95
  185. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
  186. {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/WHEEL +0 -0
  187. {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/entry_points.txt +0 -0
  188. {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,365 @@
1
+ """
2
+ Unit tests for backend/workers/worker_logging.py.
3
+
4
+ Tests the job logging utilities including JobLogger, JobLogHandler,
5
+ and job_logging_context for concurrent job isolation.
6
+ """
7
+ import pytest
8
+ import logging
9
+ from unittest.mock import MagicMock, patch
10
+
11
+
12
+ class TestJobLoggingContext:
13
+ """Tests for job_logging_context context manager."""
14
+
15
+ def test_job_logging_context_sets_and_resets_job_id(self):
16
+ """Test that job_logging_context sets and resets the current job ID."""
17
+ from backend.workers.worker_logging import job_logging_context, _current_job_id
18
+
19
+ # Initially no job
20
+ assert _current_job_id.get() is None
21
+
22
+ # Inside context, job is set
23
+ with job_logging_context("job123"):
24
+ assert _current_job_id.get() == "job123"
25
+
26
+ # After context, job is reset
27
+ assert _current_job_id.get() is None
28
+
29
+ def test_job_logging_context_nested(self):
30
+ """Test nested job_logging_context calls."""
31
+ from backend.workers.worker_logging import job_logging_context, _current_job_id
32
+
33
+ with job_logging_context("outer_job"):
34
+ assert _current_job_id.get() == "outer_job"
35
+
36
+ with job_logging_context("inner_job"):
37
+ assert _current_job_id.get() == "inner_job"
38
+
39
+ # After inner context, outer job is restored
40
+ assert _current_job_id.get() == "outer_job"
41
+
42
+ assert _current_job_id.get() is None
43
+
44
+ def test_job_logging_context_handles_exception(self):
45
+ """Test that job_logging_context resets even on exception."""
46
+ from backend.workers.worker_logging import job_logging_context, _current_job_id
47
+
48
+ try:
49
+ with job_logging_context("job123"):
50
+ assert _current_job_id.get() == "job123"
51
+ raise ValueError("Test exception")
52
+ except ValueError:
53
+ pass
54
+
55
+ # Job should still be reset after exception
56
+ assert _current_job_id.get() is None
57
+
58
+
59
+ class TestJobLogHandler:
60
+ """Tests for JobLogHandler class."""
61
+
62
+ @pytest.fixture
63
+ def mock_job_manager(self):
64
+ """Create a mock JobManager."""
65
+ manager = MagicMock()
66
+ manager.append_worker_log.return_value = None
67
+ return manager
68
+
69
+ def test_job_log_handler_init(self, mock_job_manager):
70
+ """Test JobLogHandler initialization."""
71
+ from backend.workers.worker_logging import JobLogHandler
72
+
73
+ handler = JobLogHandler(
74
+ job_id="job123",
75
+ worker_name="audio",
76
+ job_manager=mock_job_manager
77
+ )
78
+
79
+ assert handler.job_id == "job123"
80
+ assert handler.worker_name == "audio"
81
+ assert handler.job_manager == mock_job_manager
82
+ assert handler.level == logging.INFO
83
+
84
+ def test_job_log_handler_custom_level(self, mock_job_manager):
85
+ """Test JobLogHandler with custom level."""
86
+ from backend.workers.worker_logging import JobLogHandler
87
+
88
+ handler = JobLogHandler(
89
+ job_id="job123",
90
+ worker_name="audio",
91
+ job_manager=mock_job_manager,
92
+ level=logging.DEBUG
93
+ )
94
+
95
+ assert handler.level == logging.DEBUG
96
+
97
+ def test_job_log_handler_emit_logs_to_firestore(self, mock_job_manager):
98
+ """Test that emit() calls job_manager.append_worker_log."""
99
+ from backend.workers.worker_logging import JobLogHandler, job_logging_context
100
+
101
+ handler = JobLogHandler(
102
+ job_id="job123",
103
+ worker_name="audio",
104
+ job_manager=mock_job_manager
105
+ )
106
+ handler.setFormatter(logging.Formatter('%(message)s'))
107
+
108
+ # Create a log record
109
+ record = logging.LogRecord(
110
+ name="test",
111
+ level=logging.INFO,
112
+ pathname="",
113
+ lineno=0,
114
+ msg="Test message",
115
+ args=(),
116
+ exc_info=None
117
+ )
118
+
119
+ # Emit within the job context
120
+ with job_logging_context("job123"):
121
+ handler.emit(record)
122
+
123
+ # Should have called append_worker_log
124
+ mock_job_manager.append_worker_log.assert_called()
125
+
126
+ def test_job_log_handler_filters_other_job_context(self, mock_job_manager):
127
+ """Test that handler filters logs from other job contexts."""
128
+ from backend.workers.worker_logging import JobLogHandler, job_logging_context
129
+
130
+ handler = JobLogHandler(
131
+ job_id="job123",
132
+ worker_name="audio",
133
+ job_manager=mock_job_manager
134
+ )
135
+
136
+ record = logging.LogRecord(
137
+ name="test",
138
+ level=logging.INFO,
139
+ pathname="",
140
+ lineno=0,
141
+ msg="Test message",
142
+ args=(),
143
+ exc_info=None
144
+ )
145
+
146
+ # Emit from a different job's context
147
+ with job_logging_context("different_job"):
148
+ handler.emit(record)
149
+
150
+ # Should NOT have called append_worker_log
151
+ mock_job_manager.append_worker_log.assert_not_called()
152
+
153
+ def test_job_log_handler_deduplication(self, mock_job_manager):
154
+ """Test that handler deduplicates repeated messages."""
155
+ from backend.workers.worker_logging import JobLogHandler, job_logging_context
156
+
157
+ handler = JobLogHandler(
158
+ job_id="job123",
159
+ worker_name="audio",
160
+ job_manager=mock_job_manager
161
+ )
162
+ handler.setFormatter(logging.Formatter('%(message)s'))
163
+
164
+ record = logging.LogRecord(
165
+ name="test",
166
+ level=logging.INFO,
167
+ pathname="",
168
+ lineno=0,
169
+ msg="Duplicate message",
170
+ args=(),
171
+ exc_info=None
172
+ )
173
+
174
+ with job_logging_context("job123"):
175
+ # Emit the same record twice
176
+ handler.emit(record)
177
+ handler.emit(record)
178
+
179
+ # Should only be called once due to deduplication
180
+ assert mock_job_manager.append_worker_log.call_count == 1
181
+
182
+
183
+ class TestJobLogger:
184
+ """Tests for JobLogger class."""
185
+
186
+ @pytest.fixture
187
+ def mock_job_manager(self):
188
+ """Create a mock JobManager."""
189
+ manager = MagicMock()
190
+ manager.append_worker_log.return_value = None
191
+ return manager
192
+
193
+ def test_job_logger_init(self, mock_job_manager):
194
+ """Test JobLogger initialization."""
195
+ from backend.workers.worker_logging import JobLogger
196
+
197
+ logger = JobLogger(
198
+ job_id="job123",
199
+ worker_name="lyrics",
200
+ job_manager=mock_job_manager
201
+ )
202
+
203
+ assert logger.job_id == "job123"
204
+ assert logger.worker_name == "lyrics"
205
+ assert logger.job_manager == mock_job_manager
206
+
207
+ def test_job_logger_info(self, mock_job_manager):
208
+ """Test JobLogger.info() method."""
209
+ from backend.workers.worker_logging import JobLogger
210
+
211
+ logger = JobLogger(
212
+ job_id="job123",
213
+ worker_name="lyrics",
214
+ job_manager=mock_job_manager
215
+ )
216
+
217
+ logger.info("Processing started")
218
+
219
+ mock_job_manager.append_worker_log.assert_called_with(
220
+ job_id="job123",
221
+ worker="lyrics",
222
+ level="INFO",
223
+ message="Processing started"
224
+ )
225
+
226
+ def test_job_logger_warning(self, mock_job_manager):
227
+ """Test JobLogger.warning() method."""
228
+ from backend.workers.worker_logging import JobLogger
229
+
230
+ logger = JobLogger(
231
+ job_id="job123",
232
+ worker_name="audio",
233
+ job_manager=mock_job_manager
234
+ )
235
+
236
+ logger.warning("Low memory")
237
+
238
+ mock_job_manager.append_worker_log.assert_called_with(
239
+ job_id="job123",
240
+ worker="audio",
241
+ level="WARNING",
242
+ message="Low memory"
243
+ )
244
+
245
+ def test_job_logger_error(self, mock_job_manager):
246
+ """Test JobLogger.error() method."""
247
+ from backend.workers.worker_logging import JobLogger
248
+
249
+ logger = JobLogger(
250
+ job_id="job123",
251
+ worker_name="video",
252
+ job_manager=mock_job_manager
253
+ )
254
+
255
+ logger.error("Processing failed")
256
+
257
+ mock_job_manager.append_worker_log.assert_called_with(
258
+ job_id="job123",
259
+ worker="video",
260
+ level="ERROR",
261
+ message="Processing failed"
262
+ )
263
+
264
+ def test_job_logger_debug(self, mock_job_manager):
265
+ """Test JobLogger.debug() method."""
266
+ from backend.workers.worker_logging import JobLogger
267
+
268
+ logger = JobLogger(
269
+ job_id="job123",
270
+ worker_name="screens",
271
+ job_manager=mock_job_manager
272
+ )
273
+
274
+ logger.debug("Debug info")
275
+
276
+ mock_job_manager.append_worker_log.assert_called_with(
277
+ job_id="job123",
278
+ worker="screens",
279
+ level="DEBUG",
280
+ message="Debug info"
281
+ )
282
+
283
+ def test_job_logger_with_format_args(self, mock_job_manager):
284
+ """Test JobLogger with format arguments."""
285
+ from backend.workers.worker_logging import JobLogger
286
+
287
+ logger = JobLogger(
288
+ job_id="job123",
289
+ worker_name="audio",
290
+ job_manager=mock_job_manager
291
+ )
292
+
293
+ logger.info("Processing %s of %d", "audio", 10)
294
+
295
+ mock_job_manager.append_worker_log.assert_called_with(
296
+ job_id="job123",
297
+ worker="audio",
298
+ level="INFO",
299
+ message="Processing audio of 10"
300
+ )
301
+
302
+ def test_job_logger_handles_firestore_error(self, mock_job_manager):
303
+ """Test that JobLogger handles Firestore errors gracefully."""
304
+ from backend.workers.worker_logging import JobLogger
305
+
306
+ mock_job_manager.append_worker_log.side_effect = Exception("Firestore error")
307
+
308
+ logger = JobLogger(
309
+ job_id="job123",
310
+ worker_name="audio",
311
+ job_manager=mock_job_manager
312
+ )
313
+
314
+ # Should not raise exception
315
+ logger.info("Test message")
316
+
317
+
318
+ class TestCreateJobLogger:
319
+ """Tests for create_job_logger function."""
320
+
321
+ def test_create_job_logger(self):
322
+ """Test create_job_logger creates a JobLogger."""
323
+ from backend.workers.worker_logging import create_job_logger, JobLogger
324
+
325
+ # Patch at the source module where JobManager is imported
326
+ with patch('backend.services.job_manager.JobManager'):
327
+ logger = create_job_logger("job123", "audio")
328
+
329
+ assert isinstance(logger, JobLogger)
330
+ assert logger.job_id == "job123"
331
+ assert logger.worker_name == "audio"
332
+
333
+
334
+ class TestSetupJobLogging:
335
+ """Tests for setup_job_logging function."""
336
+
337
+ def test_setup_job_logging_returns_handler(self):
338
+ """Test setup_job_logging returns a JobLogHandler."""
339
+ from backend.workers.worker_logging import setup_job_logging, JobLogHandler
340
+
341
+ # Patch at the source module
342
+ with patch('backend.services.job_manager.JobManager'):
343
+ handler = setup_job_logging("job123", "lyrics", "test_logger_wl1")
344
+
345
+ assert isinstance(handler, JobLogHandler)
346
+ assert handler.job_id == "job123"
347
+ assert handler.worker_name == "lyrics"
348
+
349
+ # Clean up - remove the handler
350
+ logging.getLogger("test_logger_wl1").removeHandler(handler)
351
+
352
+ def test_setup_job_logging_adds_handler_to_loggers(self):
353
+ """Test setup_job_logging adds handler to specified loggers."""
354
+ from backend.workers.worker_logging import setup_job_logging
355
+
356
+ with patch('backend.services.job_manager.JobManager'):
357
+ handler = setup_job_logging("job123", "audio", "test_logger_wl2", "test_logger_wl3")
358
+
359
+ # Check handlers are added
360
+ assert handler in logging.getLogger("test_logger_wl2").handlers
361
+ assert handler in logging.getLogger("test_logger_wl3").handlers
362
+
363
+ # Clean up
364
+ logging.getLogger("test_logger_wl2").removeHandler(handler)
365
+ logging.getLogger("test_logger_wl3").removeHandler(handler)