karaoke-gen 0.90.1__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/utils/__init__.py +163 -8
- karaoke_gen/video_background_processor.py +9 -4
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/METADATA +1 -1
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/RECORD +186 -41
- 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.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Title and end screen generation worker.
|
|
3
|
+
|
|
4
|
+
Handles screen generation after parallel processing completes:
|
|
5
|
+
1. Generate title screen with artist/song info
|
|
6
|
+
2. Generate end screen ("Thank you for singing!")
|
|
7
|
+
3. Upload both screens to GCS
|
|
8
|
+
4. Transition to AWAITING_REVIEW (human must review lyrics)
|
|
9
|
+
|
|
10
|
+
After review:
|
|
11
|
+
5. Render video worker generates with_vocals.mkv
|
|
12
|
+
6. Then AWAITING_INSTRUMENTAL_SELECTION
|
|
13
|
+
|
|
14
|
+
This worker is triggered automatically when both audio and lyrics
|
|
15
|
+
processing complete (via mark_audio_complete/mark_lyrics_complete coordination).
|
|
16
|
+
|
|
17
|
+
Integrates with karaoke_gen.video_generator.VideoGenerator.
|
|
18
|
+
|
|
19
|
+
Observability:
|
|
20
|
+
- All operations wrapped in tracing spans for Cloud Trace visibility
|
|
21
|
+
- Logs include [job:ID] prefix for easy filtering in Cloud Logging
|
|
22
|
+
- Worker start/end timing logged with WORKER_START/WORKER_END markers
|
|
23
|
+
"""
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
import shutil
|
|
27
|
+
import tempfile
|
|
28
|
+
import time
|
|
29
|
+
from typing import Optional, Dict, Any
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from backend.models.job import JobStatus
|
|
33
|
+
from backend.services.job_manager import JobManager
|
|
34
|
+
from backend.services.storage_service import StorageService
|
|
35
|
+
from backend.config import get_settings
|
|
36
|
+
from backend.workers.style_helper import load_style_config, StyleConfig
|
|
37
|
+
from backend.workers.worker_logging import create_job_logger, setup_job_logging, job_logging_context
|
|
38
|
+
from backend.services.tracing import job_span, add_span_event, add_span_attribute
|
|
39
|
+
|
|
40
|
+
# Import from karaoke_gen package
|
|
41
|
+
from karaoke_gen.video_generator import VideoGenerator
|
|
42
|
+
from karaoke_gen.utils import sanitize_filename
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Loggers to capture for screens worker
|
|
49
|
+
SCREENS_WORKER_LOGGERS = [
|
|
50
|
+
"karaoke_gen.video_generator",
|
|
51
|
+
"backend.workers.style_helper",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def generate_screens(job_id: str) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
Generate title and end screen videos for a job.
|
|
58
|
+
|
|
59
|
+
This is the main entry point for the screens worker.
|
|
60
|
+
Called automatically when both audio and lyrics processing complete.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
job_id: Job ID to process
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True if successful, False otherwise
|
|
67
|
+
"""
|
|
68
|
+
start_time = time.time()
|
|
69
|
+
job_manager = JobManager()
|
|
70
|
+
storage = StorageService()
|
|
71
|
+
settings = get_settings()
|
|
72
|
+
|
|
73
|
+
# Create job logger for remote debugging
|
|
74
|
+
job_log = create_job_logger(job_id, "screens")
|
|
75
|
+
|
|
76
|
+
# Log with structured markers for easy Cloud Logging queries
|
|
77
|
+
logger.info(f"[job:{job_id}] WORKER_START worker=screens")
|
|
78
|
+
|
|
79
|
+
# Set up log capture for VideoGenerator and style_helper
|
|
80
|
+
log_handler = setup_job_logging(job_id, "screens", *SCREENS_WORKER_LOGGERS)
|
|
81
|
+
|
|
82
|
+
job = job_manager.get_job(job_id)
|
|
83
|
+
if not job:
|
|
84
|
+
logger.error(f"[job:{job_id}] Job not found")
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
# Validate both audio and lyrics are complete
|
|
88
|
+
if not _validate_prerequisites(job):
|
|
89
|
+
logger.error(f"[job:{job_id}] Prerequisites not met for screen generation")
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
# Create temporary working directory
|
|
93
|
+
temp_dir = tempfile.mkdtemp(prefix=f"karaoke_screens_{job_id}_")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# Wrap entire worker in a tracing span
|
|
97
|
+
with job_span("screens-worker", job_id, {"artist": job.artist, "title": job.title}) as root_span:
|
|
98
|
+
# Use job_logging_context for proper log isolation when multiple jobs run concurrently
|
|
99
|
+
with job_logging_context(job_id):
|
|
100
|
+
job_log.info(f"Starting screen generation for {job.artist} - {job.title}")
|
|
101
|
+
logger.info(f"[job:{job_id}] Starting screen generation for {job.artist} - {job.title}")
|
|
102
|
+
|
|
103
|
+
# Transition to GENERATING_SCREENS state
|
|
104
|
+
job_manager.transition_to_state(
|
|
105
|
+
job_id=job_id,
|
|
106
|
+
new_status=JobStatus.GENERATING_SCREENS,
|
|
107
|
+
progress=50,
|
|
108
|
+
message="Generating title and end screens"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Log style assets info
|
|
112
|
+
style_assets = getattr(job, 'style_assets', {}) or {}
|
|
113
|
+
job_log.info(f"Style assets from job: {list(style_assets.keys()) if style_assets else 'None'}")
|
|
114
|
+
if style_assets:
|
|
115
|
+
for key, path in style_assets.items():
|
|
116
|
+
job_log.info(f" {key}: {path}")
|
|
117
|
+
|
|
118
|
+
# Load style configuration (downloads assets from GCS if available)
|
|
119
|
+
with job_span("load-style-config", job_id):
|
|
120
|
+
job_log.info("Loading style configuration from GCS...")
|
|
121
|
+
style_config = await load_style_config(job, storage, temp_dir)
|
|
122
|
+
if style_config.has_custom_styles():
|
|
123
|
+
job_log.info("Using CUSTOM style configuration")
|
|
124
|
+
logger.info(f"[job:{job_id}] Using custom style configuration")
|
|
125
|
+
add_span_attribute("style_type", "custom")
|
|
126
|
+
else:
|
|
127
|
+
job_log.warning("Using DEFAULT style configuration (no custom styles found)")
|
|
128
|
+
logger.info(f"[job:{job_id}] Using default style configuration")
|
|
129
|
+
add_span_attribute("style_type", "default")
|
|
130
|
+
|
|
131
|
+
# Initialize video generator
|
|
132
|
+
video_generator = _create_video_generator(temp_dir)
|
|
133
|
+
|
|
134
|
+
# Generate title screen with style config
|
|
135
|
+
with job_span("generate-title-screen", job_id):
|
|
136
|
+
job_log.info("Generating title screen...")
|
|
137
|
+
title_screen_path = await _generate_title_screen(
|
|
138
|
+
job_id=job_id,
|
|
139
|
+
job=job,
|
|
140
|
+
video_generator=video_generator,
|
|
141
|
+
style_config=style_config,
|
|
142
|
+
temp_dir=temp_dir,
|
|
143
|
+
job_log=job_log
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if not title_screen_path:
|
|
147
|
+
raise Exception("Title screen generation failed")
|
|
148
|
+
job_log.info(f"Title screen generated: {title_screen_path}")
|
|
149
|
+
|
|
150
|
+
# Generate end screen with style config
|
|
151
|
+
with job_span("generate-end-screen", job_id):
|
|
152
|
+
job_log.info("Generating end screen...")
|
|
153
|
+
end_screen_path = await _generate_end_screen(
|
|
154
|
+
job_id=job_id,
|
|
155
|
+
job=job,
|
|
156
|
+
video_generator=video_generator,
|
|
157
|
+
style_config=style_config,
|
|
158
|
+
temp_dir=temp_dir,
|
|
159
|
+
job_log=job_log
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if not end_screen_path:
|
|
163
|
+
raise Exception("End screen generation failed")
|
|
164
|
+
job_log.info(f"End screen generated: {end_screen_path}")
|
|
165
|
+
|
|
166
|
+
# Upload screens to GCS
|
|
167
|
+
with job_span("upload-screens", job_id):
|
|
168
|
+
await _upload_screens(
|
|
169
|
+
job_id=job_id,
|
|
170
|
+
job_manager=job_manager,
|
|
171
|
+
storage=storage,
|
|
172
|
+
title_screen_path=title_screen_path,
|
|
173
|
+
end_screen_path=end_screen_path
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Apply countdown padding if needed
|
|
177
|
+
await _apply_countdown_padding_if_needed(job_id, job_manager, job)
|
|
178
|
+
|
|
179
|
+
# Transition to AWAITING_REVIEW
|
|
180
|
+
# Human must review lyrics before video can be rendered
|
|
181
|
+
logger.info(f"[job:{job_id}] Screens generated, awaiting lyrics review")
|
|
182
|
+
job_manager.transition_to_state(
|
|
183
|
+
job_id=job_id,
|
|
184
|
+
new_status=JobStatus.AWAITING_REVIEW,
|
|
185
|
+
progress=55,
|
|
186
|
+
message="Ready for lyrics review. Please review and correct lyrics."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
duration = time.time() - start_time
|
|
190
|
+
root_span.set_attribute("duration_seconds", duration)
|
|
191
|
+
logger.info(f"[job:{job_id}] WORKER_END worker=screens status=success duration={duration:.1f}s")
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
except Exception as e:
|
|
195
|
+
duration = time.time() - start_time
|
|
196
|
+
logger.error(f"[job:{job_id}] WORKER_END worker=screens status=error duration={duration:.1f}s error={e}")
|
|
197
|
+
job_manager.mark_job_failed(
|
|
198
|
+
job_id=job_id,
|
|
199
|
+
error_message=f"Screen generation failed: {str(e)}",
|
|
200
|
+
error_details={"stage": "screen_generation", "error": str(e)}
|
|
201
|
+
)
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
finally:
|
|
205
|
+
# Remove log handler to avoid duplicate logging on future runs
|
|
206
|
+
for logger_name in SCREENS_WORKER_LOGGERS:
|
|
207
|
+
try:
|
|
208
|
+
logging.getLogger(logger_name).removeHandler(log_handler)
|
|
209
|
+
except Exception:
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
# Cleanup temporary directory
|
|
213
|
+
if os.path.exists(temp_dir):
|
|
214
|
+
shutil.rmtree(temp_dir)
|
|
215
|
+
logger.debug(f"[job:{job_id}] Cleaned up temp directory: {temp_dir}")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _validate_prerequisites(job) -> bool:
|
|
219
|
+
"""
|
|
220
|
+
Validate that both audio and lyrics processing are complete.
|
|
221
|
+
|
|
222
|
+
Single Responsibility: Validation logic separated from main flow.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
job: Job object
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
True if prerequisites met, False otherwise
|
|
229
|
+
"""
|
|
230
|
+
audio_complete = job.state_data.get('audio_complete', False)
|
|
231
|
+
lyrics_complete = job.state_data.get('lyrics_complete', False)
|
|
232
|
+
|
|
233
|
+
if not audio_complete:
|
|
234
|
+
logger.error(f"Job {job.job_id}: Audio processing not complete")
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
if not lyrics_complete:
|
|
238
|
+
logger.error(f"Job {job.job_id}: Lyrics processing not complete")
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
if not job.artist or not job.title:
|
|
242
|
+
logger.error(f"Job {job.job_id}: Missing artist or title")
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
return True
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _create_video_generator(temp_dir: str) -> VideoGenerator:
|
|
249
|
+
"""
|
|
250
|
+
Create video generator with appropriate parameters.
|
|
251
|
+
|
|
252
|
+
Dependency Inversion: Depends on VideoGenerator abstraction.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
temp_dir: Temporary directory for output
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Configured VideoGenerator instance
|
|
259
|
+
"""
|
|
260
|
+
# FFmpeg base command (same as in audio_worker)
|
|
261
|
+
ffmpeg_base_command = "ffmpeg -hide_banner -loglevel error -nostats -y"
|
|
262
|
+
|
|
263
|
+
return VideoGenerator(
|
|
264
|
+
logger=logger,
|
|
265
|
+
ffmpeg_base_command=ffmpeg_base_command,
|
|
266
|
+
render_bounding_boxes=False,
|
|
267
|
+
output_png=True,
|
|
268
|
+
output_jpg=True
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
async def _generate_title_screen(
|
|
273
|
+
job_id: str,
|
|
274
|
+
job,
|
|
275
|
+
video_generator: VideoGenerator,
|
|
276
|
+
style_config: StyleConfig,
|
|
277
|
+
temp_dir: str,
|
|
278
|
+
job_log = None
|
|
279
|
+
) -> Optional[str]:
|
|
280
|
+
"""
|
|
281
|
+
Generate title screen video with custom style configuration.
|
|
282
|
+
|
|
283
|
+
Single Responsibility: Only handles title screen generation.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
job_id: Job ID
|
|
287
|
+
job: Job object with artist/title
|
|
288
|
+
video_generator: Video generator instance
|
|
289
|
+
style_config: Style configuration with formats and assets
|
|
290
|
+
temp_dir: Temporary directory
|
|
291
|
+
job_log: Optional JobLogger for remote debugging
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Path to generated title screen, or None if failed
|
|
295
|
+
"""
|
|
296
|
+
try:
|
|
297
|
+
logger.info(f"Job {job_id}: Generating title screen")
|
|
298
|
+
|
|
299
|
+
# Set up output paths
|
|
300
|
+
# Sanitize artist/title to handle Unicode characters (curly quotes, em dashes, etc.)
|
|
301
|
+
safe_artist = sanitize_filename(job.artist) if job.artist else "Unknown"
|
|
302
|
+
safe_title = sanitize_filename(job.title) if job.title else "Unknown"
|
|
303
|
+
artist_title = f"{safe_artist} - {safe_title}"
|
|
304
|
+
output_image_filepath_noext = os.path.join(temp_dir, f"{artist_title} (Title)")
|
|
305
|
+
output_video_filepath = os.path.join(temp_dir, f"{artist_title} (Title).mov")
|
|
306
|
+
|
|
307
|
+
# Get title format settings from style config
|
|
308
|
+
title_format = style_config.get_intro_format()
|
|
309
|
+
intro_duration = style_config.intro_video_duration
|
|
310
|
+
|
|
311
|
+
# Log detailed style info for debugging
|
|
312
|
+
if job_log:
|
|
313
|
+
job_log.info("Title screen format configuration:")
|
|
314
|
+
job_log.info(f" background_image: {title_format.get('background_image')}")
|
|
315
|
+
job_log.info(f" background_color: {title_format.get('background_color')}")
|
|
316
|
+
job_log.info(f" font: {title_format.get('font')}")
|
|
317
|
+
job_log.info(f" title_color: {title_format.get('title_color')}")
|
|
318
|
+
job_log.info(f" artist_color: {title_format.get('artist_color')}")
|
|
319
|
+
job_log.info(f" duration: {intro_duration}s")
|
|
320
|
+
|
|
321
|
+
# Check if background image exists
|
|
322
|
+
bg_image = title_format.get('background_image')
|
|
323
|
+
if bg_image and os.path.exists(bg_image):
|
|
324
|
+
job_log.info(f" background_image file EXISTS: {os.path.getsize(bg_image)} bytes")
|
|
325
|
+
elif bg_image:
|
|
326
|
+
job_log.warning(f" background_image file NOT FOUND: {bg_image}")
|
|
327
|
+
|
|
328
|
+
logger.info(f"Job {job_id}: Title format - bg_image: {title_format.get('background_image')}, font: {title_format.get('font')}")
|
|
329
|
+
|
|
330
|
+
# Generate title screen (synchronous method)
|
|
331
|
+
video_generator.create_title_video(
|
|
332
|
+
artist=job.artist,
|
|
333
|
+
title=job.title,
|
|
334
|
+
format=title_format,
|
|
335
|
+
output_image_filepath_noext=output_image_filepath_noext,
|
|
336
|
+
output_video_filepath=output_video_filepath,
|
|
337
|
+
existing_title_image=title_format.get('existing_image'),
|
|
338
|
+
intro_video_duration=intro_duration
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
if os.path.exists(output_video_filepath):
|
|
342
|
+
logger.info(f"Job {job_id}: Title screen generated at {output_video_filepath}")
|
|
343
|
+
return output_video_filepath
|
|
344
|
+
else:
|
|
345
|
+
logger.error(f"Job {job_id}: Title screen generation returned no file")
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
except Exception as e:
|
|
349
|
+
logger.error(f"Job {job_id}: Title screen generation error: {e}", exc_info=True)
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
async def _generate_end_screen(
|
|
354
|
+
job_id: str,
|
|
355
|
+
job,
|
|
356
|
+
video_generator: VideoGenerator,
|
|
357
|
+
style_config: StyleConfig,
|
|
358
|
+
temp_dir: str,
|
|
359
|
+
job_log = None
|
|
360
|
+
) -> Optional[str]:
|
|
361
|
+
"""
|
|
362
|
+
Generate end screen video with custom style configuration.
|
|
363
|
+
|
|
364
|
+
Single Responsibility: Only handles end screen generation.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
job_id: Job ID
|
|
368
|
+
job: Job object with artist/title
|
|
369
|
+
video_generator: Video generator instance
|
|
370
|
+
style_config: Style configuration with formats and assets
|
|
371
|
+
temp_dir: Temporary directory
|
|
372
|
+
job_log: Optional JobLogger for remote debugging
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
Path to generated end screen, or None if failed
|
|
376
|
+
"""
|
|
377
|
+
try:
|
|
378
|
+
logger.info(f"Job {job_id}: Generating end screen")
|
|
379
|
+
|
|
380
|
+
# Set up output paths
|
|
381
|
+
# Sanitize artist/title to handle Unicode characters (curly quotes, em dashes, etc.)
|
|
382
|
+
safe_artist = sanitize_filename(job.artist) if job.artist else "Unknown"
|
|
383
|
+
safe_title = sanitize_filename(job.title) if job.title else "Unknown"
|
|
384
|
+
artist_title = f"{safe_artist} - {safe_title}"
|
|
385
|
+
output_image_filepath_noext = os.path.join(temp_dir, f"{artist_title} (End)")
|
|
386
|
+
output_video_filepath = os.path.join(temp_dir, f"{artist_title} (End).mov")
|
|
387
|
+
|
|
388
|
+
# Get end format settings from style config
|
|
389
|
+
end_format = style_config.get_end_format()
|
|
390
|
+
end_duration = style_config.end_video_duration
|
|
391
|
+
|
|
392
|
+
# Log detailed style info for debugging
|
|
393
|
+
if job_log:
|
|
394
|
+
job_log.info("End screen format configuration:")
|
|
395
|
+
job_log.info(f" background_image: {end_format.get('background_image')}")
|
|
396
|
+
job_log.info(f" background_color: {end_format.get('background_color')}")
|
|
397
|
+
job_log.info(f" font: {end_format.get('font')}")
|
|
398
|
+
job_log.info(f" duration: {end_duration}s")
|
|
399
|
+
|
|
400
|
+
logger.info(f"Job {job_id}: End format - bg_image: {end_format.get('background_image')}, font: {end_format.get('font')}")
|
|
401
|
+
|
|
402
|
+
# Generate end screen (synchronous method)
|
|
403
|
+
video_generator.create_end_video(
|
|
404
|
+
artist=job.artist,
|
|
405
|
+
title=job.title,
|
|
406
|
+
format=end_format,
|
|
407
|
+
output_image_filepath_noext=output_image_filepath_noext,
|
|
408
|
+
output_video_filepath=output_video_filepath,
|
|
409
|
+
existing_end_image=end_format.get('existing_image'),
|
|
410
|
+
end_video_duration=end_duration
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if os.path.exists(output_video_filepath):
|
|
414
|
+
logger.info(f"Job {job_id}: End screen generated at {output_video_filepath}")
|
|
415
|
+
return output_video_filepath
|
|
416
|
+
else:
|
|
417
|
+
logger.error(f"Job {job_id}: End screen generation returned no file")
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
except Exception as e:
|
|
421
|
+
logger.error(f"Job {job_id}: End screen generation error: {e}", exc_info=True)
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
async def _upload_screens(
|
|
426
|
+
job_id: str,
|
|
427
|
+
job_manager: JobManager,
|
|
428
|
+
storage: StorageService,
|
|
429
|
+
title_screen_path: str,
|
|
430
|
+
end_screen_path: str
|
|
431
|
+
) -> None:
|
|
432
|
+
"""
|
|
433
|
+
Upload title and end screens to GCS (video + images).
|
|
434
|
+
|
|
435
|
+
Single Responsibility: Only handles uploads.
|
|
436
|
+
|
|
437
|
+
VideoGenerator creates .mov, .jpg, and .png files when configured with
|
|
438
|
+
output_png=True and output_jpg=True. We upload all three formats
|
|
439
|
+
for feature parity with the local CLI.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
job_id: Job ID
|
|
443
|
+
job_manager: Job manager instance
|
|
444
|
+
storage: Storage service instance
|
|
445
|
+
title_screen_path: Path to title screen video (.mov)
|
|
446
|
+
end_screen_path: Path to end screen video (.mov)
|
|
447
|
+
"""
|
|
448
|
+
# Upload title screen video
|
|
449
|
+
title_gcs_path = f"jobs/{job_id}/screens/title.mov"
|
|
450
|
+
title_url = storage.upload_file(title_screen_path, title_gcs_path)
|
|
451
|
+
job_manager.update_file_url(job_id, 'screens', 'title', title_url)
|
|
452
|
+
logger.info(f"Job {job_id}: Uploaded title screen video")
|
|
453
|
+
|
|
454
|
+
# Upload title screen images (.jpg and .png - created by VideoGenerator)
|
|
455
|
+
title_base = title_screen_path.replace('.mov', '')
|
|
456
|
+
for ext, key in [('.jpg', 'title_jpg'), ('.png', 'title_png')]:
|
|
457
|
+
image_path = f"{title_base}{ext}"
|
|
458
|
+
if os.path.exists(image_path):
|
|
459
|
+
gcs_path = f"jobs/{job_id}/screens/title{ext}"
|
|
460
|
+
url = storage.upload_file(image_path, gcs_path)
|
|
461
|
+
job_manager.update_file_url(job_id, 'screens', key, url)
|
|
462
|
+
logger.info(f"Job {job_id}: Uploaded title screen image ({ext})")
|
|
463
|
+
|
|
464
|
+
# Upload end screen video
|
|
465
|
+
end_gcs_path = f"jobs/{job_id}/screens/end.mov"
|
|
466
|
+
end_url = storage.upload_file(end_screen_path, end_gcs_path)
|
|
467
|
+
job_manager.update_file_url(job_id, 'screens', 'end', end_url)
|
|
468
|
+
logger.info(f"Job {job_id}: Uploaded end screen video")
|
|
469
|
+
|
|
470
|
+
# Upload end screen images (.jpg and .png - created by VideoGenerator)
|
|
471
|
+
end_base = end_screen_path.replace('.mov', '')
|
|
472
|
+
for ext, key in [('.jpg', 'end_jpg'), ('.png', 'end_png')]:
|
|
473
|
+
image_path = f"{end_base}{ext}"
|
|
474
|
+
if os.path.exists(image_path):
|
|
475
|
+
gcs_path = f"jobs/{job_id}/screens/end{ext}"
|
|
476
|
+
url = storage.upload_file(image_path, gcs_path)
|
|
477
|
+
job_manager.update_file_url(job_id, 'screens', key, url)
|
|
478
|
+
logger.info(f"Job {job_id}: Uploaded end screen image ({ext})")
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
async def _apply_countdown_padding_if_needed(
|
|
482
|
+
job_id: str,
|
|
483
|
+
job_manager: JobManager,
|
|
484
|
+
job
|
|
485
|
+
) -> None:
|
|
486
|
+
"""
|
|
487
|
+
Apply countdown padding to instrumentals if needed.
|
|
488
|
+
|
|
489
|
+
If lyrics start very early (within 3 seconds), LyricsTranscriber
|
|
490
|
+
adds a countdown intro ("3... 2... 1...") to the vocal audio.
|
|
491
|
+
We need to pad the instrumentals to match.
|
|
492
|
+
|
|
493
|
+
Single Responsibility: Only handles padding logic.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
job_id: Job ID
|
|
497
|
+
job_manager: Job manager instance
|
|
498
|
+
job: Job object with lyrics metadata
|
|
499
|
+
"""
|
|
500
|
+
# Check if countdown padding was added
|
|
501
|
+
lyrics_metadata = job.state_data.get('lyrics_metadata', {})
|
|
502
|
+
has_countdown = lyrics_metadata.get('has_countdown_padding', False)
|
|
503
|
+
|
|
504
|
+
if has_countdown:
|
|
505
|
+
logger.info(f"Job {job_id}: Countdown padding detected, applying to instrumentals")
|
|
506
|
+
|
|
507
|
+
# Transition to APPLYING_PADDING state
|
|
508
|
+
job_manager.transition_to_state(
|
|
509
|
+
job_id=job_id,
|
|
510
|
+
new_status=JobStatus.APPLYING_PADDING,
|
|
511
|
+
progress=52,
|
|
512
|
+
message="Synchronizing countdown padding"
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# TODO: Implement padding application
|
|
516
|
+
# This requires:
|
|
517
|
+
# 1. Download instrumental stems from GCS
|
|
518
|
+
# 2. Add silence to beginning
|
|
519
|
+
# 3. Re-upload padded versions
|
|
520
|
+
# For now, we'll skip this and just transition back
|
|
521
|
+
|
|
522
|
+
logger.info(f"Job {job_id}: Padding applied (TODO: implement actual padding)")
|
|
523
|
+
else:
|
|
524
|
+
logger.info(f"Job {job_id}: No countdown padding needed")
|
|
525
|
+
|