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,405 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GCE Encoding Worker Service.
|
|
3
|
+
|
|
4
|
+
This service dispatches video encoding jobs to a dedicated high-performance
|
|
5
|
+
GCE instance (C4-standard with Intel Granite Rapids CPU) for faster encoding.
|
|
6
|
+
|
|
7
|
+
The GCE worker provides:
|
|
8
|
+
- 3.9 GHz all-core frequency (vs 3.7 GHz on Cloud Run)
|
|
9
|
+
- Dedicated vCPUs (no contention)
|
|
10
|
+
- 2-3x faster FFmpeg libx264 encoding
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
encoding_service = get_encoding_service()
|
|
14
|
+
if encoding_service.is_configured:
|
|
15
|
+
result = await encoding_service.encode_videos(job_id, input_gcs_path, config)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Optional, Dict, Any
|
|
21
|
+
|
|
22
|
+
import aiohttp
|
|
23
|
+
|
|
24
|
+
from backend.config import get_settings
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EncodingService:
|
|
30
|
+
"""Service for dispatching encoding jobs to GCE worker."""
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
self.settings = get_settings()
|
|
34
|
+
self._url = None
|
|
35
|
+
self._api_key = None
|
|
36
|
+
self._initialized = False
|
|
37
|
+
|
|
38
|
+
def _load_credentials(self):
|
|
39
|
+
"""Load encoding worker URL and API key from config/secrets."""
|
|
40
|
+
if self._initialized:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# Try environment variables first, then Secret Manager
|
|
44
|
+
self._url = self.settings.encoding_worker_url
|
|
45
|
+
self._api_key = self.settings.encoding_worker_api_key
|
|
46
|
+
|
|
47
|
+
# Fall back to Secret Manager
|
|
48
|
+
if not self._url:
|
|
49
|
+
self._url = self.settings.get_secret("encoding-worker-url")
|
|
50
|
+
if not self._api_key:
|
|
51
|
+
self._api_key = self.settings.get_secret("encoding-worker-api-key")
|
|
52
|
+
|
|
53
|
+
self._initialized = True
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_configured(self) -> bool:
|
|
57
|
+
"""Check if encoding service is configured with URL and API key."""
|
|
58
|
+
self._load_credentials()
|
|
59
|
+
return bool(self._url and self._api_key)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def is_enabled(self) -> bool:
|
|
63
|
+
"""Check if GCE encoding is enabled and configured."""
|
|
64
|
+
return self.settings.use_gce_encoding and self.is_configured
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def is_preview_enabled(self) -> bool:
|
|
68
|
+
"""Check if GCE preview encoding is enabled and configured."""
|
|
69
|
+
return self.settings.use_gce_preview_encoding and self.is_configured
|
|
70
|
+
|
|
71
|
+
async def submit_encoding_job(
|
|
72
|
+
self,
|
|
73
|
+
job_id: str,
|
|
74
|
+
input_gcs_path: str,
|
|
75
|
+
output_gcs_path: str,
|
|
76
|
+
encoding_config: Dict[str, Any],
|
|
77
|
+
) -> Dict[str, Any]:
|
|
78
|
+
"""
|
|
79
|
+
Submit an encoding job to the GCE worker.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
job_id: Unique job identifier
|
|
83
|
+
input_gcs_path: GCS path to input files (gs://bucket/path/)
|
|
84
|
+
output_gcs_path: GCS path for output files (gs://bucket/path/)
|
|
85
|
+
encoding_config: Configuration for encoding (formats, quality, etc.)
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Response from the encoding worker
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
Exception: If submission fails
|
|
92
|
+
"""
|
|
93
|
+
self._load_credentials()
|
|
94
|
+
|
|
95
|
+
if not self.is_configured:
|
|
96
|
+
raise RuntimeError("Encoding service not configured")
|
|
97
|
+
|
|
98
|
+
url = f"{self._url}/encode"
|
|
99
|
+
headers = {"X-API-Key": self._api_key, "Content-Type": "application/json"}
|
|
100
|
+
payload = {
|
|
101
|
+
"job_id": job_id,
|
|
102
|
+
"input_gcs_path": input_gcs_path,
|
|
103
|
+
"output_gcs_path": output_gcs_path,
|
|
104
|
+
"encoding_config": encoding_config,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
logger.info(f"[job:{job_id}] Submitting encoding job to GCE worker: {url}")
|
|
108
|
+
|
|
109
|
+
async with aiohttp.ClientSession() as session:
|
|
110
|
+
async with session.post(url, json=payload, headers=headers, timeout=30) as resp:
|
|
111
|
+
if resp.status == 401:
|
|
112
|
+
raise RuntimeError("Invalid API key for encoding worker")
|
|
113
|
+
if resp.status == 409:
|
|
114
|
+
raise RuntimeError(f"Encoding job {job_id} already exists")
|
|
115
|
+
if resp.status != 200:
|
|
116
|
+
text = await resp.text()
|
|
117
|
+
raise RuntimeError(f"Failed to submit encoding job: {resp.status} - {text}")
|
|
118
|
+
|
|
119
|
+
return await resp.json()
|
|
120
|
+
|
|
121
|
+
async def get_job_status(self, job_id: str) -> Dict[str, Any]:
|
|
122
|
+
"""
|
|
123
|
+
Get the status of an encoding job.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
job_id: Job identifier
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Job status including: status, progress, error, output_files
|
|
130
|
+
"""
|
|
131
|
+
self._load_credentials()
|
|
132
|
+
|
|
133
|
+
if not self.is_configured:
|
|
134
|
+
raise RuntimeError("Encoding service not configured")
|
|
135
|
+
|
|
136
|
+
url = f"{self._url}/status/{job_id}"
|
|
137
|
+
headers = {"X-API-Key": self._api_key}
|
|
138
|
+
|
|
139
|
+
async with aiohttp.ClientSession() as session:
|
|
140
|
+
async with session.get(url, headers=headers, timeout=30) as resp:
|
|
141
|
+
if resp.status == 401:
|
|
142
|
+
raise RuntimeError("Invalid API key for encoding worker")
|
|
143
|
+
if resp.status == 404:
|
|
144
|
+
raise RuntimeError(f"Encoding job {job_id} not found")
|
|
145
|
+
if resp.status != 200:
|
|
146
|
+
text = await resp.text()
|
|
147
|
+
raise RuntimeError(f"Failed to get job status: {resp.status} - {text}")
|
|
148
|
+
|
|
149
|
+
return await resp.json()
|
|
150
|
+
|
|
151
|
+
async def wait_for_completion(
|
|
152
|
+
self,
|
|
153
|
+
job_id: str,
|
|
154
|
+
poll_interval: float = 10.0,
|
|
155
|
+
timeout: float = 3600.0,
|
|
156
|
+
progress_callback=None,
|
|
157
|
+
) -> Dict[str, Any]:
|
|
158
|
+
"""
|
|
159
|
+
Poll for encoding job completion.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
job_id: Job identifier
|
|
163
|
+
poll_interval: Seconds between status checks
|
|
164
|
+
timeout: Maximum time to wait (default 1 hour)
|
|
165
|
+
progress_callback: Optional callback(progress: int) for progress updates
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Final job status with output files
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
TimeoutError: If job doesn't complete within timeout
|
|
172
|
+
RuntimeError: If job fails
|
|
173
|
+
"""
|
|
174
|
+
logger.info(f"[job:{job_id}] Waiting for GCE encoding to complete...")
|
|
175
|
+
|
|
176
|
+
start_time = asyncio.get_event_loop().time()
|
|
177
|
+
last_progress = 0
|
|
178
|
+
|
|
179
|
+
while True:
|
|
180
|
+
elapsed = asyncio.get_event_loop().time() - start_time
|
|
181
|
+
if elapsed > timeout:
|
|
182
|
+
raise TimeoutError(f"Encoding job {job_id} timed out after {timeout}s")
|
|
183
|
+
|
|
184
|
+
status = await self.get_job_status(job_id)
|
|
185
|
+
|
|
186
|
+
# Handle case where GCE worker returns a list instead of dict
|
|
187
|
+
if isinstance(status, list):
|
|
188
|
+
logger.warning(f"[job:{job_id}] GCE returned list instead of dict: {status}")
|
|
189
|
+
status = status[0] if status and isinstance(status[0], dict) else {}
|
|
190
|
+
if not isinstance(status, dict):
|
|
191
|
+
logger.error(f"[job:{job_id}] Unexpected status type: {type(status)}")
|
|
192
|
+
status = {}
|
|
193
|
+
|
|
194
|
+
job_status = status.get("status", "unknown")
|
|
195
|
+
progress = status.get("progress", 0)
|
|
196
|
+
|
|
197
|
+
# Report progress
|
|
198
|
+
if progress != last_progress:
|
|
199
|
+
logger.info(f"[job:{job_id}] Encoding progress: {progress}%")
|
|
200
|
+
last_progress = progress
|
|
201
|
+
if progress_callback:
|
|
202
|
+
try:
|
|
203
|
+
progress_callback(progress)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.warning(f"Progress callback failed: {e}")
|
|
206
|
+
|
|
207
|
+
if job_status == "complete":
|
|
208
|
+
logger.info(f"[job:{job_id}] GCE encoding complete in {elapsed:.1f}s")
|
|
209
|
+
return status
|
|
210
|
+
|
|
211
|
+
if job_status == "failed":
|
|
212
|
+
error = status.get("error", "Unknown error")
|
|
213
|
+
raise RuntimeError(f"Encoding job {job_id} failed: {error}")
|
|
214
|
+
|
|
215
|
+
await asyncio.sleep(poll_interval)
|
|
216
|
+
|
|
217
|
+
async def encode_videos(
|
|
218
|
+
self,
|
|
219
|
+
job_id: str,
|
|
220
|
+
input_gcs_path: str,
|
|
221
|
+
output_gcs_path: str,
|
|
222
|
+
encoding_config: Optional[Dict[str, Any]] = None,
|
|
223
|
+
progress_callback=None,
|
|
224
|
+
) -> Dict[str, Any]:
|
|
225
|
+
"""
|
|
226
|
+
Submit encoding job and wait for completion.
|
|
227
|
+
|
|
228
|
+
This is a convenience method that combines submit + wait.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
job_id: Unique job identifier
|
|
232
|
+
input_gcs_path: GCS path to input files
|
|
233
|
+
output_gcs_path: GCS path for output files
|
|
234
|
+
encoding_config: Optional encoding configuration
|
|
235
|
+
progress_callback: Optional callback for progress updates
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Final job status with output files
|
|
239
|
+
"""
|
|
240
|
+
config = encoding_config or {"formats": ["mp4_4k", "mp4_720p"]}
|
|
241
|
+
|
|
242
|
+
# Submit the job
|
|
243
|
+
await self.submit_encoding_job(job_id, input_gcs_path, output_gcs_path, config)
|
|
244
|
+
|
|
245
|
+
# Wait for completion
|
|
246
|
+
return await self.wait_for_completion(
|
|
247
|
+
job_id, progress_callback=progress_callback
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
async def submit_preview_encoding_job(
|
|
251
|
+
self,
|
|
252
|
+
job_id: str,
|
|
253
|
+
ass_gcs_path: str,
|
|
254
|
+
audio_gcs_path: str,
|
|
255
|
+
output_gcs_path: str,
|
|
256
|
+
background_color: str = "black",
|
|
257
|
+
background_image_gcs_path: Optional[str] = None,
|
|
258
|
+
font_gcs_path: Optional[str] = None,
|
|
259
|
+
) -> Dict[str, Any]:
|
|
260
|
+
"""
|
|
261
|
+
Submit a preview video encoding job to the GCE worker.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
job_id: Unique job identifier
|
|
265
|
+
ass_gcs_path: GCS path to ASS subtitles file (gs://bucket/path/file.ass)
|
|
266
|
+
audio_gcs_path: GCS path to audio file
|
|
267
|
+
output_gcs_path: GCS path for output video
|
|
268
|
+
background_color: Background color (default: black)
|
|
269
|
+
background_image_gcs_path: Optional GCS path to background image
|
|
270
|
+
font_gcs_path: Optional GCS path to custom font file
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Response from the encoding worker
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
Exception: If submission fails
|
|
277
|
+
"""
|
|
278
|
+
self._load_credentials()
|
|
279
|
+
|
|
280
|
+
if not self.is_configured:
|
|
281
|
+
raise RuntimeError("Encoding service not configured")
|
|
282
|
+
|
|
283
|
+
url = f"{self._url}/encode-preview"
|
|
284
|
+
headers = {"X-API-Key": self._api_key, "Content-Type": "application/json"}
|
|
285
|
+
payload = {
|
|
286
|
+
"job_id": job_id,
|
|
287
|
+
"ass_gcs_path": ass_gcs_path,
|
|
288
|
+
"audio_gcs_path": audio_gcs_path,
|
|
289
|
+
"output_gcs_path": output_gcs_path,
|
|
290
|
+
"background_color": background_color,
|
|
291
|
+
}
|
|
292
|
+
if background_image_gcs_path:
|
|
293
|
+
payload["background_image_gcs_path"] = background_image_gcs_path
|
|
294
|
+
if font_gcs_path:
|
|
295
|
+
payload["font_gcs_path"] = font_gcs_path
|
|
296
|
+
|
|
297
|
+
logger.info(f"[job:{job_id}] Submitting preview encoding job to GCE worker: {url}")
|
|
298
|
+
|
|
299
|
+
async with aiohttp.ClientSession() as session:
|
|
300
|
+
async with session.post(url, json=payload, headers=headers, timeout=30) as resp:
|
|
301
|
+
if resp.status == 401:
|
|
302
|
+
raise RuntimeError("Invalid API key for encoding worker")
|
|
303
|
+
if resp.status == 409:
|
|
304
|
+
raise RuntimeError(f"Preview encoding job {job_id} already exists")
|
|
305
|
+
if resp.status != 200:
|
|
306
|
+
text = await resp.text()
|
|
307
|
+
raise RuntimeError(f"Failed to submit preview encoding job: {resp.status} - {text}")
|
|
308
|
+
|
|
309
|
+
return await resp.json()
|
|
310
|
+
|
|
311
|
+
async def encode_preview_video(
|
|
312
|
+
self,
|
|
313
|
+
job_id: str,
|
|
314
|
+
ass_gcs_path: str,
|
|
315
|
+
audio_gcs_path: str,
|
|
316
|
+
output_gcs_path: str,
|
|
317
|
+
background_color: str = "black",
|
|
318
|
+
background_image_gcs_path: Optional[str] = None,
|
|
319
|
+
font_gcs_path: Optional[str] = None,
|
|
320
|
+
timeout: float = 90.0,
|
|
321
|
+
poll_interval: float = 2.0,
|
|
322
|
+
) -> Dict[str, Any]:
|
|
323
|
+
"""
|
|
324
|
+
Submit preview encoding job and wait for completion.
|
|
325
|
+
|
|
326
|
+
This is a convenience method that combines submit + wait with shorter
|
|
327
|
+
timeouts suitable for preview videos.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
job_id: Unique job identifier
|
|
331
|
+
ass_gcs_path: GCS path to ASS subtitles file
|
|
332
|
+
audio_gcs_path: GCS path to audio file
|
|
333
|
+
output_gcs_path: GCS path for output video
|
|
334
|
+
background_color: Background color (default: black)
|
|
335
|
+
background_image_gcs_path: Optional GCS path to background image
|
|
336
|
+
font_gcs_path: Optional GCS path to custom font file
|
|
337
|
+
timeout: Maximum time to wait (default 90s for preview)
|
|
338
|
+
poll_interval: Seconds between status checks (default 2s)
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Final job status with output files
|
|
342
|
+
"""
|
|
343
|
+
# Submit the job
|
|
344
|
+
submit_result = await self.submit_preview_encoding_job(
|
|
345
|
+
job_id=job_id,
|
|
346
|
+
ass_gcs_path=ass_gcs_path,
|
|
347
|
+
audio_gcs_path=audio_gcs_path,
|
|
348
|
+
output_gcs_path=output_gcs_path,
|
|
349
|
+
background_color=background_color,
|
|
350
|
+
background_image_gcs_path=background_image_gcs_path,
|
|
351
|
+
font_gcs_path=font_gcs_path,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# If cached, return immediately - video already exists in GCS
|
|
355
|
+
submit_status = submit_result.get("status")
|
|
356
|
+
if submit_status == "cached":
|
|
357
|
+
logger.info(f"[job:{job_id}] Preview already cached, returning immediately")
|
|
358
|
+
return {"status": "complete", "output_path": submit_result.get("output_path")}
|
|
359
|
+
|
|
360
|
+
# If in_progress, another request is encoding it - just wait for that
|
|
361
|
+
if submit_status == "in_progress":
|
|
362
|
+
logger.info(f"[job:{job_id}] Preview encoding already in progress, waiting")
|
|
363
|
+
|
|
364
|
+
# Wait for completion with shorter timeout
|
|
365
|
+
return await self.wait_for_completion(
|
|
366
|
+
job_id=job_id,
|
|
367
|
+
poll_interval=poll_interval,
|
|
368
|
+
timeout=timeout,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
async def health_check(self) -> Dict[str, Any]:
|
|
372
|
+
"""
|
|
373
|
+
Check the health of the encoding worker.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Health status including active jobs and FFmpeg version
|
|
377
|
+
"""
|
|
378
|
+
self._load_credentials()
|
|
379
|
+
|
|
380
|
+
if not self.is_configured:
|
|
381
|
+
return {"status": "not_configured"}
|
|
382
|
+
|
|
383
|
+
url = f"{self._url}/health"
|
|
384
|
+
headers = {"X-API-Key": self._api_key}
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
async with aiohttp.ClientSession() as session:
|
|
388
|
+
async with session.get(url, headers=headers, timeout=10) as resp:
|
|
389
|
+
if resp.status == 200:
|
|
390
|
+
return await resp.json()
|
|
391
|
+
return {"status": "error", "code": resp.status}
|
|
392
|
+
except Exception as e:
|
|
393
|
+
return {"status": "error", "error": str(e)}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# Singleton instance
|
|
397
|
+
_encoding_service: Optional[EncodingService] = None
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def get_encoding_service() -> EncodingService:
|
|
401
|
+
"""Get the singleton encoding service instance."""
|
|
402
|
+
global _encoding_service
|
|
403
|
+
if _encoding_service is None:
|
|
404
|
+
_encoding_service = EncodingService()
|
|
405
|
+
return _encoding_service
|