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.
- backend/.coveragerc +20 -0
- backend/.gitignore +37 -0
- backend/Dockerfile +43 -0
- backend/Dockerfile.base +74 -0
- backend/README.md +242 -0
- backend/__init__.py +0 -0
- backend/api/__init__.py +0 -0
- backend/api/dependencies.py +457 -0
- backend/api/routes/__init__.py +0 -0
- backend/api/routes/admin.py +742 -0
- backend/api/routes/audio_search.py +903 -0
- backend/api/routes/auth.py +348 -0
- backend/api/routes/file_upload.py +2076 -0
- backend/api/routes/health.py +344 -0
- backend/api/routes/internal.py +435 -0
- backend/api/routes/jobs.py +1610 -0
- backend/api/routes/review.py +652 -0
- backend/api/routes/themes.py +162 -0
- backend/api/routes/users.py +1014 -0
- backend/config.py +172 -0
- backend/main.py +133 -0
- backend/middleware/__init__.py +5 -0
- backend/middleware/audit_logging.py +124 -0
- backend/models/__init__.py +0 -0
- backend/models/job.py +519 -0
- backend/models/requests.py +123 -0
- backend/models/theme.py +153 -0
- backend/models/user.py +254 -0
- backend/models/worker_log.py +164 -0
- backend/pyproject.toml +29 -0
- backend/quick-check.sh +93 -0
- backend/requirements.txt +29 -0
- backend/run_tests.sh +60 -0
- backend/services/__init__.py +0 -0
- backend/services/audio_analysis_service.py +243 -0
- backend/services/audio_editing_service.py +278 -0
- backend/services/audio_search_service.py +702 -0
- backend/services/auth_service.py +630 -0
- backend/services/credential_manager.py +792 -0
- backend/services/discord_service.py +172 -0
- backend/services/dropbox_service.py +301 -0
- backend/services/email_service.py +1093 -0
- backend/services/encoding_interface.py +454 -0
- backend/services/encoding_service.py +405 -0
- backend/services/firestore_service.py +512 -0
- backend/services/flacfetch_client.py +573 -0
- backend/services/gce_encoding/README.md +72 -0
- backend/services/gce_encoding/__init__.py +22 -0
- backend/services/gce_encoding/main.py +589 -0
- backend/services/gce_encoding/requirements.txt +16 -0
- backend/services/gdrive_service.py +356 -0
- backend/services/job_logging.py +258 -0
- backend/services/job_manager.py +842 -0
- backend/services/job_notification_service.py +271 -0
- backend/services/local_encoding_service.py +590 -0
- backend/services/local_preview_encoding_service.py +407 -0
- backend/services/lyrics_cache_service.py +216 -0
- backend/services/metrics.py +413 -0
- backend/services/packaging_service.py +287 -0
- backend/services/rclone_service.py +106 -0
- backend/services/storage_service.py +209 -0
- backend/services/stripe_service.py +275 -0
- backend/services/structured_logging.py +254 -0
- backend/services/template_service.py +330 -0
- backend/services/theme_service.py +469 -0
- backend/services/tracing.py +543 -0
- backend/services/user_service.py +721 -0
- backend/services/worker_service.py +558 -0
- backend/services/youtube_service.py +112 -0
- backend/services/youtube_upload_service.py +445 -0
- backend/tests/__init__.py +4 -0
- backend/tests/conftest.py +224 -0
- backend/tests/emulator/__init__.py +7 -0
- backend/tests/emulator/conftest.py +88 -0
- backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
- backend/tests/emulator/test_emulator_integration.py +356 -0
- backend/tests/emulator/test_style_loading_direct.py +436 -0
- backend/tests/emulator/test_worker_logs_direct.py +229 -0
- backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
- backend/tests/requirements-test.txt +10 -0
- backend/tests/requirements.txt +6 -0
- backend/tests/test_admin_email_endpoints.py +411 -0
- backend/tests/test_api_integration.py +460 -0
- backend/tests/test_api_routes.py +93 -0
- backend/tests/test_audio_analysis_service.py +294 -0
- backend/tests/test_audio_editing_service.py +386 -0
- backend/tests/test_audio_search.py +1398 -0
- backend/tests/test_audio_services.py +378 -0
- backend/tests/test_auth_firestore.py +231 -0
- backend/tests/test_config_extended.py +68 -0
- backend/tests/test_credential_manager.py +377 -0
- backend/tests/test_dependencies.py +54 -0
- backend/tests/test_discord_service.py +244 -0
- backend/tests/test_distribution_services.py +820 -0
- backend/tests/test_dropbox_service.py +472 -0
- backend/tests/test_email_service.py +492 -0
- backend/tests/test_emulator_integration.py +322 -0
- backend/tests/test_encoding_interface.py +412 -0
- backend/tests/test_file_upload.py +1739 -0
- backend/tests/test_flacfetch_client.py +632 -0
- backend/tests/test_gdrive_service.py +524 -0
- backend/tests/test_instrumental_api.py +431 -0
- backend/tests/test_internal_api.py +343 -0
- backend/tests/test_job_creation_regression.py +583 -0
- backend/tests/test_job_manager.py +339 -0
- backend/tests/test_job_manager_notifications.py +329 -0
- backend/tests/test_job_notification_service.py +443 -0
- backend/tests/test_jobs_api.py +273 -0
- backend/tests/test_local_encoding_service.py +423 -0
- backend/tests/test_local_preview_encoding_service.py +567 -0
- backend/tests/test_main.py +87 -0
- backend/tests/test_models.py +918 -0
- backend/tests/test_packaging_service.py +382 -0
- backend/tests/test_requests.py +201 -0
- backend/tests/test_routes_jobs.py +282 -0
- backend/tests/test_routes_review.py +337 -0
- backend/tests/test_services.py +556 -0
- backend/tests/test_services_extended.py +112 -0
- backend/tests/test_storage_service.py +448 -0
- backend/tests/test_style_upload.py +261 -0
- backend/tests/test_template_service.py +295 -0
- backend/tests/test_theme_service.py +516 -0
- backend/tests/test_unicode_sanitization.py +522 -0
- backend/tests/test_upload_api.py +256 -0
- backend/tests/test_validate.py +156 -0
- backend/tests/test_video_worker_orchestrator.py +847 -0
- backend/tests/test_worker_log_subcollection.py +509 -0
- backend/tests/test_worker_logging.py +365 -0
- backend/tests/test_workers.py +1116 -0
- backend/tests/test_workers_extended.py +178 -0
- backend/tests/test_youtube_service.py +247 -0
- backend/tests/test_youtube_upload_service.py +568 -0
- backend/validate.py +173 -0
- backend/version.py +27 -0
- backend/workers/README.md +597 -0
- backend/workers/__init__.py +11 -0
- backend/workers/audio_worker.py +618 -0
- backend/workers/lyrics_worker.py +683 -0
- backend/workers/render_video_worker.py +483 -0
- backend/workers/screens_worker.py +525 -0
- backend/workers/style_helper.py +198 -0
- backend/workers/video_worker.py +1277 -0
- backend/workers/video_worker_orchestrator.py +701 -0
- backend/workers/worker_logging.py +278 -0
- karaoke_gen/instrumental_review/static/index.html +7 -4
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
- karaoke_gen/style_loader.py +3 -1
- karaoke_gen/utils/__init__.py +163 -8
- karaoke_gen/video_background_processor.py +9 -4
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/METADATA +2 -1
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/RECORD +187 -42
- lyrics_transcriber/correction/agentic/providers/config.py +9 -5
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +1 -51
- lyrics_transcriber/correction/corrector.py +192 -130
- lyrics_transcriber/correction/operations.py +24 -9
- lyrics_transcriber/frontend/package-lock.json +2 -2
- lyrics_transcriber/frontend/package.json +1 -1
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
- lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
- lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
- lyrics_transcriber/frontend/src/theme.ts +42 -15
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/vite.config.js +5 -0
- lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
- lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/index.html +6 -2
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
- lyrics_transcriber/output/generator.py +17 -3
- lyrics_transcriber/output/video.py +60 -95
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API endpoint tests for instrumental review functionality.
|
|
3
|
+
|
|
4
|
+
Tests for:
|
|
5
|
+
- GET /api/jobs/{job_id}/instrumental-analysis
|
|
6
|
+
- GET /api/jobs/{job_id}/audio-stream/{stem_type}
|
|
7
|
+
- POST /api/jobs/{job_id}/create-custom-instrumental
|
|
8
|
+
- GET /api/jobs/{job_id}/waveform-data
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from unittest.mock import MagicMock, patch, AsyncMock
|
|
14
|
+
|
|
15
|
+
from backend.models.job import Job, JobStatus
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def mock_job_manager():
|
|
20
|
+
"""Create a mock job manager."""
|
|
21
|
+
with patch('backend.api.routes.jobs.job_manager') as mock:
|
|
22
|
+
yield mock
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def mock_storage_service():
|
|
27
|
+
"""Create a mock storage service."""
|
|
28
|
+
with patch('backend.api.routes.jobs.StorageService') as mock:
|
|
29
|
+
mock_instance = MagicMock()
|
|
30
|
+
mock_instance.generate_signed_url.return_value = "https://storage.googleapis.com/signed-url"
|
|
31
|
+
mock.return_value = mock_instance
|
|
32
|
+
yield mock_instance
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def sample_job():
|
|
37
|
+
"""Create a sample job for testing."""
|
|
38
|
+
return Job(
|
|
39
|
+
job_id="test-job-123",
|
|
40
|
+
artist="Test Artist",
|
|
41
|
+
title="Test Song",
|
|
42
|
+
status=JobStatus.AWAITING_INSTRUMENTAL_SELECTION,
|
|
43
|
+
created_at=datetime.now(),
|
|
44
|
+
updated_at=datetime.now(),
|
|
45
|
+
file_urls={
|
|
46
|
+
"stems": {
|
|
47
|
+
"instrumental_clean": "jobs/test/stems/instrumental_clean.flac",
|
|
48
|
+
"backing_vocals": "jobs/test/stems/backing_vocals.flac",
|
|
49
|
+
"instrumental_with_backing": "jobs/test/stems/instrumental_with_backing.flac",
|
|
50
|
+
},
|
|
51
|
+
"analysis": {
|
|
52
|
+
"backing_vocals_waveform": "jobs/test/analysis/waveform.png",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
state_data={
|
|
56
|
+
"backing_vocals_analysis": {
|
|
57
|
+
"has_audible_content": True,
|
|
58
|
+
"total_duration_seconds": 180.0,
|
|
59
|
+
"audible_segments": [
|
|
60
|
+
{
|
|
61
|
+
"start_seconds": 10.0,
|
|
62
|
+
"end_seconds": 20.0,
|
|
63
|
+
"duration_seconds": 10.0,
|
|
64
|
+
"avg_amplitude_db": -25.0,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"start_seconds": 60.0,
|
|
68
|
+
"end_seconds": 80.0,
|
|
69
|
+
"duration_seconds": 20.0,
|
|
70
|
+
"avg_amplitude_db": -30.0,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
"recommended_selection": "review_needed",
|
|
74
|
+
"total_audible_duration_seconds": 30.0,
|
|
75
|
+
"audible_percentage": 16.67,
|
|
76
|
+
"silence_threshold_db": -40.0,
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.fixture
|
|
83
|
+
def silent_job():
|
|
84
|
+
"""Create a job with silent backing vocals."""
|
|
85
|
+
return Job(
|
|
86
|
+
job_id="test-job-silent",
|
|
87
|
+
artist="Test Artist",
|
|
88
|
+
title="Test Song",
|
|
89
|
+
status=JobStatus.AWAITING_INSTRUMENTAL_SELECTION,
|
|
90
|
+
created_at=datetime.now(),
|
|
91
|
+
updated_at=datetime.now(),
|
|
92
|
+
file_urls={
|
|
93
|
+
"stems": {
|
|
94
|
+
"instrumental_clean": "jobs/test/stems/instrumental_clean.flac",
|
|
95
|
+
"backing_vocals": "jobs/test/stems/backing_vocals.flac",
|
|
96
|
+
"instrumental_with_backing": "jobs/test/stems/instrumental_with_backing.flac",
|
|
97
|
+
},
|
|
98
|
+
"analysis": {
|
|
99
|
+
"backing_vocals_waveform": "jobs/test/analysis/waveform.png",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
state_data={
|
|
103
|
+
"backing_vocals_analysis": {
|
|
104
|
+
"has_audible_content": False,
|
|
105
|
+
"total_duration_seconds": 180.0,
|
|
106
|
+
"audible_segments": [],
|
|
107
|
+
"recommended_selection": "clean",
|
|
108
|
+
"total_audible_duration_seconds": 0.0,
|
|
109
|
+
"audible_percentage": 0.0,
|
|
110
|
+
"silence_threshold_db": -40.0,
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestGetInstrumentalAnalysis:
|
|
117
|
+
"""Tests for GET /api/jobs/{job_id}/instrumental-analysis."""
|
|
118
|
+
|
|
119
|
+
@pytest.mark.asyncio
|
|
120
|
+
async def test_get_analysis_returns_data(self, mock_job_manager, mock_storage_service, sample_job):
|
|
121
|
+
"""GET /instrumental-analysis should return analysis data."""
|
|
122
|
+
mock_job_manager.get_job.return_value = sample_job
|
|
123
|
+
|
|
124
|
+
from backend.api.routes.jobs import get_instrumental_analysis
|
|
125
|
+
|
|
126
|
+
result = await get_instrumental_analysis("test-job-123")
|
|
127
|
+
|
|
128
|
+
assert result["job_id"] == "test-job-123"
|
|
129
|
+
assert result["artist"] == "Test Artist"
|
|
130
|
+
assert result["title"] == "Test Song"
|
|
131
|
+
assert result["analysis"]["has_audible_content"] is True
|
|
132
|
+
assert len(result["analysis"]["audible_segments"]) == 2
|
|
133
|
+
|
|
134
|
+
@pytest.mark.asyncio
|
|
135
|
+
async def test_get_analysis_includes_audio_urls(self, mock_job_manager, mock_storage_service, sample_job):
|
|
136
|
+
"""GET /instrumental-analysis should include audio URLs."""
|
|
137
|
+
mock_job_manager.get_job.return_value = sample_job
|
|
138
|
+
|
|
139
|
+
from backend.api.routes.jobs import get_instrumental_analysis
|
|
140
|
+
|
|
141
|
+
result = await get_instrumental_analysis("test-job-123")
|
|
142
|
+
|
|
143
|
+
assert "audio_urls" in result
|
|
144
|
+
assert "clean_instrumental" in result["audio_urls"]
|
|
145
|
+
assert "backing_vocals" in result["audio_urls"]
|
|
146
|
+
assert "with_backing" in result["audio_urls"]
|
|
147
|
+
|
|
148
|
+
@pytest.mark.asyncio
|
|
149
|
+
async def test_get_analysis_includes_waveform_url(self, mock_job_manager, sample_job):
|
|
150
|
+
"""GET /instrumental-analysis should include waveform URL."""
|
|
151
|
+
mock_job_manager.get_job.return_value = sample_job
|
|
152
|
+
|
|
153
|
+
# Need to mock StorageService at module level for the signed URL
|
|
154
|
+
with patch('backend.api.routes.jobs.StorageService') as mock_storage_cls:
|
|
155
|
+
mock_storage = MagicMock()
|
|
156
|
+
mock_storage.generate_signed_url.return_value = "https://storage.googleapis.com/signed-url"
|
|
157
|
+
mock_storage_cls.return_value = mock_storage
|
|
158
|
+
|
|
159
|
+
from backend.api.routes.jobs import get_instrumental_analysis
|
|
160
|
+
|
|
161
|
+
result = await get_instrumental_analysis("test-job-123")
|
|
162
|
+
|
|
163
|
+
assert "waveform_url" in result
|
|
164
|
+
# Waveform URL should be generated from the file_urls
|
|
165
|
+
assert result["waveform_url"] == "https://storage.googleapis.com/signed-url"
|
|
166
|
+
|
|
167
|
+
@pytest.mark.asyncio
|
|
168
|
+
async def test_get_analysis_silent_audio(self, mock_job_manager, mock_storage_service, silent_job):
|
|
169
|
+
"""GET /instrumental-analysis should correctly report silent audio."""
|
|
170
|
+
mock_job_manager.get_job.return_value = silent_job
|
|
171
|
+
|
|
172
|
+
from backend.api.routes.jobs import get_instrumental_analysis
|
|
173
|
+
|
|
174
|
+
result = await get_instrumental_analysis("test-job-silent")
|
|
175
|
+
|
|
176
|
+
assert result["analysis"]["has_audible_content"] is False
|
|
177
|
+
assert result["analysis"]["recommended_selection"] == "clean"
|
|
178
|
+
assert len(result["analysis"]["audible_segments"]) == 0
|
|
179
|
+
|
|
180
|
+
@pytest.mark.asyncio
|
|
181
|
+
async def test_get_analysis_job_not_found(self, mock_job_manager):
|
|
182
|
+
"""GET /instrumental-analysis should return 404 for non-existent job."""
|
|
183
|
+
mock_job_manager.get_job.return_value = None
|
|
184
|
+
|
|
185
|
+
from backend.api.routes.jobs import get_instrumental_analysis
|
|
186
|
+
from fastapi import HTTPException
|
|
187
|
+
|
|
188
|
+
with pytest.raises(HTTPException) as exc_info:
|
|
189
|
+
await get_instrumental_analysis("non-existent")
|
|
190
|
+
|
|
191
|
+
assert exc_info.value.status_code == 404
|
|
192
|
+
|
|
193
|
+
@pytest.mark.asyncio
|
|
194
|
+
async def test_get_analysis_wrong_status(self, mock_job_manager, sample_job):
|
|
195
|
+
"""GET /instrumental-analysis should return 400 for wrong job status."""
|
|
196
|
+
sample_job.status = JobStatus.PENDING
|
|
197
|
+
mock_job_manager.get_job.return_value = sample_job
|
|
198
|
+
|
|
199
|
+
from backend.api.routes.jobs import get_instrumental_analysis
|
|
200
|
+
from fastapi import HTTPException
|
|
201
|
+
|
|
202
|
+
with pytest.raises(HTTPException) as exc_info:
|
|
203
|
+
await get_instrumental_analysis("test-job-123")
|
|
204
|
+
|
|
205
|
+
assert exc_info.value.status_code == 400
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class TestAudioStream:
|
|
209
|
+
"""Tests for GET /api/jobs/{job_id}/audio-stream/{stem_type}."""
|
|
210
|
+
|
|
211
|
+
def test_stream_backing_vocals_valid(self, mock_job_manager, sample_job):
|
|
212
|
+
"""Should return redirect for backing vocals stream."""
|
|
213
|
+
mock_job_manager.get_job.return_value = sample_job
|
|
214
|
+
|
|
215
|
+
from backend.api.routes.jobs import stream_audio
|
|
216
|
+
|
|
217
|
+
# This will try to stream from GCS which we can't test easily
|
|
218
|
+
# Instead, verify the function exists and has correct signature
|
|
219
|
+
assert callable(stream_audio)
|
|
220
|
+
|
|
221
|
+
@pytest.mark.asyncio
|
|
222
|
+
async def test_stream_invalid_stem_type(self, mock_job_manager, sample_job):
|
|
223
|
+
"""Should return 400 for invalid stem type."""
|
|
224
|
+
mock_job_manager.get_job.return_value = sample_job
|
|
225
|
+
|
|
226
|
+
from backend.api.routes.jobs import stream_audio
|
|
227
|
+
from fastapi import HTTPException
|
|
228
|
+
|
|
229
|
+
with pytest.raises(HTTPException) as exc_info:
|
|
230
|
+
await stream_audio("test-job-123", "invalid_stem")
|
|
231
|
+
|
|
232
|
+
assert exc_info.value.status_code == 400
|
|
233
|
+
|
|
234
|
+
@pytest.mark.asyncio
|
|
235
|
+
async def test_stream_job_not_found(self, mock_job_manager):
|
|
236
|
+
"""Should return 404 for non-existent job."""
|
|
237
|
+
mock_job_manager.get_job.return_value = None
|
|
238
|
+
|
|
239
|
+
from backend.api.routes.jobs import stream_audio
|
|
240
|
+
from fastapi import HTTPException
|
|
241
|
+
|
|
242
|
+
with pytest.raises(HTTPException) as exc_info:
|
|
243
|
+
await stream_audio("non-existent", "backing_vocals")
|
|
244
|
+
|
|
245
|
+
assert exc_info.value.status_code == 404
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class TestCreateCustomInstrumental:
|
|
249
|
+
"""Tests for POST /api/jobs/{job_id}/create-custom-instrumental."""
|
|
250
|
+
|
|
251
|
+
@pytest.fixture
|
|
252
|
+
def mock_audio_editing_service(self):
|
|
253
|
+
"""Mock the audio editing service."""
|
|
254
|
+
with patch('backend.api.routes.jobs.AudioEditingService') as mock:
|
|
255
|
+
mock_instance = MagicMock()
|
|
256
|
+
mock_instance.create_custom_instrumental.return_value = MagicMock(
|
|
257
|
+
output_path="jobs/test/stems/custom_instrumental.flac",
|
|
258
|
+
mute_regions_applied=[
|
|
259
|
+
MagicMock(start_seconds=10.0, end_seconds=20.0),
|
|
260
|
+
],
|
|
261
|
+
total_muted_duration_seconds=10.0,
|
|
262
|
+
output_duration_seconds=180.0,
|
|
263
|
+
)
|
|
264
|
+
mock.return_value = mock_instance
|
|
265
|
+
yield mock_instance
|
|
266
|
+
|
|
267
|
+
def test_create_custom_validates_mute_regions(self):
|
|
268
|
+
"""Should validate mute regions at model level."""
|
|
269
|
+
from backend.models.requests import CreateCustomInstrumentalRequest, MuteRegionRequest
|
|
270
|
+
from pydantic import ValidationError
|
|
271
|
+
|
|
272
|
+
# Empty mute regions should fail at model validation
|
|
273
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
274
|
+
CreateCustomInstrumentalRequest(mute_regions=[])
|
|
275
|
+
|
|
276
|
+
assert "At least one mute region is required" in str(exc_info.value)
|
|
277
|
+
|
|
278
|
+
@pytest.mark.asyncio
|
|
279
|
+
async def test_create_custom_job_not_found(self, mock_job_manager):
|
|
280
|
+
"""Should return 404 for non-existent job."""
|
|
281
|
+
mock_job_manager.get_job.return_value = None
|
|
282
|
+
|
|
283
|
+
from backend.api.routes.jobs import create_custom_instrumental
|
|
284
|
+
from backend.models.requests import CreateCustomInstrumentalRequest, MuteRegionRequest
|
|
285
|
+
from fastapi import HTTPException
|
|
286
|
+
|
|
287
|
+
request = CreateCustomInstrumentalRequest(
|
|
288
|
+
mute_regions=[MuteRegionRequest(start_seconds=10.0, end_seconds=20.0)]
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
with pytest.raises(HTTPException) as exc_info:
|
|
292
|
+
await create_custom_instrumental("non-existent", request)
|
|
293
|
+
|
|
294
|
+
assert exc_info.value.status_code == 404
|
|
295
|
+
|
|
296
|
+
@pytest.mark.asyncio
|
|
297
|
+
async def test_create_custom_wrong_status(self, mock_job_manager, sample_job):
|
|
298
|
+
"""Should return 400 for wrong job status."""
|
|
299
|
+
sample_job.status = JobStatus.PENDING
|
|
300
|
+
mock_job_manager.get_job.return_value = sample_job
|
|
301
|
+
|
|
302
|
+
from backend.api.routes.jobs import create_custom_instrumental
|
|
303
|
+
from backend.models.requests import CreateCustomInstrumentalRequest, MuteRegionRequest
|
|
304
|
+
from fastapi import HTTPException
|
|
305
|
+
|
|
306
|
+
request = CreateCustomInstrumentalRequest(
|
|
307
|
+
mute_regions=[MuteRegionRequest(start_seconds=10.0, end_seconds=20.0)]
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
with pytest.raises(HTTPException) as exc_info:
|
|
311
|
+
await create_custom_instrumental("test-job-123", request)
|
|
312
|
+
|
|
313
|
+
assert exc_info.value.status_code == 400
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class TestGetWaveformData:
|
|
317
|
+
"""Tests for GET /api/jobs/{job_id}/waveform-data."""
|
|
318
|
+
|
|
319
|
+
@pytest.mark.asyncio
|
|
320
|
+
async def test_get_waveform_data_job_not_found(self, mock_job_manager):
|
|
321
|
+
"""Should return 404 for non-existent job."""
|
|
322
|
+
mock_job_manager.get_job.return_value = None
|
|
323
|
+
|
|
324
|
+
from backend.api.routes.jobs import get_waveform_data
|
|
325
|
+
from fastapi import HTTPException
|
|
326
|
+
|
|
327
|
+
with pytest.raises(HTTPException) as exc_info:
|
|
328
|
+
await get_waveform_data("non-existent")
|
|
329
|
+
|
|
330
|
+
assert exc_info.value.status_code == 404
|
|
331
|
+
|
|
332
|
+
@pytest.mark.asyncio
|
|
333
|
+
async def test_get_waveform_data_wrong_status(self, mock_job_manager, sample_job):
|
|
334
|
+
"""Should return 400 for wrong job status."""
|
|
335
|
+
sample_job.status = JobStatus.PENDING
|
|
336
|
+
mock_job_manager.get_job.return_value = sample_job
|
|
337
|
+
|
|
338
|
+
from backend.api.routes.jobs import get_waveform_data
|
|
339
|
+
from fastapi import HTTPException
|
|
340
|
+
|
|
341
|
+
with pytest.raises(HTTPException) as exc_info:
|
|
342
|
+
await get_waveform_data("test-job-123")
|
|
343
|
+
|
|
344
|
+
assert exc_info.value.status_code == 400
|
|
345
|
+
|
|
346
|
+
def test_waveform_data_endpoint_exists(self):
|
|
347
|
+
"""Verify the endpoint function exists and is callable."""
|
|
348
|
+
from backend.api.routes.jobs import get_waveform_data
|
|
349
|
+
|
|
350
|
+
assert callable(get_waveform_data)
|
|
351
|
+
# Verify it's an async function
|
|
352
|
+
import inspect
|
|
353
|
+
assert inspect.iscoroutinefunction(get_waveform_data)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class TestRequestModels:
|
|
357
|
+
"""Tests for API request models."""
|
|
358
|
+
|
|
359
|
+
def test_mute_region_request_valid(self):
|
|
360
|
+
"""MuteRegionRequest should accept valid values."""
|
|
361
|
+
from backend.models.requests import MuteRegionRequest
|
|
362
|
+
|
|
363
|
+
region = MuteRegionRequest(start_seconds=10.0, end_seconds=20.0)
|
|
364
|
+
|
|
365
|
+
assert region.start_seconds == 10.0
|
|
366
|
+
assert region.end_seconds == 20.0
|
|
367
|
+
|
|
368
|
+
def test_mute_region_request_invalid_start(self):
|
|
369
|
+
"""MuteRegionRequest should reject negative start."""
|
|
370
|
+
from backend.models.requests import MuteRegionRequest
|
|
371
|
+
from pydantic import ValidationError
|
|
372
|
+
|
|
373
|
+
with pytest.raises(ValidationError):
|
|
374
|
+
MuteRegionRequest(start_seconds=-1.0, end_seconds=20.0)
|
|
375
|
+
|
|
376
|
+
def test_mute_region_request_invalid_order(self):
|
|
377
|
+
"""MuteRegionRequest should reject end before start."""
|
|
378
|
+
from backend.models.requests import MuteRegionRequest
|
|
379
|
+
from pydantic import ValidationError
|
|
380
|
+
|
|
381
|
+
with pytest.raises(ValidationError):
|
|
382
|
+
MuteRegionRequest(start_seconds=20.0, end_seconds=10.0)
|
|
383
|
+
|
|
384
|
+
def test_create_custom_instrumental_request_valid(self):
|
|
385
|
+
"""CreateCustomInstrumentalRequest should accept valid mute regions."""
|
|
386
|
+
from backend.models.requests import (
|
|
387
|
+
CreateCustomInstrumentalRequest,
|
|
388
|
+
MuteRegionRequest,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
request = CreateCustomInstrumentalRequest(
|
|
392
|
+
mute_regions=[
|
|
393
|
+
MuteRegionRequest(start_seconds=10.0, end_seconds=20.0),
|
|
394
|
+
MuteRegionRequest(start_seconds=60.0, end_seconds=80.0),
|
|
395
|
+
]
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
assert len(request.mute_regions) == 2
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class TestInstrumentalSelectionExtension:
|
|
402
|
+
"""Tests for extended instrumental selection (including 'custom')."""
|
|
403
|
+
|
|
404
|
+
def test_instrumental_selection_accepts_custom(self):
|
|
405
|
+
"""InstrumentalSelection should accept 'custom' as valid selection."""
|
|
406
|
+
from backend.models.requests import InstrumentalSelection
|
|
407
|
+
|
|
408
|
+
selection = InstrumentalSelection(selection="custom")
|
|
409
|
+
assert selection.selection == "custom"
|
|
410
|
+
|
|
411
|
+
def test_instrumental_selection_accepts_clean(self):
|
|
412
|
+
"""InstrumentalSelection should accept 'clean' as valid selection."""
|
|
413
|
+
from backend.models.requests import InstrumentalSelection
|
|
414
|
+
|
|
415
|
+
selection = InstrumentalSelection(selection="clean")
|
|
416
|
+
assert selection.selection == "clean"
|
|
417
|
+
|
|
418
|
+
def test_instrumental_selection_accepts_with_backing(self):
|
|
419
|
+
"""InstrumentalSelection should accept 'with_backing' as valid selection."""
|
|
420
|
+
from backend.models.requests import InstrumentalSelection
|
|
421
|
+
|
|
422
|
+
selection = InstrumentalSelection(selection="with_backing")
|
|
423
|
+
assert selection.selection == "with_backing"
|
|
424
|
+
|
|
425
|
+
def test_instrumental_selection_rejects_invalid(self):
|
|
426
|
+
"""InstrumentalSelection should reject invalid values."""
|
|
427
|
+
from backend.models.requests import InstrumentalSelection
|
|
428
|
+
from pydantic import ValidationError
|
|
429
|
+
|
|
430
|
+
with pytest.raises(ValidationError):
|
|
431
|
+
InstrumentalSelection(selection="invalid_option")
|