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.
- 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 +835 -0
- backend/api/routes/audio_search.py +913 -0
- backend/api/routes/auth.py +348 -0
- backend/api/routes/file_upload.py +2112 -0
- backend/api/routes/health.py +409 -0
- backend/api/routes/internal.py +435 -0
- backend/api/routes/jobs.py +1629 -0
- backend/api/routes/review.py +652 -0
- backend/api/routes/themes.py +162 -0
- backend/api/routes/users.py +1513 -0
- backend/config.py +172 -0
- backend/main.py +157 -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 +502 -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 +853 -0
- backend/services/job_notification_service.py +271 -0
- backend/services/langfuse_preloader.py +98 -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/nltk_preloader.py +122 -0
- backend/services/packaging_service.py +287 -0
- backend/services/rclone_service.py +106 -0
- backend/services/spacy_preloader.py +65 -0
- backend/services/storage_service.py +209 -0
- backend/services/stripe_service.py +371 -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 +109 -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 +356 -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 +283 -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_spacy_preloader.py +119 -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/utils/test_data.py +27 -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 +535 -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/utils/__init__.py +163 -8
- karaoke_gen/video_background_processor.py +9 -4
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/METADATA +1 -1
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/RECORD +196 -46
- lyrics_transcriber/correction/agentic/agent.py +17 -6
- lyrics_transcriber/correction/agentic/providers/config.py +9 -5
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +96 -93
- lyrics_transcriber/correction/agentic/providers/model_factory.py +27 -6
- lyrics_transcriber/correction/anchor_sequence.py +151 -37
- lyrics_transcriber/correction/corrector.py +192 -130
- lyrics_transcriber/correction/handlers/syllables_match.py +44 -2
- lyrics_transcriber/correction/operations.py +24 -9
- lyrics_transcriber/correction/phrase_analyzer.py +18 -0
- 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.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Encoding Interface.
|
|
3
|
+
|
|
4
|
+
Tests cover:
|
|
5
|
+
- EncodingInput and EncodingOutput dataclasses
|
|
6
|
+
- LocalEncodingBackend implementation
|
|
7
|
+
- Backend factory function
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from unittest.mock import MagicMock, patch, AsyncMock
|
|
12
|
+
|
|
13
|
+
from backend.services.encoding_interface import (
|
|
14
|
+
EncodingInput,
|
|
15
|
+
EncodingOutput,
|
|
16
|
+
EncodingBackend,
|
|
17
|
+
LocalEncodingBackend,
|
|
18
|
+
GCEEncodingBackend,
|
|
19
|
+
get_encoding_backend,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestEncodingInput:
|
|
24
|
+
"""Test EncodingInput dataclass."""
|
|
25
|
+
|
|
26
|
+
def test_required_fields(self):
|
|
27
|
+
"""Test that required fields must be provided."""
|
|
28
|
+
input_config = EncodingInput(
|
|
29
|
+
title_video_path="/path/title.mov",
|
|
30
|
+
karaoke_video_path="/path/karaoke.mov",
|
|
31
|
+
instrumental_audio_path="/path/audio.flac",
|
|
32
|
+
)
|
|
33
|
+
assert input_config.title_video_path == "/path/title.mov"
|
|
34
|
+
assert input_config.end_video_path is None
|
|
35
|
+
|
|
36
|
+
def test_all_fields(self):
|
|
37
|
+
"""Test all fields including optional ones."""
|
|
38
|
+
input_config = EncodingInput(
|
|
39
|
+
title_video_path="/path/title.mov",
|
|
40
|
+
karaoke_video_path="/path/karaoke.mov",
|
|
41
|
+
instrumental_audio_path="/path/audio.flac",
|
|
42
|
+
end_video_path="/path/end.mov",
|
|
43
|
+
artist="Test Artist",
|
|
44
|
+
title="Test Title",
|
|
45
|
+
brand_code="NOMAD-1234",
|
|
46
|
+
output_dir="/output",
|
|
47
|
+
options={"quality": "high"}
|
|
48
|
+
)
|
|
49
|
+
assert input_config.artist == "Test Artist"
|
|
50
|
+
assert input_config.brand_code == "NOMAD-1234"
|
|
51
|
+
assert input_config.options["quality"] == "high"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestEncodingOutput:
|
|
55
|
+
"""Test EncodingOutput dataclass."""
|
|
56
|
+
|
|
57
|
+
def test_success_output(self):
|
|
58
|
+
"""Test successful output."""
|
|
59
|
+
output = EncodingOutput(
|
|
60
|
+
success=True,
|
|
61
|
+
lossless_4k_mp4_path="/output/video.mp4",
|
|
62
|
+
encoding_backend="local"
|
|
63
|
+
)
|
|
64
|
+
assert output.success is True
|
|
65
|
+
assert output.error_message is None
|
|
66
|
+
|
|
67
|
+
def test_failure_output(self):
|
|
68
|
+
"""Test failure output."""
|
|
69
|
+
output = EncodingOutput(
|
|
70
|
+
success=False,
|
|
71
|
+
error_message="Encoding failed",
|
|
72
|
+
encoding_backend="gce"
|
|
73
|
+
)
|
|
74
|
+
assert output.success is False
|
|
75
|
+
assert output.error_message == "Encoding failed"
|
|
76
|
+
|
|
77
|
+
def test_output_files_dict(self):
|
|
78
|
+
"""Test output_files dictionary."""
|
|
79
|
+
output = EncodingOutput(
|
|
80
|
+
success=True,
|
|
81
|
+
output_files={
|
|
82
|
+
"lossless_4k_mp4": "/output/lossless.mp4",
|
|
83
|
+
"720p_mp4": "/output/720p.mp4"
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
assert "lossless_4k_mp4" in output.output_files
|
|
87
|
+
assert output.output_files["720p_mp4"] == "/output/720p.mp4"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestLocalEncodingBackend:
|
|
91
|
+
"""Test LocalEncodingBackend implementation."""
|
|
92
|
+
|
|
93
|
+
def test_name(self):
|
|
94
|
+
"""Test backend name."""
|
|
95
|
+
backend = LocalEncodingBackend()
|
|
96
|
+
assert backend.name == "local"
|
|
97
|
+
|
|
98
|
+
def test_init_with_dry_run(self):
|
|
99
|
+
"""Test initialization with dry run."""
|
|
100
|
+
backend = LocalEncodingBackend(dry_run=True)
|
|
101
|
+
assert backend.dry_run is True
|
|
102
|
+
|
|
103
|
+
@pytest.mark.asyncio
|
|
104
|
+
@patch("subprocess.run")
|
|
105
|
+
async def test_is_available_success(self, mock_run):
|
|
106
|
+
"""Test availability check when FFmpeg is installed."""
|
|
107
|
+
mock_run.return_value = MagicMock(returncode=0)
|
|
108
|
+
|
|
109
|
+
backend = LocalEncodingBackend()
|
|
110
|
+
available = await backend.is_available()
|
|
111
|
+
|
|
112
|
+
assert available is True
|
|
113
|
+
|
|
114
|
+
@pytest.mark.asyncio
|
|
115
|
+
@patch("subprocess.run")
|
|
116
|
+
async def test_is_available_not_found(self, mock_run):
|
|
117
|
+
"""Test availability check when FFmpeg is not installed."""
|
|
118
|
+
mock_run.side_effect = FileNotFoundError()
|
|
119
|
+
|
|
120
|
+
backend = LocalEncodingBackend()
|
|
121
|
+
available = await backend.is_available()
|
|
122
|
+
|
|
123
|
+
assert available is False
|
|
124
|
+
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
@patch.object(LocalEncodingBackend, "is_available")
|
|
127
|
+
@patch.object(LocalEncodingBackend, "_get_service")
|
|
128
|
+
async def test_get_status(self, mock_get_service, mock_is_available):
|
|
129
|
+
"""Test status retrieval."""
|
|
130
|
+
mock_is_available.return_value = True
|
|
131
|
+
mock_service = MagicMock()
|
|
132
|
+
mock_service.hwaccel_available = True
|
|
133
|
+
mock_service.video_encoder = "h264_nvenc"
|
|
134
|
+
mock_get_service.return_value = mock_service
|
|
135
|
+
|
|
136
|
+
backend = LocalEncodingBackend()
|
|
137
|
+
status = await backend.get_status()
|
|
138
|
+
|
|
139
|
+
assert status["backend"] == "local"
|
|
140
|
+
assert status["available"] is True
|
|
141
|
+
|
|
142
|
+
@pytest.mark.asyncio
|
|
143
|
+
@patch("asyncio.to_thread")
|
|
144
|
+
@patch.object(LocalEncodingBackend, "_get_service")
|
|
145
|
+
async def test_encode_success(self, mock_get_service, mock_to_thread):
|
|
146
|
+
"""Test successful encoding."""
|
|
147
|
+
from backend.services.local_encoding_service import EncodingResult
|
|
148
|
+
|
|
149
|
+
mock_result = EncodingResult(
|
|
150
|
+
success=True,
|
|
151
|
+
output_files={"key": "/path/file.mp4"}
|
|
152
|
+
)
|
|
153
|
+
mock_to_thread.return_value = mock_result
|
|
154
|
+
|
|
155
|
+
mock_service = MagicMock()
|
|
156
|
+
mock_get_service.return_value = mock_service
|
|
157
|
+
|
|
158
|
+
backend = LocalEncodingBackend()
|
|
159
|
+
input_config = EncodingInput(
|
|
160
|
+
title_video_path="/input/title.mov",
|
|
161
|
+
karaoke_video_path="/input/karaoke.mov",
|
|
162
|
+
instrumental_audio_path="/input/audio.flac",
|
|
163
|
+
artist="Test Artist",
|
|
164
|
+
title="Test Title",
|
|
165
|
+
output_dir="/output"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
output = await backend.encode(input_config)
|
|
169
|
+
|
|
170
|
+
assert output.success is True
|
|
171
|
+
assert output.encoding_backend == "local"
|
|
172
|
+
assert output.encoding_time_seconds is not None
|
|
173
|
+
|
|
174
|
+
@pytest.mark.asyncio
|
|
175
|
+
@patch("asyncio.to_thread")
|
|
176
|
+
@patch.object(LocalEncodingBackend, "_get_service")
|
|
177
|
+
async def test_encode_failure(self, mock_get_service, mock_to_thread):
|
|
178
|
+
"""Test encoding failure."""
|
|
179
|
+
from backend.services.local_encoding_service import EncodingResult
|
|
180
|
+
|
|
181
|
+
mock_result = EncodingResult(
|
|
182
|
+
success=False,
|
|
183
|
+
output_files={},
|
|
184
|
+
error="FFmpeg failed"
|
|
185
|
+
)
|
|
186
|
+
mock_to_thread.return_value = mock_result
|
|
187
|
+
|
|
188
|
+
mock_service = MagicMock()
|
|
189
|
+
mock_get_service.return_value = mock_service
|
|
190
|
+
|
|
191
|
+
backend = LocalEncodingBackend()
|
|
192
|
+
input_config = EncodingInput(
|
|
193
|
+
title_video_path="/input/title.mov",
|
|
194
|
+
karaoke_video_path="/input/karaoke.mov",
|
|
195
|
+
instrumental_audio_path="/input/audio.flac",
|
|
196
|
+
artist="Test",
|
|
197
|
+
title="Test",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
output = await backend.encode(input_config)
|
|
201
|
+
|
|
202
|
+
assert output.success is False
|
|
203
|
+
assert output.error_message == "FFmpeg failed"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class TestGCEEncodingBackend:
|
|
207
|
+
"""Test GCEEncodingBackend implementation."""
|
|
208
|
+
|
|
209
|
+
def test_name(self):
|
|
210
|
+
"""Test backend name."""
|
|
211
|
+
backend = GCEEncodingBackend()
|
|
212
|
+
assert backend.name == "gce"
|
|
213
|
+
|
|
214
|
+
@patch.object(GCEEncodingBackend, "_get_service")
|
|
215
|
+
@pytest.mark.asyncio
|
|
216
|
+
async def test_is_available_enabled(self, mock_get_service):
|
|
217
|
+
"""Test availability when GCE is enabled."""
|
|
218
|
+
mock_service = MagicMock()
|
|
219
|
+
mock_service.is_enabled = True
|
|
220
|
+
mock_get_service.return_value = mock_service
|
|
221
|
+
|
|
222
|
+
backend = GCEEncodingBackend()
|
|
223
|
+
available = await backend.is_available()
|
|
224
|
+
|
|
225
|
+
assert available is True
|
|
226
|
+
|
|
227
|
+
@patch.object(GCEEncodingBackend, "_get_service")
|
|
228
|
+
@pytest.mark.asyncio
|
|
229
|
+
async def test_is_available_disabled(self, mock_get_service):
|
|
230
|
+
"""Test availability when GCE is disabled."""
|
|
231
|
+
mock_service = MagicMock()
|
|
232
|
+
mock_service.is_enabled = False
|
|
233
|
+
mock_get_service.return_value = mock_service
|
|
234
|
+
|
|
235
|
+
backend = GCEEncodingBackend()
|
|
236
|
+
available = await backend.is_available()
|
|
237
|
+
|
|
238
|
+
assert available is False
|
|
239
|
+
|
|
240
|
+
@patch.object(GCEEncodingBackend, "_get_service")
|
|
241
|
+
@pytest.mark.asyncio
|
|
242
|
+
async def test_encode_missing_gcs_paths(self, mock_get_service):
|
|
243
|
+
"""Test encoding fails without GCS paths."""
|
|
244
|
+
backend = GCEEncodingBackend()
|
|
245
|
+
input_config = EncodingInput(
|
|
246
|
+
title_video_path="/input/title.mov",
|
|
247
|
+
karaoke_video_path="/input/karaoke.mov",
|
|
248
|
+
instrumental_audio_path="/input/audio.flac",
|
|
249
|
+
# Missing GCS paths in options
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
output = await backend.encode(input_config)
|
|
253
|
+
|
|
254
|
+
assert output.success is False
|
|
255
|
+
assert "gcs_path" in output.error_message.lower()
|
|
256
|
+
|
|
257
|
+
@patch.object(GCEEncodingBackend, "_get_service")
|
|
258
|
+
@pytest.mark.asyncio
|
|
259
|
+
async def test_encode_success(self, mock_get_service):
|
|
260
|
+
"""Test successful GCE encoding."""
|
|
261
|
+
mock_service = MagicMock()
|
|
262
|
+
mock_service.encode_videos = AsyncMock(return_value={
|
|
263
|
+
"status": "complete",
|
|
264
|
+
"output_files": {
|
|
265
|
+
"mp4_4k_lossless": "gs://bucket/output/lossless.mp4",
|
|
266
|
+
"mp4_720p": "gs://bucket/output/720p.mp4",
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
mock_get_service.return_value = mock_service
|
|
270
|
+
|
|
271
|
+
backend = GCEEncodingBackend()
|
|
272
|
+
input_config = EncodingInput(
|
|
273
|
+
title_video_path="/input/title.mov",
|
|
274
|
+
karaoke_video_path="/input/karaoke.mov",
|
|
275
|
+
instrumental_audio_path="/input/audio.flac",
|
|
276
|
+
artist="Test Artist",
|
|
277
|
+
title="Test Title",
|
|
278
|
+
options={
|
|
279
|
+
"job_id": "test-job",
|
|
280
|
+
"input_gcs_path": "gs://bucket/input/",
|
|
281
|
+
"output_gcs_path": "gs://bucket/output/",
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
output = await backend.encode(input_config)
|
|
286
|
+
|
|
287
|
+
assert output.success is True
|
|
288
|
+
assert output.encoding_backend == "gce"
|
|
289
|
+
mock_service.encode_videos.assert_called_once()
|
|
290
|
+
|
|
291
|
+
@patch.object(GCEEncodingBackend, "_get_service")
|
|
292
|
+
@pytest.mark.asyncio
|
|
293
|
+
async def test_encode_failure(self, mock_get_service):
|
|
294
|
+
"""Test GCE encoding failure handling."""
|
|
295
|
+
mock_service = MagicMock()
|
|
296
|
+
mock_service.encode_videos = AsyncMock(side_effect=Exception("GCE worker error"))
|
|
297
|
+
mock_get_service.return_value = mock_service
|
|
298
|
+
|
|
299
|
+
backend = GCEEncodingBackend()
|
|
300
|
+
input_config = EncodingInput(
|
|
301
|
+
title_video_path="/input/title.mov",
|
|
302
|
+
karaoke_video_path="/input/karaoke.mov",
|
|
303
|
+
instrumental_audio_path="/input/audio.flac",
|
|
304
|
+
options={
|
|
305
|
+
"job_id": "test-job",
|
|
306
|
+
"input_gcs_path": "gs://bucket/input/",
|
|
307
|
+
"output_gcs_path": "gs://bucket/output/",
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
output = await backend.encode(input_config)
|
|
312
|
+
|
|
313
|
+
assert output.success is False
|
|
314
|
+
assert "GCE worker error" in output.error_message
|
|
315
|
+
|
|
316
|
+
@patch.object(GCEEncodingBackend, "_get_service")
|
|
317
|
+
@pytest.mark.asyncio
|
|
318
|
+
async def test_encode_handles_list_result(self, mock_get_service):
|
|
319
|
+
"""Test GCE encoding handles list response gracefully.
|
|
320
|
+
|
|
321
|
+
This would have caught: 'list' object has no attribute 'get' error
|
|
322
|
+
when GCE worker returns a list instead of a dict.
|
|
323
|
+
"""
|
|
324
|
+
mock_service = MagicMock()
|
|
325
|
+
# Simulate GCE worker returning a list instead of dict
|
|
326
|
+
mock_service.encode_videos = AsyncMock(return_value=[
|
|
327
|
+
{"output_files": {"mp4_4k_lossless": "gs://bucket/output/lossless.mp4"}}
|
|
328
|
+
])
|
|
329
|
+
mock_get_service.return_value = mock_service
|
|
330
|
+
|
|
331
|
+
backend = GCEEncodingBackend()
|
|
332
|
+
input_config = EncodingInput(
|
|
333
|
+
title_video_path="/input/title.mov",
|
|
334
|
+
karaoke_video_path="/input/karaoke.mov",
|
|
335
|
+
instrumental_audio_path="/input/audio.flac",
|
|
336
|
+
options={
|
|
337
|
+
"job_id": "test-job",
|
|
338
|
+
"input_gcs_path": "gs://bucket/input/",
|
|
339
|
+
"output_gcs_path": "gs://bucket/output/",
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# This should not raise an error
|
|
344
|
+
output = await backend.encode(input_config)
|
|
345
|
+
|
|
346
|
+
# Should still succeed by extracting from the list
|
|
347
|
+
assert output.success is True
|
|
348
|
+
assert output.lossless_4k_mp4_path == "gs://bucket/output/lossless.mp4"
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class TestGetEncodingBackend:
|
|
352
|
+
"""Test encoding backend factory function."""
|
|
353
|
+
|
|
354
|
+
def test_get_local_backend(self):
|
|
355
|
+
"""Test getting local backend."""
|
|
356
|
+
backend = get_encoding_backend("local")
|
|
357
|
+
assert isinstance(backend, LocalEncodingBackend)
|
|
358
|
+
assert backend.name == "local"
|
|
359
|
+
|
|
360
|
+
@patch.object(GCEEncodingBackend, "_get_service")
|
|
361
|
+
def test_get_auto_backend_gce_disabled(self, mock_get_service):
|
|
362
|
+
"""Test getting auto backend falls back to local when GCE disabled."""
|
|
363
|
+
mock_service = MagicMock()
|
|
364
|
+
mock_service.is_enabled = False
|
|
365
|
+
mock_get_service.return_value = mock_service
|
|
366
|
+
|
|
367
|
+
backend = get_encoding_backend("auto")
|
|
368
|
+
assert isinstance(backend, LocalEncodingBackend)
|
|
369
|
+
|
|
370
|
+
def test_get_local_backend_with_options(self):
|
|
371
|
+
"""Test getting local backend with options."""
|
|
372
|
+
backend = get_encoding_backend("local", dry_run=True)
|
|
373
|
+
assert backend.dry_run is True
|
|
374
|
+
|
|
375
|
+
def test_get_gce_backend(self):
|
|
376
|
+
"""Test getting GCE backend."""
|
|
377
|
+
backend = get_encoding_backend("gce")
|
|
378
|
+
assert isinstance(backend, GCEEncodingBackend)
|
|
379
|
+
assert backend.name == "gce"
|
|
380
|
+
|
|
381
|
+
def test_get_unknown_backend_raises(self):
|
|
382
|
+
"""Test that unknown backend raises ValueError."""
|
|
383
|
+
with pytest.raises(ValueError) as exc_info:
|
|
384
|
+
get_encoding_backend("unknown")
|
|
385
|
+
assert "Unknown encoding backend type" in str(exc_info.value)
|
|
386
|
+
|
|
387
|
+
def test_get_gce_backend_with_options(self):
|
|
388
|
+
"""Test getting GCE backend with common options like dry_run.
|
|
389
|
+
|
|
390
|
+
This ensures all backends accept the same kwargs, preventing
|
|
391
|
+
errors when get_encoding_backend() passes **kwargs to different backends.
|
|
392
|
+
"""
|
|
393
|
+
# This would have caught: GCEEncodingBackend.__init__() got an unexpected keyword argument 'dry_run'
|
|
394
|
+
backend = get_encoding_backend("gce", dry_run=True)
|
|
395
|
+
assert isinstance(backend, GCEEncodingBackend)
|
|
396
|
+
assert backend.dry_run is True
|
|
397
|
+
|
|
398
|
+
@patch.object(GCEEncodingBackend, "_get_service")
|
|
399
|
+
def test_all_backends_accept_dry_run(self, mock_get_service):
|
|
400
|
+
"""Test that all backend types accept dry_run parameter.
|
|
401
|
+
|
|
402
|
+
This is an integration test to ensure the factory function
|
|
403
|
+
can pass dry_run to any backend without TypeError.
|
|
404
|
+
"""
|
|
405
|
+
mock_service = MagicMock()
|
|
406
|
+
mock_service.is_enabled = False
|
|
407
|
+
mock_get_service.return_value = mock_service
|
|
408
|
+
|
|
409
|
+
for backend_type in ["local", "gce", "auto"]:
|
|
410
|
+
# This should not raise TypeError
|
|
411
|
+
backend = get_encoding_backend(backend_type, dry_run=True)
|
|
412
|
+
assert backend is not None
|