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,356 @@
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 tests mock background workers to avoid race conditions.
10
+ The fixtures are defined in conftest.py in this directory.
11
+ """
12
+ import pytest
13
+ import time
14
+
15
+ from .conftest import emulators_running
16
+
17
+
18
+ # Skip all tests in this module if emulators aren't running
19
+ pytestmark = pytest.mark.skipif(
20
+ not emulators_running(),
21
+ reason="GCP emulators not running. Start with: scripts/start-emulators.sh"
22
+ )
23
+
24
+ # Fixtures are loaded from conftest.py in this directory
25
+
26
+
27
+ class TestEmulatorBasics:
28
+ """Basic emulator connectivity tests."""
29
+
30
+ def test_health_endpoint(self, client, auth_headers):
31
+ """Test health endpoint works."""
32
+ response = client.get("/api/health", )
33
+ assert response.status_code == 200
34
+ assert response.json()["status"] == "healthy"
35
+
36
+ def test_root_endpoint(self, client, auth_headers):
37
+ """Test root endpoint works."""
38
+ response = client.get("/", )
39
+ assert response.status_code == 200
40
+ assert response.json()["service"] == "karaoke-gen-backend"
41
+
42
+
43
+ class TestJobCreation:
44
+ """Test job creation with Firestore emulator."""
45
+
46
+ def test_create_job_simple(self, client, auth_headers):
47
+ """Test creating a simple job."""
48
+ response = client.post(
49
+ "/api/jobs",
50
+ headers=auth_headers,
51
+ json={"url": "https://youtube.com/watch?v=test123"}
52
+ )
53
+
54
+ assert response.status_code == 200
55
+ data = response.json()
56
+ assert data["status"] == "success"
57
+ assert "job_id" in data
58
+
59
+ def test_create_job_with_metadata(self, client, auth_headers):
60
+ """Test creating a job with artist/title."""
61
+ response = client.post(
62
+ "/api/jobs",
63
+ headers=auth_headers,
64
+ json={
65
+ "url": "https://youtube.com/watch?v=test",
66
+ "artist": "Test Artist",
67
+ "title": "Test Song"
68
+ }
69
+ )
70
+
71
+ assert response.status_code == 200
72
+ data = response.json()
73
+ assert "job_id" in data
74
+
75
+
76
+ class TestJobRetrieval:
77
+ """Test job retrieval from Firestore emulator."""
78
+
79
+ def test_create_and_get_job(self, client, auth_headers):
80
+ """Test creating and then retrieving a job."""
81
+ # Create
82
+ create_resp = client.post(
83
+ "/api/jobs",
84
+ headers=auth_headers,
85
+ json={
86
+ "url": "https://youtube.com/watch?v=abc123",
87
+ "artist": "Test Artist",
88
+ "title": "Test Song"
89
+ }
90
+ )
91
+ assert create_resp.status_code == 200
92
+ job_id = create_resp.json()["job_id"]
93
+
94
+ # Small delay for emulator consistency
95
+ time.sleep(0.2)
96
+
97
+ # Retrieve
98
+ get_resp = client.get(f"/api/jobs/{job_id}", headers=auth_headers)
99
+
100
+ if get_resp.status_code != 200:
101
+ print(f"GET failed: {get_resp.status_code} - {get_resp.text}")
102
+
103
+ assert get_resp.status_code == 200
104
+
105
+ job = get_resp.json()
106
+ assert job["job_id"] == job_id
107
+ assert job["status"] == "pending"
108
+ assert job["artist"] == "Test Artist"
109
+ assert job["title"] == "Test Song"
110
+
111
+ def test_get_nonexistent_job(self, client, auth_headers):
112
+ """Test fetching a job that doesn't exist."""
113
+ response = client.get("/api/jobs/nonexistent-id", headers=auth_headers)
114
+ assert response.status_code == 404
115
+
116
+
117
+ class TestJobList:
118
+ """Test listing jobs from Firestore."""
119
+
120
+ def test_list_jobs(self, client, auth_headers):
121
+ """Test listing jobs."""
122
+ # Create a few jobs
123
+ for i in range(3):
124
+ client.post(
125
+ "/api/jobs",
126
+ headers=auth_headers,
127
+ json={"url": f"https://youtube.com/watch?v=list{i}"}
128
+ )
129
+
130
+ time.sleep(0.2)
131
+
132
+ # List
133
+ response = client.get("/api/jobs", headers=auth_headers)
134
+ assert response.status_code == 200
135
+
136
+ jobs = response.json()
137
+ assert isinstance(jobs, list)
138
+ # We should have at least the 3 we just created
139
+ assert len(jobs) >= 3
140
+
141
+
142
+ class TestJobDeletion:
143
+ """Test job deletion."""
144
+
145
+ def test_delete_job(self, client, auth_headers):
146
+ """Test deleting a job."""
147
+ # Create
148
+ create_resp = client.post(
149
+ "/api/jobs",
150
+ headers=auth_headers,
151
+ json={"url": "https://youtube.com/watch?v=delete-me"}
152
+ )
153
+ job_id = create_resp.json()["job_id"]
154
+
155
+ time.sleep(0.2)
156
+
157
+ # Delete
158
+ del_resp = client.delete(f"/api/jobs/{job_id}", headers=auth_headers)
159
+ assert del_resp.status_code == 200
160
+
161
+ # Verify deleted
162
+ get_resp = client.get(f"/api/jobs/{job_id}", headers=auth_headers)
163
+ assert get_resp.status_code == 404
164
+
165
+
166
+ class TestJobUpdates:
167
+ """Test job status updates."""
168
+
169
+ def test_cancel_job(self, client, auth_headers):
170
+ """Test cancelling a job."""
171
+ # Create
172
+ create_resp = client.post(
173
+ "/api/jobs",
174
+ headers=auth_headers,
175
+ json={"url": "https://youtube.com/watch?v=cancel-me"}
176
+ )
177
+ job_id = create_resp.json()["job_id"]
178
+
179
+ time.sleep(0.2)
180
+
181
+ # Cancel
182
+ cancel_resp = client.post(
183
+ f"/api/jobs/{job_id}/cancel",
184
+ headers=auth_headers,
185
+ json={"reason": "test cancellation"}
186
+ )
187
+ assert cancel_resp.status_code == 200
188
+
189
+ # Verify cancelled
190
+ get_resp = client.get(f"/api/jobs/{job_id}", headers=auth_headers)
191
+ assert get_resp.status_code == 200
192
+ job = get_resp.json()
193
+ assert job["status"] == "cancelled"
194
+
195
+
196
+ class TestFileUpload:
197
+ """Test file upload with GCS emulator."""
198
+
199
+ def test_upload_file(self, client, auth_headers):
200
+ """Test uploading a file."""
201
+ test_file_content = b"fake audio data for testing"
202
+
203
+ response = client.post(
204
+ "/api/jobs/upload",
205
+ headers={"Authorization": auth_headers["Authorization"]},
206
+ files={"file": ("test.flac", test_file_content, "audio/flac")},
207
+ data={"artist": "Upload Artist", "title": "Upload Song"}
208
+ )
209
+
210
+ assert response.status_code == 200
211
+ data = response.json()
212
+ assert data["status"] == "success"
213
+ assert "job_id" in data
214
+
215
+ job_id = data["job_id"]
216
+
217
+ time.sleep(0.2)
218
+
219
+ # Verify job created with upload data
220
+ get_resp = client.get(f"/api/jobs/{job_id}", headers=auth_headers)
221
+ assert get_resp.status_code == 200
222
+ job = get_resp.json()
223
+ assert job["artist"] == "Upload Artist"
224
+ assert job["title"] == "Upload Song"
225
+ assert "input_media_gcs_path" in job
226
+
227
+
228
+ class TestInternalEndpoints:
229
+ """Test internal worker endpoints."""
230
+
231
+ def test_internal_workers_exist(self, client, auth_headers):
232
+ """Test that internal worker endpoints exist and respond."""
233
+ # Create a job first
234
+ create_resp = client.post(
235
+ "/api/jobs",
236
+ headers=auth_headers,
237
+ json={"url": "https://youtube.com/watch?v=worker-test"}
238
+ )
239
+ job_id = create_resp.json()["job_id"]
240
+
241
+ time.sleep(0.2)
242
+
243
+ # Test audio worker endpoint exists
244
+ response = client.post(
245
+ "/api/internal/workers/audio",
246
+ headers=auth_headers,
247
+ json={"job_id": job_id}
248
+ )
249
+ assert response.status_code == 200
250
+ assert response.json()["status"] == "started"
251
+
252
+
253
+ class TestReviewEndpoints:
254
+ """Test lyrics review API endpoints."""
255
+
256
+ def test_review_ping(self, client, auth_headers):
257
+ """Test review ping endpoint."""
258
+ # Create a job first
259
+ create_resp = client.post(
260
+ "/api/jobs",
261
+ headers=auth_headers,
262
+ json={"url": "https://youtube.com/watch?v=review-test"}
263
+ )
264
+ job_id = create_resp.json()["job_id"]
265
+
266
+ # Ping should work for any job
267
+ response = client.get(f"/api/review/{job_id}/ping")
268
+ assert response.status_code == 200
269
+ assert response.json()["status"] == "ok"
270
+
271
+ def test_review_correction_data_wrong_status(self, client, auth_headers):
272
+ """Test that correction-data returns error for non-review jobs."""
273
+ # Create a job (starts in pending status)
274
+ create_resp = client.post(
275
+ "/api/jobs",
276
+ headers=auth_headers,
277
+ json={"url": "https://youtube.com/watch?v=review-status-test"}
278
+ )
279
+ job_id = create_resp.json()["job_id"]
280
+
281
+ time.sleep(0.2)
282
+
283
+ # Should fail - job not in AWAITING_REVIEW status
284
+ response = client.get(f"/api/review/{job_id}/correction-data", headers=auth_headers)
285
+ assert response.status_code == 400
286
+ assert "not ready for review" in response.json()["detail"].lower()
287
+
288
+ def test_review_audio_no_job(self, client, auth_headers):
289
+ """Test audio endpoint returns 404 for nonexistent job."""
290
+ response = client.get("/api/review/nonexistent-job/audio/", headers=auth_headers)
291
+ assert response.status_code == 404
292
+
293
+ def test_review_preview_video_stub(self, client, auth_headers):
294
+ """Test preview video endpoint exists."""
295
+ # Create a job
296
+ create_resp = client.post(
297
+ "/api/jobs",
298
+ headers=auth_headers,
299
+ json={"url": "https://youtube.com/watch?v=preview-test"}
300
+ )
301
+ job_id = create_resp.json()["job_id"]
302
+
303
+ # Preview video should return error since job not ready
304
+ response = client.post(
305
+ f"/api/review/{job_id}/preview-video",
306
+ headers=auth_headers,
307
+ json={"corrections": [], "corrected_segments": []}
308
+ )
309
+ # Should return 400 (job not in AWAITING_REVIEW state)
310
+ assert response.status_code == 400
311
+
312
+ def test_review_annotations_stub(self, client, auth_headers):
313
+ """Test annotations endpoint (stub)."""
314
+ create_resp = client.post(
315
+ "/api/jobs",
316
+ headers=auth_headers,
317
+ json={"url": "https://youtube.com/watch?v=annotation-test"}
318
+ )
319
+ job_id = create_resp.json()["job_id"]
320
+
321
+ # Annotations endpoint should accept data (even if it just logs it)
322
+ response = client.post(
323
+ f"/api/review/{job_id}/v1/annotations",
324
+ headers=auth_headers,
325
+ json={"type": "test", "data": "test annotation"}
326
+ )
327
+ assert response.status_code == 200
328
+ assert response.json()["status"] == "success"
329
+
330
+
331
+ class TestRenderVideoWorker:
332
+ """Test render video worker endpoint."""
333
+
334
+ def test_render_video_worker_endpoint_exists(self, client, auth_headers):
335
+ """Test that render-video worker endpoint exists."""
336
+ # Create a job
337
+ create_resp = client.post(
338
+ "/api/jobs",
339
+ headers=auth_headers,
340
+ json={"url": "https://youtube.com/watch?v=render-test"}
341
+ )
342
+ job_id = create_resp.json()["job_id"]
343
+
344
+ time.sleep(0.2)
345
+
346
+ # Test render-video worker endpoint exists
347
+ response = client.post(
348
+ "/api/internal/workers/render-video",
349
+ headers=auth_headers,
350
+ json={"job_id": job_id}
351
+ )
352
+ # Should return 200 (endpoint exists and starts)
353
+ assert response.status_code == 200
354
+
355
+
356
+ print("✅ Emulator integration tests ready to run")