karaoke-gen 0.96.0__py3-none-any.whl → 0.101.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.
- backend/api/routes/admin.py +696 -92
- backend/api/routes/audio_search.py +29 -8
- backend/api/routes/file_upload.py +99 -22
- backend/api/routes/health.py +65 -0
- backend/api/routes/internal.py +6 -0
- backend/api/routes/jobs.py +28 -1
- backend/api/routes/review.py +13 -6
- backend/api/routes/tenant.py +120 -0
- backend/api/routes/users.py +472 -51
- backend/main.py +31 -2
- backend/middleware/__init__.py +7 -1
- backend/middleware/tenant.py +192 -0
- backend/models/job.py +19 -3
- backend/models/tenant.py +208 -0
- backend/models/user.py +18 -0
- backend/services/email_service.py +253 -6
- backend/services/encoding_service.py +128 -31
- backend/services/firestore_service.py +6 -0
- backend/services/job_manager.py +44 -2
- backend/services/langfuse_preloader.py +98 -0
- backend/services/nltk_preloader.py +122 -0
- backend/services/spacy_preloader.py +65 -0
- backend/services/stripe_service.py +133 -11
- backend/services/tenant_service.py +285 -0
- backend/services/user_service.py +85 -7
- backend/tests/emulator/conftest.py +22 -1
- backend/tests/emulator/test_made_for_you_integration.py +167 -0
- backend/tests/test_admin_job_files.py +337 -0
- backend/tests/test_admin_job_reset.py +384 -0
- backend/tests/test_admin_job_update.py +326 -0
- backend/tests/test_email_service.py +233 -0
- backend/tests/test_impersonation.py +223 -0
- backend/tests/test_job_creation_regression.py +4 -0
- backend/tests/test_job_manager.py +171 -9
- backend/tests/test_jobs_api.py +11 -1
- backend/tests/test_made_for_you.py +2086 -0
- backend/tests/test_models.py +139 -0
- backend/tests/test_spacy_preloader.py +119 -0
- backend/tests/test_tenant_api.py +350 -0
- backend/tests/test_tenant_middleware.py +345 -0
- backend/tests/test_tenant_models.py +406 -0
- backend/tests/test_tenant_service.py +418 -0
- backend/utils/test_data.py +27 -0
- backend/workers/screens_worker.py +16 -6
- backend/workers/video_worker.py +8 -3
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.dist-info}/METADATA +1 -1
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.dist-info}/RECORD +58 -39
- lyrics_transcriber/correction/agentic/agent.py +17 -6
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +96 -43
- lyrics_transcriber/correction/agentic/providers/model_factory.py +27 -6
- lyrics_transcriber/correction/anchor_sequence.py +151 -37
- lyrics_transcriber/correction/handlers/syllables_match.py +44 -2
- lyrics_transcriber/correction/phrase_analyzer.py +18 -0
- lyrics_transcriber/frontend/src/api.ts +13 -5
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +90 -57
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for admin job reset endpoint.
|
|
3
|
+
|
|
4
|
+
Tests the POST /api/admin/jobs/{job_id}/reset endpoint that allows admins
|
|
5
|
+
to reset a job to a specific state for re-processing.
|
|
6
|
+
"""
|
|
7
|
+
import pytest
|
|
8
|
+
from unittest.mock import Mock, patch
|
|
9
|
+
from fastapi.testclient import TestClient
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
from backend.api.routes.admin import router
|
|
14
|
+
from backend.api.dependencies import require_admin
|
|
15
|
+
from backend.models.job import Job, JobStatus
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Create a test app with the admin router
|
|
19
|
+
app = FastAPI()
|
|
20
|
+
app.include_router(router, prefix="/api")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_mock_admin():
|
|
24
|
+
"""Override for require_admin dependency."""
|
|
25
|
+
from backend.api.dependencies import AuthResult, UserType
|
|
26
|
+
return AuthResult(
|
|
27
|
+
is_valid=True,
|
|
28
|
+
user_type=UserType.ADMIN,
|
|
29
|
+
remaining_uses=999,
|
|
30
|
+
message="Admin authenticated",
|
|
31
|
+
user_email="admin@example.com",
|
|
32
|
+
is_admin=True,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Override the require_admin dependency
|
|
37
|
+
app.dependency_overrides[require_admin] = get_mock_admin
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def client():
|
|
42
|
+
"""Create a test client."""
|
|
43
|
+
return TestClient(app)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def mock_job():
|
|
48
|
+
"""Create a mock job in COMPLETE status for testing."""
|
|
49
|
+
job = Mock(spec=Job)
|
|
50
|
+
job.job_id = "test-job-123"
|
|
51
|
+
job.user_email = "user@example.com"
|
|
52
|
+
job.artist = "Test Artist"
|
|
53
|
+
job.title = "Test Title"
|
|
54
|
+
job.status = JobStatus.COMPLETE
|
|
55
|
+
job.theme_id = "nomad"
|
|
56
|
+
job.file_urls = {
|
|
57
|
+
"input": "gs://bucket/input.flac",
|
|
58
|
+
"stems": {
|
|
59
|
+
"instrumental_clean": "gs://bucket/stems/clean.flac",
|
|
60
|
+
},
|
|
61
|
+
"lyrics": {
|
|
62
|
+
"corrections": "gs://bucket/lyrics/corrections.json",
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
job.state_data = {
|
|
66
|
+
"audio_search_results": [{"provider": "test"}],
|
|
67
|
+
"audio_selection": {"index": 0},
|
|
68
|
+
"review_complete": True,
|
|
69
|
+
"corrected_lyrics": {"lines": []},
|
|
70
|
+
"instrumental_selection": "clean",
|
|
71
|
+
"brand_code": "NOMAD-1234",
|
|
72
|
+
}
|
|
73
|
+
job.timeline = []
|
|
74
|
+
job.created_at = datetime(2026, 1, 9, 10, 0, 0)
|
|
75
|
+
job.updated_at = datetime(2026, 1, 9, 12, 0, 0)
|
|
76
|
+
return job
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestResetJobToPending:
|
|
80
|
+
"""Tests for resetting job to PENDING state."""
|
|
81
|
+
|
|
82
|
+
def test_reset_to_pending_success(self, client, mock_job):
|
|
83
|
+
"""Test resetting a job to PENDING state."""
|
|
84
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
85
|
+
mock_jm = Mock()
|
|
86
|
+
mock_jm.get_job.return_value = mock_job
|
|
87
|
+
mock_jm.update_job.return_value = True
|
|
88
|
+
mock_jm_class.return_value = mock_jm
|
|
89
|
+
|
|
90
|
+
response = client.post(
|
|
91
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
92
|
+
json={"target_state": "pending"},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
assert response.status_code == 200
|
|
96
|
+
data = response.json()
|
|
97
|
+
assert data["status"] == "success"
|
|
98
|
+
assert data["job_id"] == "test-job-123"
|
|
99
|
+
assert data["new_status"] == "pending"
|
|
100
|
+
assert "pending" in data["message"].lower()
|
|
101
|
+
|
|
102
|
+
def test_reset_to_pending_clears_state_data(self, client, mock_job):
|
|
103
|
+
"""Test that resetting to PENDING clears appropriate state_data keys."""
|
|
104
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
105
|
+
mock_jm = Mock()
|
|
106
|
+
mock_jm.get_job.return_value = mock_job
|
|
107
|
+
mock_jm.update_job.return_value = True
|
|
108
|
+
mock_jm_class.return_value = mock_jm
|
|
109
|
+
|
|
110
|
+
response = client.post(
|
|
111
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
112
|
+
json={"target_state": "pending"},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
assert response.status_code == 200
|
|
116
|
+
# Verify update_job was called with cleared state_data keys
|
|
117
|
+
call_args = mock_jm.update_job.call_args
|
|
118
|
+
update_data = call_args[0][1]
|
|
119
|
+
# Verify status was set
|
|
120
|
+
assert update_data["status"] == "pending"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestResetJobToAwaitingAudioSelection:
|
|
124
|
+
"""Tests for resetting job to AWAITING_AUDIO_SELECTION state."""
|
|
125
|
+
|
|
126
|
+
def test_reset_to_awaiting_audio_selection(self, client, mock_job):
|
|
127
|
+
"""Test resetting a job to AWAITING_AUDIO_SELECTION state."""
|
|
128
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
129
|
+
mock_jm = Mock()
|
|
130
|
+
mock_jm.get_job.return_value = mock_job
|
|
131
|
+
mock_jm.update_job.return_value = True
|
|
132
|
+
mock_jm_class.return_value = mock_jm
|
|
133
|
+
|
|
134
|
+
response = client.post(
|
|
135
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
136
|
+
json={"target_state": "awaiting_audio_selection"},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
assert response.status_code == 200
|
|
140
|
+
data = response.json()
|
|
141
|
+
assert data["new_status"] == "awaiting_audio_selection"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TestResetJobToAwaitingReview:
|
|
145
|
+
"""Tests for resetting job to AWAITING_REVIEW state."""
|
|
146
|
+
|
|
147
|
+
def test_reset_to_awaiting_review(self, client, mock_job):
|
|
148
|
+
"""Test resetting a job to AWAITING_REVIEW state."""
|
|
149
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
150
|
+
mock_jm = Mock()
|
|
151
|
+
mock_jm.get_job.return_value = mock_job
|
|
152
|
+
mock_jm.update_job.return_value = True
|
|
153
|
+
mock_jm_class.return_value = mock_jm
|
|
154
|
+
|
|
155
|
+
response = client.post(
|
|
156
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
157
|
+
json={"target_state": "awaiting_review"},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
assert response.status_code == 200
|
|
161
|
+
data = response.json()
|
|
162
|
+
assert data["new_status"] == "awaiting_review"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TestResetJobToAwaitingInstrumentalSelection:
|
|
166
|
+
"""Tests for resetting job to AWAITING_INSTRUMENTAL_SELECTION state."""
|
|
167
|
+
|
|
168
|
+
def test_reset_to_awaiting_instrumental_selection(self, client, mock_job):
|
|
169
|
+
"""Test resetting a job to AWAITING_INSTRUMENTAL_SELECTION state."""
|
|
170
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
171
|
+
mock_jm = Mock()
|
|
172
|
+
mock_jm.get_job.return_value = mock_job
|
|
173
|
+
mock_jm.update_job.return_value = True
|
|
174
|
+
mock_jm_class.return_value = mock_jm
|
|
175
|
+
|
|
176
|
+
response = client.post(
|
|
177
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
178
|
+
json={"target_state": "awaiting_instrumental_selection"},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
assert response.status_code == 200
|
|
182
|
+
data = response.json()
|
|
183
|
+
assert data["new_status"] == "awaiting_instrumental_selection"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestResetJobValidation:
|
|
187
|
+
"""Tests for reset endpoint validation."""
|
|
188
|
+
|
|
189
|
+
def test_rejects_invalid_target_state(self, client, mock_job):
|
|
190
|
+
"""Test that invalid target states are rejected."""
|
|
191
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
192
|
+
mock_jm = Mock()
|
|
193
|
+
mock_jm.get_job.return_value = mock_job
|
|
194
|
+
mock_jm_class.return_value = mock_jm
|
|
195
|
+
|
|
196
|
+
response = client.post(
|
|
197
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
198
|
+
json={"target_state": "encoding"},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
assert response.status_code == 400
|
|
202
|
+
assert "invalid" in response.json()["detail"].lower() or "not allowed" in response.json()["detail"].lower()
|
|
203
|
+
|
|
204
|
+
def test_rejects_complete_as_target(self, client, mock_job):
|
|
205
|
+
"""Test that COMPLETE is not a valid reset target."""
|
|
206
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
207
|
+
mock_jm = Mock()
|
|
208
|
+
mock_jm.get_job.return_value = mock_job
|
|
209
|
+
mock_jm_class.return_value = mock_jm
|
|
210
|
+
|
|
211
|
+
response = client.post(
|
|
212
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
213
|
+
json={"target_state": "complete"},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
assert response.status_code == 400
|
|
217
|
+
|
|
218
|
+
def test_rejects_failed_as_target(self, client, mock_job):
|
|
219
|
+
"""Test that FAILED is not a valid reset target."""
|
|
220
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
221
|
+
mock_jm = Mock()
|
|
222
|
+
mock_jm.get_job.return_value = mock_job
|
|
223
|
+
mock_jm_class.return_value = mock_jm
|
|
224
|
+
|
|
225
|
+
response = client.post(
|
|
226
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
227
|
+
json={"target_state": "failed"},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
assert response.status_code == 400
|
|
231
|
+
|
|
232
|
+
def test_returns_404_when_job_not_found(self, client):
|
|
233
|
+
"""Test 404 when job doesn't exist."""
|
|
234
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
235
|
+
mock_jm = Mock()
|
|
236
|
+
mock_jm.get_job.return_value = None
|
|
237
|
+
mock_jm_class.return_value = mock_jm
|
|
238
|
+
|
|
239
|
+
response = client.post(
|
|
240
|
+
"/api/admin/jobs/nonexistent-job/reset",
|
|
241
|
+
json={"target_state": "pending"},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
assert response.status_code == 404
|
|
245
|
+
assert "not found" in response.json()["detail"].lower()
|
|
246
|
+
|
|
247
|
+
def test_rejects_missing_target_state(self, client, mock_job):
|
|
248
|
+
"""Test that target_state is required."""
|
|
249
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
250
|
+
mock_jm = Mock()
|
|
251
|
+
mock_jm.get_job.return_value = mock_job
|
|
252
|
+
mock_jm_class.return_value = mock_jm
|
|
253
|
+
|
|
254
|
+
response = client.post(
|
|
255
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
256
|
+
json={},
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
assert response.status_code == 422 # Validation error
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class TestResetJobLogging:
|
|
263
|
+
"""Tests for logging on the reset endpoint."""
|
|
264
|
+
|
|
265
|
+
def test_logs_admin_reset_action(self, client, mock_job):
|
|
266
|
+
"""Test that admin reset actions are logged."""
|
|
267
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
268
|
+
patch('backend.api.routes.admin.logger') as mock_logger:
|
|
269
|
+
mock_jm = Mock()
|
|
270
|
+
mock_jm.get_job.return_value = mock_job
|
|
271
|
+
mock_jm.update_job.return_value = True
|
|
272
|
+
mock_jm_class.return_value = mock_jm
|
|
273
|
+
|
|
274
|
+
response = client.post(
|
|
275
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
276
|
+
json={"target_state": "pending"},
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
assert response.status_code == 200
|
|
280
|
+
# Verify logging was called
|
|
281
|
+
mock_logger.info.assert_called()
|
|
282
|
+
log_message = mock_logger.info.call_args[0][0]
|
|
283
|
+
assert "admin" in log_message.lower() or "reset" in log_message.lower()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class TestResetJobTimeline:
|
|
287
|
+
"""Tests for timeline event on reset."""
|
|
288
|
+
|
|
289
|
+
def test_adds_timeline_event_on_reset(self, client, mock_job):
|
|
290
|
+
"""Test that a timeline event is added when job is reset."""
|
|
291
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
292
|
+
mock_jm = Mock()
|
|
293
|
+
mock_jm.get_job.return_value = mock_job
|
|
294
|
+
mock_jm.update_job.return_value = True
|
|
295
|
+
mock_jm_class.return_value = mock_jm
|
|
296
|
+
|
|
297
|
+
response = client.post(
|
|
298
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
299
|
+
json={"target_state": "awaiting_review"},
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
assert response.status_code == 200
|
|
303
|
+
data = response.json()
|
|
304
|
+
# Timeline event should be included in response
|
|
305
|
+
assert "timeline_event" in data or data.get("message")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class TestResetJobAuthorization:
|
|
309
|
+
"""Tests for authorization on the reset endpoint."""
|
|
310
|
+
|
|
311
|
+
def test_requires_admin_access(self, client, mock_job):
|
|
312
|
+
"""Test that non-admin users cannot access the endpoint."""
|
|
313
|
+
original_override = app.dependency_overrides.get(require_admin)
|
|
314
|
+
|
|
315
|
+
def get_non_admin():
|
|
316
|
+
from fastapi import HTTPException
|
|
317
|
+
raise HTTPException(status_code=403, detail="Admin access required")
|
|
318
|
+
|
|
319
|
+
app.dependency_overrides[require_admin] = get_non_admin
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
response = client.post(
|
|
323
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
324
|
+
json={"target_state": "pending"},
|
|
325
|
+
headers={"Authorization": "Bearer user-token"}
|
|
326
|
+
)
|
|
327
|
+
assert response.status_code == 403
|
|
328
|
+
finally:
|
|
329
|
+
if original_override:
|
|
330
|
+
app.dependency_overrides[require_admin] = original_override
|
|
331
|
+
else:
|
|
332
|
+
app.dependency_overrides[require_admin] = get_mock_admin
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class TestResetJobClearsStateData:
|
|
336
|
+
"""Tests for state_data clearing based on target state."""
|
|
337
|
+
|
|
338
|
+
def test_reset_to_pending_clears_all_processing_data(self, client, mock_job):
|
|
339
|
+
"""Test that resetting to PENDING clears audio search results."""
|
|
340
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
341
|
+
mock_jm = Mock()
|
|
342
|
+
mock_jm.get_job.return_value = mock_job
|
|
343
|
+
mock_jm.update_job.return_value = True
|
|
344
|
+
mock_jm_class.return_value = mock_jm
|
|
345
|
+
|
|
346
|
+
response = client.post(
|
|
347
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
348
|
+
json={"target_state": "pending"},
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
assert response.status_code == 200
|
|
352
|
+
# The cleared_data field should list what was cleared
|
|
353
|
+
data = response.json()
|
|
354
|
+
assert "cleared_data" in data or "state_data" in str(data)
|
|
355
|
+
|
|
356
|
+
def test_reset_to_awaiting_review_preserves_audio_data(self, client, mock_job):
|
|
357
|
+
"""Test that resetting to AWAITING_REVIEW preserves audio stems."""
|
|
358
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
359
|
+
mock_jm = Mock()
|
|
360
|
+
mock_jm.get_job.return_value = mock_job
|
|
361
|
+
mock_jm.update_job.return_value = True
|
|
362
|
+
mock_jm_class.return_value = mock_jm
|
|
363
|
+
|
|
364
|
+
response = client.post(
|
|
365
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
366
|
+
json={"target_state": "awaiting_review"},
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
assert response.status_code == 200
|
|
370
|
+
|
|
371
|
+
def test_reset_to_instrumental_preserves_review_data(self, client, mock_job):
|
|
372
|
+
"""Test that resetting to AWAITING_INSTRUMENTAL_SELECTION preserves review data."""
|
|
373
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
374
|
+
mock_jm = Mock()
|
|
375
|
+
mock_jm.get_job.return_value = mock_job
|
|
376
|
+
mock_jm.update_job.return_value = True
|
|
377
|
+
mock_jm_class.return_value = mock_jm
|
|
378
|
+
|
|
379
|
+
response = client.post(
|
|
380
|
+
"/api/admin/jobs/test-job-123/reset",
|
|
381
|
+
json={"target_state": "awaiting_instrumental_selection"},
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
assert response.status_code == 200
|