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,423 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for LocalEncodingService.
|
|
3
|
+
|
|
4
|
+
Tests cover:
|
|
5
|
+
- Service initialization
|
|
6
|
+
- Hardware acceleration detection
|
|
7
|
+
- FFmpeg command execution
|
|
8
|
+
- Individual encoding methods
|
|
9
|
+
- Full encoding pipeline
|
|
10
|
+
- Dry run mode
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import tempfile
|
|
15
|
+
import pytest
|
|
16
|
+
from unittest.mock import MagicMock, patch, call
|
|
17
|
+
|
|
18
|
+
from backend.services.local_encoding_service import (
|
|
19
|
+
LocalEncodingService,
|
|
20
|
+
EncodingConfig,
|
|
21
|
+
EncodingResult,
|
|
22
|
+
get_local_encoding_service,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestLocalEncodingServiceInit:
|
|
27
|
+
"""Test service initialization."""
|
|
28
|
+
|
|
29
|
+
def test_init_default_values(self):
|
|
30
|
+
"""Test default initialization."""
|
|
31
|
+
service = LocalEncodingService()
|
|
32
|
+
assert service.dry_run is False
|
|
33
|
+
assert "ffmpeg" in service._ffmpeg_base_command
|
|
34
|
+
|
|
35
|
+
def test_init_with_dry_run(self):
|
|
36
|
+
"""Test initialization with dry run mode."""
|
|
37
|
+
service = LocalEncodingService(dry_run=True)
|
|
38
|
+
assert service.dry_run is True
|
|
39
|
+
|
|
40
|
+
def test_init_with_debug_logging(self):
|
|
41
|
+
"""Test initialization with debug logging level."""
|
|
42
|
+
import logging
|
|
43
|
+
service = LocalEncodingService(log_level=logging.DEBUG)
|
|
44
|
+
assert "-loglevel verbose" in service._ffmpeg_base_command
|
|
45
|
+
|
|
46
|
+
def test_init_with_info_logging(self):
|
|
47
|
+
"""Test initialization with info logging level."""
|
|
48
|
+
import logging
|
|
49
|
+
service = LocalEncodingService(log_level=logging.INFO)
|
|
50
|
+
assert "-loglevel fatal" in service._ffmpeg_base_command
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestLocalEncodingServiceHWAccel:
|
|
54
|
+
"""Test hardware acceleration detection."""
|
|
55
|
+
|
|
56
|
+
@patch("subprocess.run")
|
|
57
|
+
def test_detect_nvenc_available(self, mock_run):
|
|
58
|
+
"""Test NVENC detection when available."""
|
|
59
|
+
mock_run.return_value = MagicMock(returncode=0)
|
|
60
|
+
|
|
61
|
+
service = LocalEncodingService()
|
|
62
|
+
# Force detection
|
|
63
|
+
service._detect_and_set_hwaccel()
|
|
64
|
+
|
|
65
|
+
assert service._hwaccel_available is True
|
|
66
|
+
assert service._video_encoder == "h264_nvenc"
|
|
67
|
+
assert service._scale_filter == "scale_cuda"
|
|
68
|
+
|
|
69
|
+
@patch("subprocess.run")
|
|
70
|
+
def test_detect_no_hwaccel(self, mock_run):
|
|
71
|
+
"""Test fallback when no hardware acceleration available."""
|
|
72
|
+
import subprocess
|
|
73
|
+
mock_run.side_effect = subprocess.CalledProcessError(1, "test")
|
|
74
|
+
|
|
75
|
+
service = LocalEncodingService()
|
|
76
|
+
# Force detection
|
|
77
|
+
service._detect_and_set_hwaccel()
|
|
78
|
+
|
|
79
|
+
assert service._hwaccel_available is False
|
|
80
|
+
assert service._video_encoder == "libx264"
|
|
81
|
+
assert service._scale_filter == "scale"
|
|
82
|
+
|
|
83
|
+
def test_nvenc_quality_settings(self):
|
|
84
|
+
"""Test NVENC quality settings for different presets."""
|
|
85
|
+
service = LocalEncodingService()
|
|
86
|
+
service._hwaccel_available = True
|
|
87
|
+
|
|
88
|
+
lossless = service._get_nvenc_quality_settings("lossless")
|
|
89
|
+
assert "cq 0" in lossless
|
|
90
|
+
|
|
91
|
+
medium = service._get_nvenc_quality_settings("medium")
|
|
92
|
+
assert "p4" in medium
|
|
93
|
+
|
|
94
|
+
def test_nvenc_quality_settings_disabled(self):
|
|
95
|
+
"""Test NVENC quality settings when hwaccel is disabled."""
|
|
96
|
+
service = LocalEncodingService()
|
|
97
|
+
service._hwaccel_available = False
|
|
98
|
+
|
|
99
|
+
settings = service._get_nvenc_quality_settings("lossless")
|
|
100
|
+
assert settings == ""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestLocalEncodingServiceExecuteCommand:
|
|
104
|
+
"""Test command execution."""
|
|
105
|
+
|
|
106
|
+
@patch("subprocess.run")
|
|
107
|
+
def test_execute_command_success(self, mock_run):
|
|
108
|
+
"""Test successful command execution."""
|
|
109
|
+
mock_run.return_value = MagicMock(returncode=0)
|
|
110
|
+
|
|
111
|
+
service = LocalEncodingService()
|
|
112
|
+
result = service._execute_command("echo test", "Test command")
|
|
113
|
+
|
|
114
|
+
assert result is True
|
|
115
|
+
mock_run.assert_called_once()
|
|
116
|
+
|
|
117
|
+
@patch("subprocess.run")
|
|
118
|
+
def test_execute_command_failure(self, mock_run):
|
|
119
|
+
"""Test command execution failure."""
|
|
120
|
+
import subprocess
|
|
121
|
+
mock_run.side_effect = subprocess.CalledProcessError(
|
|
122
|
+
1, "test", stderr="Error message"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
service = LocalEncodingService()
|
|
126
|
+
result = service._execute_command("echo test", "Test command")
|
|
127
|
+
|
|
128
|
+
assert result is False
|
|
129
|
+
|
|
130
|
+
@patch("subprocess.run")
|
|
131
|
+
def test_execute_command_timeout(self, mock_run):
|
|
132
|
+
"""Test command execution timeout."""
|
|
133
|
+
import subprocess
|
|
134
|
+
mock_run.side_effect = subprocess.TimeoutExpired("test", 30)
|
|
135
|
+
|
|
136
|
+
service = LocalEncodingService()
|
|
137
|
+
result = service._execute_command("echo test", "Test command", timeout=30)
|
|
138
|
+
|
|
139
|
+
assert result is False
|
|
140
|
+
|
|
141
|
+
def test_execute_command_dry_run(self):
|
|
142
|
+
"""Test command execution in dry run mode."""
|
|
143
|
+
service = LocalEncodingService(dry_run=True)
|
|
144
|
+
result = service._execute_command("echo test", "Test command")
|
|
145
|
+
|
|
146
|
+
assert result is True
|
|
147
|
+
# No actual subprocess should be called in dry run mode
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TestLocalEncodingServiceEncodingMethods:
|
|
151
|
+
"""Test individual encoding methods."""
|
|
152
|
+
|
|
153
|
+
@patch.object(LocalEncodingService, "_execute_command")
|
|
154
|
+
def test_remux_with_instrumental(self, mock_execute):
|
|
155
|
+
"""Test remuxing with instrumental audio."""
|
|
156
|
+
mock_execute.return_value = True
|
|
157
|
+
|
|
158
|
+
service = LocalEncodingService()
|
|
159
|
+
result = service.remux_with_instrumental(
|
|
160
|
+
"/input/video.mov",
|
|
161
|
+
"/input/audio.flac",
|
|
162
|
+
"/output/video.mp4"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
assert result is True
|
|
166
|
+
mock_execute.assert_called_once()
|
|
167
|
+
call_args = mock_execute.call_args[0][0]
|
|
168
|
+
assert "/input/video.mov" in call_args
|
|
169
|
+
assert "/input/audio.flac" in call_args
|
|
170
|
+
assert "-map 0:v -map 1:a" in call_args
|
|
171
|
+
|
|
172
|
+
@patch.object(LocalEncodingService, "_execute_command_with_fallback")
|
|
173
|
+
def test_convert_mov_to_mp4(self, mock_execute):
|
|
174
|
+
"""Test MOV to MP4 conversion."""
|
|
175
|
+
mock_execute.return_value = True
|
|
176
|
+
|
|
177
|
+
service = LocalEncodingService()
|
|
178
|
+
result = service.convert_mov_to_mp4(
|
|
179
|
+
"/input/video.mov",
|
|
180
|
+
"/output/video.mp4"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
assert result is True
|
|
184
|
+
mock_execute.assert_called_once()
|
|
185
|
+
|
|
186
|
+
@patch.object(LocalEncodingService, "_execute_command_with_fallback")
|
|
187
|
+
def test_encode_lossless_mp4_without_end(self, mock_execute):
|
|
188
|
+
"""Test lossless 4K MP4 encoding without end credits."""
|
|
189
|
+
mock_execute.return_value = True
|
|
190
|
+
|
|
191
|
+
service = LocalEncodingService()
|
|
192
|
+
result = service.encode_lossless_mp4(
|
|
193
|
+
"/input/title.mov",
|
|
194
|
+
"/input/karaoke.mp4",
|
|
195
|
+
"/output/lossless.mp4"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
assert result is True
|
|
199
|
+
mock_execute.assert_called_once()
|
|
200
|
+
call_args = mock_execute.call_args
|
|
201
|
+
assert "concat=n=2" in call_args[0][0] # GPU command
|
|
202
|
+
|
|
203
|
+
@patch.object(LocalEncodingService, "_execute_command_with_fallback")
|
|
204
|
+
def test_encode_lossless_mp4_with_end(self, mock_execute):
|
|
205
|
+
"""Test lossless 4K MP4 encoding with end credits."""
|
|
206
|
+
mock_execute.return_value = True
|
|
207
|
+
|
|
208
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
209
|
+
end_file = os.path.join(tmpdir, "end.mov")
|
|
210
|
+
with open(end_file, "w") as f:
|
|
211
|
+
f.write("fake video")
|
|
212
|
+
|
|
213
|
+
service = LocalEncodingService()
|
|
214
|
+
result = service.encode_lossless_mp4(
|
|
215
|
+
"/input/title.mov",
|
|
216
|
+
"/input/karaoke.mp4",
|
|
217
|
+
"/output/lossless.mp4",
|
|
218
|
+
end_video=end_file
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
assert result is True
|
|
222
|
+
call_args = mock_execute.call_args
|
|
223
|
+
assert "concat=n=3" in call_args[0][0] # 3 videos
|
|
224
|
+
|
|
225
|
+
@patch.object(LocalEncodingService, "_execute_command")
|
|
226
|
+
def test_encode_lossy_mp4(self, mock_execute):
|
|
227
|
+
"""Test lossy 4K MP4 encoding."""
|
|
228
|
+
mock_execute.return_value = True
|
|
229
|
+
|
|
230
|
+
service = LocalEncodingService()
|
|
231
|
+
result = service.encode_lossy_mp4(
|
|
232
|
+
"/input/lossless.mp4",
|
|
233
|
+
"/output/lossy.mp4"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
assert result is True
|
|
237
|
+
call_args = mock_execute.call_args[0][0]
|
|
238
|
+
assert "-c:v copy" in call_args
|
|
239
|
+
assert "aac" in call_args.lower()
|
|
240
|
+
|
|
241
|
+
@patch.object(LocalEncodingService, "_execute_command")
|
|
242
|
+
def test_encode_lossless_mkv(self, mock_execute):
|
|
243
|
+
"""Test MKV encoding with FLAC audio."""
|
|
244
|
+
mock_execute.return_value = True
|
|
245
|
+
|
|
246
|
+
service = LocalEncodingService()
|
|
247
|
+
result = service.encode_lossless_mkv(
|
|
248
|
+
"/input/lossless.mp4",
|
|
249
|
+
"/output/video.mkv"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
assert result is True
|
|
253
|
+
call_args = mock_execute.call_args[0][0]
|
|
254
|
+
assert "-c:v copy" in call_args
|
|
255
|
+
assert "-c:a flac" in call_args
|
|
256
|
+
|
|
257
|
+
@patch.object(LocalEncodingService, "_execute_command_with_fallback")
|
|
258
|
+
def test_encode_720p(self, mock_execute):
|
|
259
|
+
"""Test 720p encoding."""
|
|
260
|
+
mock_execute.return_value = True
|
|
261
|
+
|
|
262
|
+
service = LocalEncodingService()
|
|
263
|
+
result = service.encode_720p(
|
|
264
|
+
"/input/lossless.mp4",
|
|
265
|
+
"/output/720p.mp4"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
assert result is True
|
|
269
|
+
call_args = mock_execute.call_args
|
|
270
|
+
# Should contain scale filter
|
|
271
|
+
assert "1280:720" in call_args[0][0] or "1280:720" in call_args[0][1]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class TestLocalEncodingServiceFullPipeline:
|
|
275
|
+
"""Test full encoding pipeline."""
|
|
276
|
+
|
|
277
|
+
@patch.object(LocalEncodingService, "remux_with_instrumental")
|
|
278
|
+
@patch.object(LocalEncodingService, "convert_mov_to_mp4")
|
|
279
|
+
@patch.object(LocalEncodingService, "encode_lossless_mp4")
|
|
280
|
+
@patch.object(LocalEncodingService, "encode_lossy_mp4")
|
|
281
|
+
@patch.object(LocalEncodingService, "encode_lossless_mkv")
|
|
282
|
+
@patch.object(LocalEncodingService, "encode_720p")
|
|
283
|
+
def test_encode_all_formats_success(
|
|
284
|
+
self, mock_720p, mock_mkv, mock_lossy, mock_lossless, mock_convert, mock_remux
|
|
285
|
+
):
|
|
286
|
+
"""Test successful full encoding pipeline."""
|
|
287
|
+
mock_remux.return_value = True
|
|
288
|
+
mock_convert.return_value = True
|
|
289
|
+
mock_lossless.return_value = True
|
|
290
|
+
mock_lossy.return_value = True
|
|
291
|
+
mock_mkv.return_value = True
|
|
292
|
+
mock_720p.return_value = True
|
|
293
|
+
|
|
294
|
+
service = LocalEncodingService()
|
|
295
|
+
config = EncodingConfig(
|
|
296
|
+
title_video="/input/title.mov",
|
|
297
|
+
karaoke_video="/input/karaoke.mov",
|
|
298
|
+
instrumental_audio="/input/instrumental.flac",
|
|
299
|
+
output_karaoke_mp4="/output/karaoke.mp4",
|
|
300
|
+
output_with_vocals_mp4="/output/with_vocals.mp4",
|
|
301
|
+
output_lossless_4k_mp4="/output/lossless_4k.mp4",
|
|
302
|
+
output_lossy_4k_mp4="/output/lossy_4k.mp4",
|
|
303
|
+
output_lossless_mkv="/output/lossless.mkv",
|
|
304
|
+
output_720p_mp4="/output/720p.mp4",
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
result = service.encode_all_formats(config)
|
|
308
|
+
|
|
309
|
+
assert result.success is True
|
|
310
|
+
assert "karaoke_mp4" in result.output_files
|
|
311
|
+
assert "lossless_4k_mp4" in result.output_files
|
|
312
|
+
assert "720p_mp4" in result.output_files
|
|
313
|
+
|
|
314
|
+
@patch.object(LocalEncodingService, "remux_with_instrumental")
|
|
315
|
+
def test_encode_all_formats_failure_early(self, mock_remux):
|
|
316
|
+
"""Test encoding pipeline failure at early step."""
|
|
317
|
+
mock_remux.return_value = False
|
|
318
|
+
|
|
319
|
+
service = LocalEncodingService()
|
|
320
|
+
config = EncodingConfig(
|
|
321
|
+
title_video="/input/title.mov",
|
|
322
|
+
karaoke_video="/input/karaoke.mov",
|
|
323
|
+
instrumental_audio="/input/instrumental.flac",
|
|
324
|
+
output_karaoke_mp4="/output/karaoke.mp4",
|
|
325
|
+
output_lossless_4k_mp4="/output/lossless_4k.mp4",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
result = service.encode_all_formats(config)
|
|
329
|
+
|
|
330
|
+
assert result.success is False
|
|
331
|
+
assert "Failed to remux" in result.error
|
|
332
|
+
|
|
333
|
+
def test_encode_all_formats_dry_run(self):
|
|
334
|
+
"""Test encoding pipeline in dry run mode."""
|
|
335
|
+
service = LocalEncodingService(dry_run=True)
|
|
336
|
+
config = EncodingConfig(
|
|
337
|
+
title_video="/input/title.mov",
|
|
338
|
+
karaoke_video="/input/karaoke.mp4", # Already MP4
|
|
339
|
+
instrumental_audio="/input/instrumental.flac",
|
|
340
|
+
output_karaoke_mp4="/output/karaoke.mp4",
|
|
341
|
+
output_lossless_4k_mp4="/output/lossless_4k.mp4",
|
|
342
|
+
output_lossy_4k_mp4="/output/lossy_4k.mp4",
|
|
343
|
+
output_lossless_mkv="/output/lossless.mkv",
|
|
344
|
+
output_720p_mp4="/output/720p.mp4",
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
result = service.encode_all_formats(config)
|
|
348
|
+
|
|
349
|
+
# In dry run mode, all operations should "succeed"
|
|
350
|
+
assert result.success is True
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class TestEncodingConfig:
|
|
354
|
+
"""Test EncodingConfig dataclass."""
|
|
355
|
+
|
|
356
|
+
def test_config_required_fields(self):
|
|
357
|
+
"""Test that required fields must be provided."""
|
|
358
|
+
config = EncodingConfig(
|
|
359
|
+
title_video="/path/title.mov",
|
|
360
|
+
karaoke_video="/path/karaoke.mov",
|
|
361
|
+
instrumental_audio="/path/audio.flac",
|
|
362
|
+
)
|
|
363
|
+
assert config.title_video == "/path/title.mov"
|
|
364
|
+
assert config.end_video is None # Optional field
|
|
365
|
+
|
|
366
|
+
def test_config_all_fields(self):
|
|
367
|
+
"""Test config with all fields."""
|
|
368
|
+
config = EncodingConfig(
|
|
369
|
+
title_video="/path/title.mov",
|
|
370
|
+
karaoke_video="/path/karaoke.mov",
|
|
371
|
+
instrumental_audio="/path/audio.flac",
|
|
372
|
+
end_video="/path/end.mov",
|
|
373
|
+
output_karaoke_mp4="/output/karaoke.mp4",
|
|
374
|
+
output_720p_mp4="/output/720p.mp4",
|
|
375
|
+
)
|
|
376
|
+
assert config.end_video == "/path/end.mov"
|
|
377
|
+
assert config.output_karaoke_mp4 == "/output/karaoke.mp4"
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class TestEncodingResult:
|
|
381
|
+
"""Test EncodingResult dataclass."""
|
|
382
|
+
|
|
383
|
+
def test_result_success(self):
|
|
384
|
+
"""Test successful result."""
|
|
385
|
+
result = EncodingResult(
|
|
386
|
+
success=True,
|
|
387
|
+
output_files={"key": "/path/file.mp4"}
|
|
388
|
+
)
|
|
389
|
+
assert result.success is True
|
|
390
|
+
assert result.error is None
|
|
391
|
+
|
|
392
|
+
def test_result_failure(self):
|
|
393
|
+
"""Test failure result."""
|
|
394
|
+
result = EncodingResult(
|
|
395
|
+
success=False,
|
|
396
|
+
output_files={},
|
|
397
|
+
error="Something went wrong"
|
|
398
|
+
)
|
|
399
|
+
assert result.success is False
|
|
400
|
+
assert result.error == "Something went wrong"
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class TestGetLocalEncodingService:
|
|
404
|
+
"""Test factory function."""
|
|
405
|
+
|
|
406
|
+
def test_get_service_creates_instance(self):
|
|
407
|
+
"""Test that factory function creates a new instance."""
|
|
408
|
+
import backend.services.local_encoding_service as module
|
|
409
|
+
module._local_encoding_service = None
|
|
410
|
+
|
|
411
|
+
service = get_local_encoding_service()
|
|
412
|
+
|
|
413
|
+
assert service is not None
|
|
414
|
+
assert isinstance(service, LocalEncodingService)
|
|
415
|
+
|
|
416
|
+
def test_get_service_with_dry_run(self):
|
|
417
|
+
"""Test factory function with dry run option."""
|
|
418
|
+
import backend.services.local_encoding_service as module
|
|
419
|
+
module._local_encoding_service = None
|
|
420
|
+
|
|
421
|
+
service = get_local_encoding_service(dry_run=True)
|
|
422
|
+
|
|
423
|
+
assert service.dry_run is True
|