karaoke-gen 0.90.1__py3-none-any.whl → 0.99.3__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 (197) 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 +835 -0
  11. backend/api/routes/audio_search.py +913 -0
  12. backend/api/routes/auth.py +348 -0
  13. backend/api/routes/file_upload.py +2112 -0
  14. backend/api/routes/health.py +409 -0
  15. backend/api/routes/internal.py +435 -0
  16. backend/api/routes/jobs.py +1629 -0
  17. backend/api/routes/review.py +652 -0
  18. backend/api/routes/themes.py +162 -0
  19. backend/api/routes/users.py +1513 -0
  20. backend/config.py +172 -0
  21. backend/main.py +157 -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 +502 -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 +853 -0
  54. backend/services/job_notification_service.py +271 -0
  55. backend/services/langfuse_preloader.py +98 -0
  56. backend/services/local_encoding_service.py +590 -0
  57. backend/services/local_preview_encoding_service.py +407 -0
  58. backend/services/lyrics_cache_service.py +216 -0
  59. backend/services/metrics.py +413 -0
  60. backend/services/nltk_preloader.py +122 -0
  61. backend/services/packaging_service.py +287 -0
  62. backend/services/rclone_service.py +106 -0
  63. backend/services/spacy_preloader.py +65 -0
  64. backend/services/storage_service.py +209 -0
  65. backend/services/stripe_service.py +371 -0
  66. backend/services/structured_logging.py +254 -0
  67. backend/services/template_service.py +330 -0
  68. backend/services/theme_service.py +469 -0
  69. backend/services/tracing.py +543 -0
  70. backend/services/user_service.py +721 -0
  71. backend/services/worker_service.py +558 -0
  72. backend/services/youtube_service.py +112 -0
  73. backend/services/youtube_upload_service.py +445 -0
  74. backend/tests/__init__.py +4 -0
  75. backend/tests/conftest.py +224 -0
  76. backend/tests/emulator/__init__.py +7 -0
  77. backend/tests/emulator/conftest.py +109 -0
  78. backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
  79. backend/tests/emulator/test_emulator_integration.py +356 -0
  80. backend/tests/emulator/test_style_loading_direct.py +436 -0
  81. backend/tests/emulator/test_worker_logs_direct.py +229 -0
  82. backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
  83. backend/tests/requirements-test.txt +10 -0
  84. backend/tests/requirements.txt +6 -0
  85. backend/tests/test_admin_email_endpoints.py +411 -0
  86. backend/tests/test_api_integration.py +460 -0
  87. backend/tests/test_api_routes.py +93 -0
  88. backend/tests/test_audio_analysis_service.py +294 -0
  89. backend/tests/test_audio_editing_service.py +386 -0
  90. backend/tests/test_audio_search.py +1398 -0
  91. backend/tests/test_audio_services.py +378 -0
  92. backend/tests/test_auth_firestore.py +231 -0
  93. backend/tests/test_config_extended.py +68 -0
  94. backend/tests/test_credential_manager.py +377 -0
  95. backend/tests/test_dependencies.py +54 -0
  96. backend/tests/test_discord_service.py +244 -0
  97. backend/tests/test_distribution_services.py +820 -0
  98. backend/tests/test_dropbox_service.py +472 -0
  99. backend/tests/test_email_service.py +492 -0
  100. backend/tests/test_emulator_integration.py +322 -0
  101. backend/tests/test_encoding_interface.py +412 -0
  102. backend/tests/test_file_upload.py +1739 -0
  103. backend/tests/test_flacfetch_client.py +632 -0
  104. backend/tests/test_gdrive_service.py +524 -0
  105. backend/tests/test_instrumental_api.py +431 -0
  106. backend/tests/test_internal_api.py +343 -0
  107. backend/tests/test_job_creation_regression.py +583 -0
  108. backend/tests/test_job_manager.py +356 -0
  109. backend/tests/test_job_manager_notifications.py +329 -0
  110. backend/tests/test_job_notification_service.py +443 -0
  111. backend/tests/test_jobs_api.py +283 -0
  112. backend/tests/test_local_encoding_service.py +423 -0
  113. backend/tests/test_local_preview_encoding_service.py +567 -0
  114. backend/tests/test_main.py +87 -0
  115. backend/tests/test_models.py +918 -0
  116. backend/tests/test_packaging_service.py +382 -0
  117. backend/tests/test_requests.py +201 -0
  118. backend/tests/test_routes_jobs.py +282 -0
  119. backend/tests/test_routes_review.py +337 -0
  120. backend/tests/test_services.py +556 -0
  121. backend/tests/test_services_extended.py +112 -0
  122. backend/tests/test_spacy_preloader.py +119 -0
  123. backend/tests/test_storage_service.py +448 -0
  124. backend/tests/test_style_upload.py +261 -0
  125. backend/tests/test_template_service.py +295 -0
  126. backend/tests/test_theme_service.py +516 -0
  127. backend/tests/test_unicode_sanitization.py +522 -0
  128. backend/tests/test_upload_api.py +256 -0
  129. backend/tests/test_validate.py +156 -0
  130. backend/tests/test_video_worker_orchestrator.py +847 -0
  131. backend/tests/test_worker_log_subcollection.py +509 -0
  132. backend/tests/test_worker_logging.py +365 -0
  133. backend/tests/test_workers.py +1116 -0
  134. backend/tests/test_workers_extended.py +178 -0
  135. backend/tests/test_youtube_service.py +247 -0
  136. backend/tests/test_youtube_upload_service.py +568 -0
  137. backend/utils/test_data.py +27 -0
  138. backend/validate.py +173 -0
  139. backend/version.py +27 -0
  140. backend/workers/README.md +597 -0
  141. backend/workers/__init__.py +11 -0
  142. backend/workers/audio_worker.py +618 -0
  143. backend/workers/lyrics_worker.py +683 -0
  144. backend/workers/render_video_worker.py +483 -0
  145. backend/workers/screens_worker.py +535 -0
  146. backend/workers/style_helper.py +198 -0
  147. backend/workers/video_worker.py +1277 -0
  148. backend/workers/video_worker_orchestrator.py +701 -0
  149. backend/workers/worker_logging.py +278 -0
  150. karaoke_gen/instrumental_review/static/index.html +7 -4
  151. karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
  152. karaoke_gen/utils/__init__.py +163 -8
  153. karaoke_gen/video_background_processor.py +9 -4
  154. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/METADATA +1 -1
  155. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/RECORD +196 -46
  156. lyrics_transcriber/correction/agentic/agent.py +17 -6
  157. lyrics_transcriber/correction/agentic/providers/config.py +9 -5
  158. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +96 -93
  159. lyrics_transcriber/correction/agentic/providers/model_factory.py +27 -6
  160. lyrics_transcriber/correction/anchor_sequence.py +151 -37
  161. lyrics_transcriber/correction/corrector.py +192 -130
  162. lyrics_transcriber/correction/handlers/syllables_match.py +44 -2
  163. lyrics_transcriber/correction/operations.py +24 -9
  164. lyrics_transcriber/correction/phrase_analyzer.py +18 -0
  165. lyrics_transcriber/frontend/package-lock.json +2 -2
  166. lyrics_transcriber/frontend/package.json +1 -1
  167. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
  168. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
  169. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
  170. lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
  171. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
  172. lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
  173. lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
  174. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
  175. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
  176. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
  177. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
  178. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
  179. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
  180. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
  181. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
  182. lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
  183. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
  184. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
  185. lyrics_transcriber/frontend/src/theme.ts +42 -15
  186. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  187. lyrics_transcriber/frontend/vite.config.js +5 -0
  188. lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
  189. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
  190. lyrics_transcriber/frontend/web_assets/index.html +6 -2
  191. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
  192. lyrics_transcriber/output/generator.py +17 -3
  193. lyrics_transcriber/output/video.py +60 -95
  194. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
  195. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/WHEEL +0 -0
  196. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/entry_points.txt +0 -0
  197. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,283 @@
1
+ """
2
+ Unit tests for jobs.py API routes using FastAPI TestClient.
3
+
4
+ These tests mock the underlying services and test the route logic directly.
5
+ """
6
+ import pytest
7
+ from datetime import datetime, UTC
8
+ from unittest.mock import MagicMock, AsyncMock, patch
9
+ from fastapi.testclient import TestClient
10
+
11
+ from backend.models.job import Job, JobStatus
12
+
13
+
14
+ @pytest.fixture
15
+ def mock_job():
16
+ """Create a standard mock job for testing."""
17
+ return Job(
18
+ job_id="test123",
19
+ status=JobStatus.PENDING,
20
+ created_at=datetime.now(UTC),
21
+ updated_at=datetime.now(UTC),
22
+ artist="Test Artist",
23
+ title="Test Song"
24
+ )
25
+
26
+
27
+ @pytest.fixture
28
+ def mock_job_manager(mock_job):
29
+ """Create a mock JobManager with common methods."""
30
+ manager = MagicMock()
31
+ manager.get_job.return_value = mock_job
32
+ manager.list_jobs.return_value = [mock_job]
33
+ manager.create_job.return_value = mock_job
34
+ manager.delete_job.return_value = True
35
+ manager.cancel_job.return_value = mock_job
36
+ manager.update_job_status.return_value = None
37
+ manager.update_state_data.return_value = None
38
+ return manager
39
+
40
+
41
+ @pytest.fixture
42
+ def mock_worker_service():
43
+ """Create a mock WorkerService."""
44
+ service = MagicMock()
45
+ service.trigger_audio_worker = AsyncMock(return_value=True)
46
+ service.trigger_lyrics_worker = AsyncMock(return_value=True)
47
+ service.trigger_screens_worker = AsyncMock(return_value=True)
48
+ service.trigger_video_worker = AsyncMock(return_value=True)
49
+ return service
50
+
51
+
52
+ @pytest.fixture
53
+ def mock_theme_service():
54
+ """Create a mock ThemeService that returns 'nomad' as default theme."""
55
+ service = MagicMock()
56
+ service.get_default_theme_id.return_value = "nomad"
57
+ service.get_theme.return_value = None
58
+ return service
59
+
60
+
61
+ @pytest.fixture
62
+ def client(mock_job_manager, mock_worker_service, mock_theme_service):
63
+ """Create TestClient with mocked dependencies."""
64
+ mock_creds = MagicMock()
65
+ mock_creds.universe_domain = 'googleapis.com'
66
+
67
+ # Create a JobManager class that returns our mock instance
68
+ def mock_job_manager_factory(*args, **kwargs):
69
+ return mock_job_manager
70
+
71
+ # Patch at the module level where jobs.py imports them
72
+ # Also patch JobManager class used in dependencies.py for auth checks
73
+ with patch('backend.api.routes.jobs.job_manager', mock_job_manager), \
74
+ patch('backend.api.routes.jobs.worker_service', mock_worker_service), \
75
+ patch('backend.api.routes.jobs.get_theme_service', return_value=mock_theme_service), \
76
+ patch('backend.services.job_manager.JobManager', mock_job_manager_factory), \
77
+ patch('backend.services.firestore_service.firestore'), \
78
+ patch('backend.services.storage_service.storage'), \
79
+ patch('google.auth.default', return_value=(mock_creds, 'test-project')):
80
+ from backend.main import app
81
+ yield TestClient(app)
82
+
83
+
84
+ class TestGetJob:
85
+ """Tests for GET /api/jobs/{job_id}."""
86
+
87
+ def test_get_job_returns_200(self, client, mock_job_manager, mock_job, auth_headers):
88
+ """Test getting an existing job returns 200."""
89
+ response = client.get("/api/jobs/test123", headers=auth_headers)
90
+ assert response.status_code == 200
91
+
92
+ def test_get_job_returns_job_data(self, client, mock_job_manager, mock_job, auth_headers):
93
+ """Test response contains job data."""
94
+ response = client.get("/api/jobs/test123", headers=auth_headers)
95
+ data = response.json()
96
+ assert data["job_id"] == "test123"
97
+ assert data["status"] == "pending"
98
+ assert data["artist"] == "Test Artist"
99
+
100
+ def test_get_nonexistent_job_returns_404(self, mock_worker_service, auth_headers):
101
+ """Test getting non-existent job returns 404."""
102
+ mock_job_manager = MagicMock()
103
+ mock_job_manager.get_job.return_value = None
104
+ mock_creds = MagicMock()
105
+ mock_creds.universe_domain = 'googleapis.com'
106
+
107
+ def mock_job_manager_factory(*args, **kwargs):
108
+ return mock_job_manager
109
+
110
+ with patch('backend.api.routes.jobs.job_manager', mock_job_manager), \
111
+ patch('backend.api.routes.jobs.worker_service', mock_worker_service), \
112
+ patch('backend.services.job_manager.JobManager', mock_job_manager_factory), \
113
+ patch('backend.services.firestore_service.firestore'), \
114
+ patch('backend.services.storage_service.storage'), \
115
+ patch('google.auth.default', return_value=(mock_creds, 'test-project')):
116
+ from backend.main import app
117
+ client = TestClient(app)
118
+ response = client.get("/api/jobs/nonexistent", headers=auth_headers)
119
+ assert response.status_code == 404
120
+
121
+
122
+ class TestListJobs:
123
+ """Tests for GET /api/jobs."""
124
+
125
+ def test_list_jobs_returns_200(self, client, auth_headers):
126
+ """Test listing jobs returns 200."""
127
+ response = client.get("/api/jobs", headers=auth_headers)
128
+ assert response.status_code == 200
129
+
130
+ def test_list_jobs_returns_array(self, client, mock_job_manager, auth_headers):
131
+ """Test response is an array of jobs."""
132
+ response = client.get("/api/jobs", headers=auth_headers)
133
+ data = response.json()
134
+ assert isinstance(data, list)
135
+ assert len(data) == 1
136
+ assert data[0]["job_id"] == "test123"
137
+
138
+ def test_list_jobs_with_status_filter(self, client, mock_job_manager, auth_headers):
139
+ """Test listing jobs with status filter."""
140
+ response = client.get("/api/jobs?status=pending", headers=auth_headers)
141
+ assert response.status_code == 200
142
+
143
+ def test_list_jobs_with_limit(self, client, mock_job_manager, auth_headers):
144
+ """Test listing jobs with limit."""
145
+ response = client.get("/api/jobs?limit=10", headers=auth_headers)
146
+ assert response.status_code == 200
147
+
148
+
149
+ class TestCreateJob:
150
+ """Tests for POST /api/jobs."""
151
+
152
+ def test_create_job_with_url_returns_200(self, client, mock_job_manager, auth_headers):
153
+ """Test creating job with URL returns 200."""
154
+ response = client.post(
155
+ "/api/jobs",
156
+ json={"url": "https://youtube.com/watch?v=test123"},
157
+ headers=auth_headers
158
+ )
159
+ assert response.status_code == 200
160
+
161
+ def test_create_job_returns_job_id(self, client, mock_job_manager, auth_headers):
162
+ """Test create response contains job_id."""
163
+ response = client.post(
164
+ "/api/jobs",
165
+ json={"url": "https://youtube.com/watch?v=test"},
166
+ headers=auth_headers
167
+ )
168
+ data = response.json()
169
+ assert "job_id" in data
170
+
171
+ def test_create_job_with_artist_title(self, client, mock_job_manager, auth_headers):
172
+ """Test creating job with artist and title."""
173
+ response = client.post(
174
+ "/api/jobs",
175
+ json={
176
+ "url": "https://youtube.com/watch?v=test",
177
+ "artist": "Test Artist",
178
+ "title": "Test Song"
179
+ },
180
+ headers=auth_headers
181
+ )
182
+ assert response.status_code == 200
183
+
184
+
185
+ class TestDeleteJob:
186
+ """Tests for DELETE /api/jobs/{job_id}."""
187
+
188
+ def test_delete_job_returns_200(self, client, mock_job_manager, auth_headers):
189
+ """Test deleting job returns 200."""
190
+ response = client.delete("/api/jobs/test123", headers=auth_headers)
191
+ assert response.status_code == 200
192
+
193
+ def test_delete_nonexistent_job(self, mock_worker_service, auth_headers):
194
+ """Test deleting non-existent job."""
195
+ mock_job_manager = MagicMock()
196
+ mock_job_manager.get_job.return_value = None # Job doesn't exist
197
+ mock_job_manager.delete_job.return_value = False
198
+ mock_creds = MagicMock()
199
+ mock_creds.universe_domain = 'googleapis.com'
200
+
201
+ def mock_job_manager_factory(*args, **kwargs):
202
+ return mock_job_manager
203
+
204
+ with patch('backend.api.routes.jobs.job_manager', mock_job_manager), \
205
+ patch('backend.api.routes.jobs.worker_service', mock_worker_service), \
206
+ patch('backend.services.job_manager.JobManager', mock_job_manager_factory), \
207
+ patch('backend.services.firestore_service.firestore'), \
208
+ patch('backend.services.storage_service.storage'), \
209
+ patch('google.auth.default', return_value=(mock_creds, 'test-project')):
210
+ from backend.main import app
211
+ client = TestClient(app)
212
+ response = client.delete("/api/jobs/nonexistent", headers=auth_headers)
213
+ # Either 200 or 404 depending on implementation
214
+ assert response.status_code in [200, 404]
215
+
216
+
217
+ class TestCancelJob:
218
+ """Tests for POST /api/jobs/{job_id}/cancel."""
219
+
220
+ def test_cancel_job_returns_200(self, client, mock_job_manager, mock_job, auth_headers):
221
+ """Test cancelling job returns 200."""
222
+ mock_job_manager.cancel_job.return_value = mock_job
223
+ response = client.post(
224
+ "/api/jobs/test123/cancel",
225
+ json={},
226
+ headers=auth_headers
227
+ )
228
+ assert response.status_code == 200
229
+
230
+
231
+ class TestSubmitCorrections:
232
+ """Tests for POST /api/jobs/{job_id}/corrections."""
233
+
234
+ def test_submit_corrections_returns_200(self, client, mock_job_manager, mock_job, auth_headers):
235
+ """Test submitting corrections returns 200."""
236
+ # Job needs to be in AWAITING_REVIEW or IN_REVIEW status
237
+ review_job = Job(
238
+ job_id="test123",
239
+ status=JobStatus.IN_REVIEW,
240
+ created_at=datetime.now(UTC),
241
+ updated_at=datetime.now(UTC),
242
+ artist="Test",
243
+ title="Test"
244
+ )
245
+ mock_job_manager.get_job.return_value = review_job
246
+
247
+ response = client.post(
248
+ "/api/jobs/test123/corrections",
249
+ json={
250
+ "corrections": {
251
+ "lines": [],
252
+ "metadata": {"source": "test"}
253
+ }
254
+ },
255
+ headers=auth_headers
256
+ )
257
+ # Should be 200 or validation error
258
+ assert response.status_code in [200, 400, 422]
259
+
260
+
261
+ class TestSelectInstrumental:
262
+ """Tests for POST /api/jobs/{job_id}/select-instrumental."""
263
+
264
+ def test_select_instrumental_requires_selection_field(self, client, mock_job_manager, mock_job, auth_headers):
265
+ """Test instrumental selection requires selection field."""
266
+ response = client.post(
267
+ "/api/jobs/test123/select-instrumental",
268
+ json={}, # Missing selection field
269
+ headers=auth_headers
270
+ )
271
+ # Missing field should cause validation error
272
+ assert response.status_code == 422
273
+
274
+
275
+ class TestStartReview:
276
+ """Tests for POST /api/jobs/{job_id}/start-review."""
277
+
278
+ def test_start_review_endpoint_exists(self, client, mock_job_manager, mock_job, auth_headers):
279
+ """Test start review endpoint exists."""
280
+ response = client.post("/api/jobs/test123/start-review", headers=auth_headers)
281
+ # Should not be 404 or 405
282
+ assert response.status_code not in [404, 405]
283
+