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,322 @@
1
+ """
2
+ Integration tests using local GCP emulators.
3
+
4
+ These tests use real Firestore and GCS emulators running locally,
5
+ providing true integration testing without cloud resources or costs.
6
+
7
+ Run with: ./scripts/run-emulator-tests.sh
8
+
9
+ NOTE: These are simplified integration tests that focus on the most critical
10
+ paths. They mock background workers to avoid race conditions and network issues.
11
+ """
12
+ import pytest
13
+ import os
14
+ import time
15
+ from unittest.mock import AsyncMock, patch
16
+ from datetime import datetime, UTC
17
+ import requests
18
+
19
+
20
+ def emulators_running() -> bool:
21
+ """Check if GCP emulators are running."""
22
+ try:
23
+ requests.get("http://127.0.0.1:8080", timeout=1)
24
+ requests.get("http://127.0.0.1:4443", timeout=1)
25
+ return True
26
+ except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
27
+ return False
28
+
29
+
30
+ # Skip all tests in this module if emulators aren't running
31
+ pytestmark = pytest.mark.skipif(
32
+ not emulators_running(),
33
+ reason="GCP emulators not running. Start with: scripts/start-emulators.sh"
34
+ )
35
+
36
+ # Set emulator environment variables before any imports (use 127.0.0.1 for IPv4)
37
+ os.environ["FIRESTORE_EMULATOR_HOST"] = "127.0.0.1:8080"
38
+ os.environ["STORAGE_EMULATOR_HOST"] = "http://127.0.0.1:4443"
39
+ os.environ["GOOGLE_CLOUD_PROJECT"] = "test-project"
40
+ os.environ["GCS_BUCKET_NAME"] = "test-bucket"
41
+ os.environ["FIRESTORE_COLLECTION"] = "test-jobs" # Use separate collection for tests
42
+ os.environ["ENVIRONMENT"] = "test"
43
+ os.environ["ADMIN_TOKENS"] = "test-admin-token"
44
+
45
+ # Only import if emulators are running
46
+ if emulators_running():
47
+ from fastapi.testclient import TestClient
48
+ from backend.main import app
49
+ from backend.models.job import JobStatus
50
+ else:
51
+ TestClient = None
52
+ app = None
53
+ JobStatus = None
54
+
55
+
56
+ @pytest.fixture(scope="module", autouse=True)
57
+ def setup_gcs_bucket():
58
+ """Create GCS bucket in emulator before tests."""
59
+ try:
60
+ response = requests.post(
61
+ "http://localhost:4443/storage/v1/b",
62
+ json={"name": "test-bucket"},
63
+ params={"project": "test-project"}
64
+ )
65
+ if response.status_code in [200, 409]:
66
+ print(f"✅ GCS bucket 'test-bucket' ready")
67
+ except Exception as e:
68
+ print(f"⚠️ GCS bucket setup failed: {e}")
69
+ yield
70
+
71
+
72
+ @pytest.fixture(scope="module")
73
+ def mock_worker_service():
74
+ """Mock the worker service to prevent background tasks."""
75
+ with patch("backend.api.routes.jobs.worker_service") as mock:
76
+ mock.trigger_audio_worker = AsyncMock(return_value=True)
77
+ mock.trigger_lyrics_worker = AsyncMock(return_value=True)
78
+ mock.trigger_screens_worker = AsyncMock(return_value=True)
79
+ mock.trigger_video_worker = AsyncMock(return_value=True)
80
+ yield mock
81
+
82
+
83
+ @pytest.fixture(scope="module")
84
+ def client(mock_worker_service):
85
+ """Create FastAPI test client with mocked workers."""
86
+ with patch("backend.api.routes.file_upload.worker_service", mock_worker_service):
87
+ return TestClient(app)
88
+
89
+
90
+ @pytest.fixture
91
+ def auth_headers():
92
+ """Auth headers for testing."""
93
+ return {"Authorization": "Bearer test-admin-token"}
94
+
95
+
96
+ class TestEmulatorBasics:
97
+ """Basic emulator connectivity tests."""
98
+
99
+ def test_health_endpoint(self, client, auth_headers):
100
+ """Test health endpoint works."""
101
+ response = client.get("/api/health", )
102
+ assert response.status_code == 200
103
+ assert response.json()["status"] == "healthy"
104
+
105
+ def test_root_endpoint(self, client, auth_headers):
106
+ """Test root endpoint works."""
107
+ response = client.get("/", )
108
+ assert response.status_code == 200
109
+ assert response.json()["service"] == "karaoke-gen-backend"
110
+
111
+
112
+ class TestJobCreation:
113
+ """Test job creation with Firestore emulator."""
114
+
115
+ def test_create_job_simple(self, client, auth_headers):
116
+ """Test creating a simple job."""
117
+ response = client.post(
118
+ "/api/jobs",
119
+ headers=auth_headers,
120
+ json={"url": "https://youtube.com/watch?v=test123"}
121
+ )
122
+
123
+ assert response.status_code == 200
124
+ data = response.json()
125
+ assert data["status"] == "success"
126
+ assert "job_id" in data
127
+
128
+ def test_create_job_with_metadata(self, client, auth_headers):
129
+ """Test creating a job with artist/title."""
130
+ response = client.post(
131
+ "/api/jobs",
132
+ headers=auth_headers,
133
+ json={
134
+ "url": "https://youtube.com/watch?v=test",
135
+ "artist": "Test Artist",
136
+ "title": "Test Song"
137
+ }
138
+ )
139
+
140
+ assert response.status_code == 200
141
+ data = response.json()
142
+ assert "job_id" in data
143
+
144
+
145
+ class TestJobRetrieval:
146
+ """Test job retrieval from Firestore emulator."""
147
+
148
+ def test_create_and_get_job(self, client, auth_headers):
149
+ """Test creating and then retrieving a job."""
150
+ # Create
151
+ create_resp = client.post(
152
+ "/api/jobs",
153
+ headers=auth_headers,
154
+ json={
155
+ "url": "https://youtube.com/watch?v=abc123",
156
+ "artist": "Test Artist",
157
+ "title": "Test Song"
158
+ }
159
+ )
160
+ assert create_resp.status_code == 200
161
+ job_id = create_resp.json()["job_id"]
162
+
163
+ # Small delay for emulator consistency
164
+ time.sleep(0.2)
165
+
166
+ # Retrieve
167
+ get_resp = client.get(f"/api/jobs/{job_id}", headers=auth_headers)
168
+
169
+ if get_resp.status_code != 200:
170
+ print(f"GET failed: {get_resp.status_code} - {get_resp.text}")
171
+
172
+ assert get_resp.status_code == 200
173
+
174
+ job = get_resp.json()
175
+ assert job["job_id"] == job_id
176
+ assert job["status"] == "pending"
177
+ assert job["artist"] == "Test Artist"
178
+ assert job["title"] == "Test Song"
179
+
180
+ def test_get_nonexistent_job(self, client, auth_headers):
181
+ """Test fetching a job that doesn't exist."""
182
+ response = client.get("/api/jobs/nonexistent-id", headers=auth_headers)
183
+ assert response.status_code == 404
184
+
185
+
186
+ class TestJobList:
187
+ """Test listing jobs from Firestore."""
188
+
189
+ def test_list_jobs(self, client, auth_headers):
190
+ """Test listing jobs."""
191
+ # Create a few jobs
192
+ for i in range(3):
193
+ client.post(
194
+ "/api/jobs",
195
+ headers=auth_headers,
196
+ json={"url": f"https://youtube.com/watch?v=list{i}"}
197
+ )
198
+
199
+ time.sleep(0.2)
200
+
201
+ # List
202
+ response = client.get("/api/jobs", headers=auth_headers)
203
+ assert response.status_code == 200
204
+
205
+ jobs = response.json()
206
+ assert isinstance(jobs, list)
207
+ # We should have at least the 3 we just created
208
+ assert len(jobs) >= 3
209
+
210
+
211
+ class TestJobDeletion:
212
+ """Test job deletion."""
213
+
214
+ def test_delete_job(self, client, auth_headers):
215
+ """Test deleting a job."""
216
+ # Create
217
+ create_resp = client.post(
218
+ "/api/jobs",
219
+ headers=auth_headers,
220
+ json={"url": "https://youtube.com/watch?v=delete-me"}
221
+ )
222
+ job_id = create_resp.json()["job_id"]
223
+
224
+ time.sleep(0.2)
225
+
226
+ # Delete
227
+ del_resp = client.delete(f"/api/jobs/{job_id}", headers=auth_headers)
228
+ assert del_resp.status_code == 200
229
+
230
+ # Verify deleted
231
+ get_resp = client.get(f"/api/jobs/{job_id}", headers=auth_headers)
232
+ assert get_resp.status_code == 404
233
+
234
+
235
+ class TestJobUpdates:
236
+ """Test job status updates."""
237
+
238
+ def test_cancel_job(self, client, auth_headers):
239
+ """Test cancelling a job."""
240
+ # Create
241
+ create_resp = client.post(
242
+ "/api/jobs",
243
+ headers=auth_headers,
244
+ json={"url": "https://youtube.com/watch?v=cancel-me"}
245
+ )
246
+ job_id = create_resp.json()["job_id"]
247
+
248
+ time.sleep(0.2)
249
+
250
+ # Cancel
251
+ cancel_resp = client.post(
252
+ f"/api/jobs/{job_id}/cancel",
253
+ headers=auth_headers,
254
+ json={"reason": "test cancellation"}
255
+ )
256
+ assert cancel_resp.status_code == 200
257
+
258
+ # Verify cancelled
259
+ get_resp = client.get(f"/api/jobs/{job_id}", headers=auth_headers)
260
+ assert get_resp.status_code == 200
261
+ job = get_resp.json()
262
+ assert job["status"] == "cancelled"
263
+
264
+
265
+ class TestFileUpload:
266
+ """Test file upload with GCS emulator."""
267
+
268
+ def test_upload_file(self, client, auth_headers):
269
+ """Test uploading a file."""
270
+ test_file_content = b"fake audio data for testing"
271
+
272
+ response = client.post(
273
+ "/api/jobs/upload",
274
+ headers={"Authorization": auth_headers["Authorization"]},
275
+ files={"file": ("test.flac", test_file_content, "audio/flac")},
276
+ data={"artist": "Upload Artist", "title": "Upload Song"}
277
+ )
278
+
279
+ assert response.status_code == 200
280
+ data = response.json()
281
+ assert data["status"] == "success"
282
+ assert "job_id" in data
283
+
284
+ job_id = data["job_id"]
285
+
286
+ time.sleep(0.2)
287
+
288
+ # Verify job created with upload data
289
+ get_resp = client.get(f"/api/jobs/{job_id}", headers=auth_headers)
290
+ assert get_resp.status_code == 200
291
+ job = get_resp.json()
292
+ assert job["artist"] == "Upload Artist"
293
+ assert job["title"] == "Upload Song"
294
+ assert "input_media_gcs_path" in job
295
+
296
+
297
+ class TestInternalEndpoints:
298
+ """Test internal worker endpoints."""
299
+
300
+ def test_internal_workers_exist(self, client, auth_headers):
301
+ """Test that internal worker endpoints exist and respond."""
302
+ # Create a job first
303
+ create_resp = client.post(
304
+ "/api/jobs",
305
+ headers=auth_headers,
306
+ json={"url": "https://youtube.com/watch?v=worker-test"}
307
+ )
308
+ job_id = create_resp.json()["job_id"]
309
+
310
+ time.sleep(0.2)
311
+
312
+ # Test audio worker endpoint exists
313
+ response = client.post(
314
+ "/api/internal/workers/audio",
315
+ headers=auth_headers,
316
+ json={"job_id": job_id}
317
+ )
318
+ assert response.status_code == 200
319
+ assert response.json()["status"] == "started"
320
+
321
+
322
+ print("✅ Emulator integration tests ready to run")