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,435 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Internal API routes for worker coordination.
|
|
3
|
+
|
|
4
|
+
These endpoints are for internal use only (backend → workers).
|
|
5
|
+
They are protected by admin authentication.
|
|
6
|
+
|
|
7
|
+
With Cloud Tasks integration, these endpoints may be called multiple times
|
|
8
|
+
(retry on failure). Idempotency checks prevent duplicate processing.
|
|
9
|
+
|
|
10
|
+
Observability:
|
|
11
|
+
- Extracts trace context from incoming requests (propagated via Cloud Tasks)
|
|
12
|
+
- Creates worker spans linked to the original request trace
|
|
13
|
+
- All logs include job_id for easy filtering in Cloud Logging
|
|
14
|
+
"""
|
|
15
|
+
import logging
|
|
16
|
+
import asyncio
|
|
17
|
+
import time
|
|
18
|
+
from typing import Tuple, Optional
|
|
19
|
+
from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends, Request
|
|
20
|
+
from pydantic import BaseModel
|
|
21
|
+
|
|
22
|
+
from backend.workers.audio_worker import process_audio_separation
|
|
23
|
+
from backend.workers.lyrics_worker import process_lyrics_transcription
|
|
24
|
+
from backend.workers.screens_worker import generate_screens
|
|
25
|
+
from backend.workers.video_worker import generate_video
|
|
26
|
+
from backend.workers.render_video_worker import process_render_video
|
|
27
|
+
from backend.api.dependencies import require_admin
|
|
28
|
+
from backend.services.auth_service import UserType
|
|
29
|
+
from backend.services.job_manager import JobManager
|
|
30
|
+
from backend.services.tracing import (
|
|
31
|
+
extract_trace_context,
|
|
32
|
+
start_span_with_context,
|
|
33
|
+
add_span_attribute,
|
|
34
|
+
add_span_event,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
router = APIRouter(prefix="/internal", tags=["internal"])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class WorkerRequest(BaseModel):
|
|
43
|
+
"""Request to trigger a worker."""
|
|
44
|
+
job_id: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class WorkerResponse(BaseModel):
|
|
48
|
+
"""Response from worker trigger."""
|
|
49
|
+
status: str
|
|
50
|
+
job_id: str
|
|
51
|
+
message: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _check_worker_idempotency(job_id: str, worker_name: str) -> Optional[WorkerResponse]:
|
|
55
|
+
"""
|
|
56
|
+
Check if a worker is already running or completed for this job.
|
|
57
|
+
|
|
58
|
+
This provides idempotency for Cloud Tasks retries - if a task is retried
|
|
59
|
+
but the worker is already running or has completed, we skip processing.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
job_id: Job ID to check
|
|
63
|
+
worker_name: Worker name (audio, lyrics, screens, render, video)
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
WorkerResponse if should skip (already running/complete), None to proceed
|
|
67
|
+
"""
|
|
68
|
+
job_manager = JobManager()
|
|
69
|
+
job = job_manager.get_job(job_id)
|
|
70
|
+
|
|
71
|
+
if not job:
|
|
72
|
+
logger.warning(f"[job:{job_id}] Job not found for {worker_name} worker")
|
|
73
|
+
return WorkerResponse(
|
|
74
|
+
status="not_found",
|
|
75
|
+
job_id=job_id,
|
|
76
|
+
message=f"Job {job_id} not found"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Check worker-specific progress in state_data
|
|
80
|
+
progress_key = f"{worker_name}_progress"
|
|
81
|
+
worker_progress = job.state_data.get(progress_key, {})
|
|
82
|
+
stage = worker_progress.get('stage')
|
|
83
|
+
|
|
84
|
+
if stage == 'running':
|
|
85
|
+
logger.info(f"[job:{job_id}] {worker_name.capitalize()} worker already running, skipping")
|
|
86
|
+
return WorkerResponse(
|
|
87
|
+
status="already_running",
|
|
88
|
+
job_id=job_id,
|
|
89
|
+
message=f"{worker_name.capitalize()} worker already in progress"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if stage == 'complete':
|
|
93
|
+
logger.info(f"[job:{job_id}] {worker_name.capitalize()} worker already complete, skipping")
|
|
94
|
+
return WorkerResponse(
|
|
95
|
+
status="already_complete",
|
|
96
|
+
job_id=job_id,
|
|
97
|
+
message=f"{worker_name.capitalize()} worker already completed"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Mark as running before starting (for idempotency on next retry)
|
|
101
|
+
job_manager.update_state_data(job_id, progress_key, {'stage': 'running'})
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@router.post("/workers/audio", response_model=WorkerResponse)
|
|
106
|
+
async def trigger_audio_worker(
|
|
107
|
+
request: WorkerRequest,
|
|
108
|
+
http_request: Request,
|
|
109
|
+
background_tasks: BackgroundTasks,
|
|
110
|
+
auth_data: Tuple[str, UserType, int] = Depends(require_admin)
|
|
111
|
+
):
|
|
112
|
+
"""
|
|
113
|
+
Trigger audio separation worker for a job.
|
|
114
|
+
|
|
115
|
+
This endpoint is called internally after job creation to start
|
|
116
|
+
the audio processing track (parallel with lyrics processing).
|
|
117
|
+
|
|
118
|
+
Idempotency: If worker is already running or complete, returns early.
|
|
119
|
+
|
|
120
|
+
The worker runs in the background and updates job state as it progresses.
|
|
121
|
+
"""
|
|
122
|
+
job_id = request.job_id
|
|
123
|
+
|
|
124
|
+
# Extract trace context from incoming request (propagated via Cloud Tasks)
|
|
125
|
+
trace_context = extract_trace_context(dict(http_request.headers))
|
|
126
|
+
|
|
127
|
+
logger.info(f"[job:{job_id}] WORKER_TRIGGER worker=audio")
|
|
128
|
+
add_span_attribute("job_id", job_id)
|
|
129
|
+
add_span_attribute("worker", "audio")
|
|
130
|
+
|
|
131
|
+
# Idempotency check
|
|
132
|
+
skip_response = _check_worker_idempotency(job_id, "audio")
|
|
133
|
+
if skip_response:
|
|
134
|
+
add_span_event("worker_skipped", {"reason": skip_response.status})
|
|
135
|
+
return skip_response
|
|
136
|
+
|
|
137
|
+
# Add task to background tasks
|
|
138
|
+
# This allows the HTTP response to return immediately
|
|
139
|
+
# while the worker continues processing
|
|
140
|
+
background_tasks.add_task(process_audio_separation, job_id)
|
|
141
|
+
|
|
142
|
+
add_span_event("worker_started")
|
|
143
|
+
return WorkerResponse(
|
|
144
|
+
status="started",
|
|
145
|
+
job_id=job_id,
|
|
146
|
+
message="Audio separation worker started"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@router.post("/workers/lyrics", response_model=WorkerResponse)
|
|
151
|
+
async def trigger_lyrics_worker(
|
|
152
|
+
request: WorkerRequest,
|
|
153
|
+
http_request: Request,
|
|
154
|
+
background_tasks: BackgroundTasks,
|
|
155
|
+
auth_data: Tuple[str, UserType, int] = Depends(require_admin)
|
|
156
|
+
):
|
|
157
|
+
"""
|
|
158
|
+
Trigger lyrics transcription worker for a job.
|
|
159
|
+
|
|
160
|
+
This endpoint is called internally after job creation to start
|
|
161
|
+
the lyrics processing track (parallel with audio processing).
|
|
162
|
+
|
|
163
|
+
Idempotency: If worker is already running or complete, returns early.
|
|
164
|
+
|
|
165
|
+
The worker runs in the background and updates job state as it progresses.
|
|
166
|
+
"""
|
|
167
|
+
job_id = request.job_id
|
|
168
|
+
|
|
169
|
+
# Extract trace context from incoming request
|
|
170
|
+
trace_context = extract_trace_context(dict(http_request.headers))
|
|
171
|
+
|
|
172
|
+
logger.info(f"[job:{job_id}] WORKER_TRIGGER worker=lyrics")
|
|
173
|
+
add_span_attribute("job_id", job_id)
|
|
174
|
+
add_span_attribute("worker", "lyrics")
|
|
175
|
+
|
|
176
|
+
# Idempotency check
|
|
177
|
+
skip_response = _check_worker_idempotency(job_id, "lyrics")
|
|
178
|
+
if skip_response:
|
|
179
|
+
add_span_event("worker_skipped", {"reason": skip_response.status})
|
|
180
|
+
return skip_response
|
|
181
|
+
|
|
182
|
+
# Add task to background tasks
|
|
183
|
+
background_tasks.add_task(process_lyrics_transcription, job_id)
|
|
184
|
+
|
|
185
|
+
add_span_event("worker_started")
|
|
186
|
+
return WorkerResponse(
|
|
187
|
+
status="started",
|
|
188
|
+
job_id=job_id,
|
|
189
|
+
message="Lyrics transcription worker started"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@router.post("/workers/screens", response_model=WorkerResponse)
|
|
194
|
+
async def trigger_screens_worker(
|
|
195
|
+
request: WorkerRequest,
|
|
196
|
+
http_request: Request,
|
|
197
|
+
background_tasks: BackgroundTasks,
|
|
198
|
+
auth_data: Tuple[str, UserType, int] = Depends(require_admin)
|
|
199
|
+
):
|
|
200
|
+
"""
|
|
201
|
+
Trigger title/end screen generation worker.
|
|
202
|
+
|
|
203
|
+
This is called automatically when both audio and lyrics are complete.
|
|
204
|
+
|
|
205
|
+
Idempotency: If worker is already running or complete, returns early.
|
|
206
|
+
"""
|
|
207
|
+
job_id = request.job_id
|
|
208
|
+
|
|
209
|
+
# Extract trace context from incoming request
|
|
210
|
+
trace_context = extract_trace_context(dict(http_request.headers))
|
|
211
|
+
|
|
212
|
+
logger.info(f"[job:{job_id}] WORKER_TRIGGER worker=screens")
|
|
213
|
+
add_span_attribute("job_id", job_id)
|
|
214
|
+
add_span_attribute("worker", "screens")
|
|
215
|
+
|
|
216
|
+
# Idempotency check
|
|
217
|
+
skip_response = _check_worker_idempotency(job_id, "screens")
|
|
218
|
+
if skip_response:
|
|
219
|
+
add_span_event("worker_skipped", {"reason": skip_response.status})
|
|
220
|
+
return skip_response
|
|
221
|
+
|
|
222
|
+
# Add task to background tasks
|
|
223
|
+
background_tasks.add_task(generate_screens, job_id)
|
|
224
|
+
|
|
225
|
+
add_span_event("worker_started")
|
|
226
|
+
return WorkerResponse(
|
|
227
|
+
status="started",
|
|
228
|
+
job_id=job_id,
|
|
229
|
+
message="Screens generation worker started"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@router.post("/workers/video", response_model=WorkerResponse)
|
|
234
|
+
async def trigger_video_worker(
|
|
235
|
+
request: WorkerRequest,
|
|
236
|
+
http_request: Request,
|
|
237
|
+
background_tasks: BackgroundTasks,
|
|
238
|
+
auth_data: Tuple[str, UserType, int] = Depends(require_admin)
|
|
239
|
+
):
|
|
240
|
+
"""
|
|
241
|
+
Trigger final video generation and encoding worker.
|
|
242
|
+
|
|
243
|
+
This is called after user selects their preferred instrumental.
|
|
244
|
+
This is the longest-running stage (15-20 minutes).
|
|
245
|
+
|
|
246
|
+
Idempotency: If worker is already running or complete, returns early.
|
|
247
|
+
"""
|
|
248
|
+
job_id = request.job_id
|
|
249
|
+
|
|
250
|
+
# Extract trace context from incoming request
|
|
251
|
+
trace_context = extract_trace_context(dict(http_request.headers))
|
|
252
|
+
|
|
253
|
+
logger.info(f"[job:{job_id}] WORKER_TRIGGER worker=video")
|
|
254
|
+
add_span_attribute("job_id", job_id)
|
|
255
|
+
add_span_attribute("worker", "video")
|
|
256
|
+
|
|
257
|
+
# Idempotency check
|
|
258
|
+
skip_response = _check_worker_idempotency(job_id, "video")
|
|
259
|
+
if skip_response:
|
|
260
|
+
add_span_event("worker_skipped", {"reason": skip_response.status})
|
|
261
|
+
return skip_response
|
|
262
|
+
|
|
263
|
+
# Add task to background tasks
|
|
264
|
+
background_tasks.add_task(generate_video, job_id)
|
|
265
|
+
|
|
266
|
+
add_span_event("worker_started")
|
|
267
|
+
return WorkerResponse(
|
|
268
|
+
status="started",
|
|
269
|
+
job_id=job_id,
|
|
270
|
+
message="Video generation worker started"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@router.post("/workers/render-video", response_model=WorkerResponse)
|
|
275
|
+
async def trigger_render_video_worker(
|
|
276
|
+
request: WorkerRequest,
|
|
277
|
+
http_request: Request,
|
|
278
|
+
background_tasks: BackgroundTasks,
|
|
279
|
+
auth_data: Tuple[str, UserType, int] = Depends(require_admin)
|
|
280
|
+
):
|
|
281
|
+
"""
|
|
282
|
+
Trigger render video worker (post-review).
|
|
283
|
+
|
|
284
|
+
This is called after human review is complete.
|
|
285
|
+
Uses OutputGenerator from LyricsTranscriber to generate the karaoke video
|
|
286
|
+
with the corrected lyrics.
|
|
287
|
+
|
|
288
|
+
Idempotency: If worker is already running or complete, returns early.
|
|
289
|
+
|
|
290
|
+
Output: with_vocals.mkv in GCS
|
|
291
|
+
Next state: AWAITING_INSTRUMENTAL_SELECTION
|
|
292
|
+
"""
|
|
293
|
+
job_id = request.job_id
|
|
294
|
+
|
|
295
|
+
# Extract trace context from incoming request
|
|
296
|
+
trace_context = extract_trace_context(dict(http_request.headers))
|
|
297
|
+
|
|
298
|
+
logger.info(f"[job:{job_id}] WORKER_TRIGGER worker=render-video")
|
|
299
|
+
add_span_attribute("job_id", job_id)
|
|
300
|
+
add_span_attribute("worker", "render-video")
|
|
301
|
+
|
|
302
|
+
# Idempotency check
|
|
303
|
+
skip_response = _check_worker_idempotency(job_id, "render")
|
|
304
|
+
if skip_response:
|
|
305
|
+
add_span_event("worker_skipped", {"reason": skip_response.status})
|
|
306
|
+
return skip_response
|
|
307
|
+
|
|
308
|
+
# Add task to background tasks
|
|
309
|
+
background_tasks.add_task(process_render_video, job_id)
|
|
310
|
+
|
|
311
|
+
add_span_event("worker_started")
|
|
312
|
+
return WorkerResponse(
|
|
313
|
+
status="started",
|
|
314
|
+
job_id=job_id,
|
|
315
|
+
message="Render video worker started (post-review)"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@router.post("/jobs/{job_id}/check-idle-reminder")
|
|
320
|
+
async def check_idle_reminder(
|
|
321
|
+
job_id: str,
|
|
322
|
+
http_request: Request,
|
|
323
|
+
auth_data: Tuple[str, UserType, int] = Depends(require_admin)
|
|
324
|
+
):
|
|
325
|
+
"""
|
|
326
|
+
Check if a job needs an idle reminder email.
|
|
327
|
+
|
|
328
|
+
This endpoint is called by a Cloud Tasks scheduled task 5 minutes after
|
|
329
|
+
a job enters a blocking state (AWAITING_REVIEW or AWAITING_INSTRUMENTAL_SELECTION).
|
|
330
|
+
|
|
331
|
+
If the job is still in the blocking state and no reminder has been sent yet,
|
|
332
|
+
sends a reminder email to the user.
|
|
333
|
+
|
|
334
|
+
Idempotency: Only one reminder per job (tracked via reminder_sent flag).
|
|
335
|
+
"""
|
|
336
|
+
from backend.models.job import JobStatus
|
|
337
|
+
from backend.services.job_notification_service import get_job_notification_service
|
|
338
|
+
|
|
339
|
+
# Extract trace context from incoming request
|
|
340
|
+
trace_context = extract_trace_context(dict(http_request.headers))
|
|
341
|
+
|
|
342
|
+
logger.info(f"[job:{job_id}] IDLE_REMINDER_CHECK starting")
|
|
343
|
+
add_span_attribute("job_id", job_id)
|
|
344
|
+
add_span_attribute("operation", "idle_reminder_check")
|
|
345
|
+
|
|
346
|
+
job_manager = JobManager()
|
|
347
|
+
job = job_manager.get_job(job_id)
|
|
348
|
+
|
|
349
|
+
if not job:
|
|
350
|
+
logger.warning(f"[job:{job_id}] Job not found for idle reminder check")
|
|
351
|
+
add_span_event("job_not_found")
|
|
352
|
+
return {"status": "not_found", "job_id": job_id, "message": "Job not found"}
|
|
353
|
+
|
|
354
|
+
# Check if job is still in a blocking state
|
|
355
|
+
blocking_states = [JobStatus.AWAITING_REVIEW, JobStatus.AWAITING_INSTRUMENTAL_SELECTION]
|
|
356
|
+
if job.status not in [s.value for s in blocking_states]:
|
|
357
|
+
logger.info(f"[job:{job_id}] Job no longer in blocking state ({job.status}), skipping reminder")
|
|
358
|
+
add_span_event("not_blocking", {"current_status": job.status})
|
|
359
|
+
return {
|
|
360
|
+
"status": "skipped",
|
|
361
|
+
"job_id": job_id,
|
|
362
|
+
"message": f"Job not in blocking state (current: {job.status})"
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
# Normalize state_data to prevent None errors
|
|
366
|
+
state_data = job.state_data or {}
|
|
367
|
+
|
|
368
|
+
# Check if reminder was already sent (idempotency)
|
|
369
|
+
if state_data.get('reminder_sent'):
|
|
370
|
+
logger.info(f"[job:{job_id}] Reminder already sent, skipping")
|
|
371
|
+
add_span_event("already_sent")
|
|
372
|
+
return {"status": "already_sent", "job_id": job_id, "message": "Reminder already sent"}
|
|
373
|
+
|
|
374
|
+
# Check if user has an email
|
|
375
|
+
if not job.user_email:
|
|
376
|
+
logger.warning(f"[job:{job_id}] No user email, cannot send reminder")
|
|
377
|
+
add_span_event("no_email")
|
|
378
|
+
return {"status": "no_email", "job_id": job_id, "message": "No user email configured"}
|
|
379
|
+
|
|
380
|
+
# Determine action type
|
|
381
|
+
action_type = state_data.get('blocking_action_type')
|
|
382
|
+
if not action_type:
|
|
383
|
+
action_type = "lyrics" if job.status == JobStatus.AWAITING_REVIEW.value else "instrumental"
|
|
384
|
+
|
|
385
|
+
# Send the reminder email
|
|
386
|
+
try:
|
|
387
|
+
notification_service = get_job_notification_service()
|
|
388
|
+
|
|
389
|
+
success = await notification_service.send_action_reminder_email(
|
|
390
|
+
job_id=job.job_id,
|
|
391
|
+
user_email=job.user_email,
|
|
392
|
+
action_type=action_type,
|
|
393
|
+
user_name=None, # Could fetch from user service if needed
|
|
394
|
+
artist=job.artist,
|
|
395
|
+
title=job.title,
|
|
396
|
+
audio_hash=job.audio_hash,
|
|
397
|
+
review_token=job.review_token,
|
|
398
|
+
instrumental_token=job.instrumental_token,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
if success:
|
|
402
|
+
# Mark reminder as sent (prevents duplicate sends)
|
|
403
|
+
job_manager.firestore.update_job(job_id, {
|
|
404
|
+
'state_data': {**state_data, 'reminder_sent': True}
|
|
405
|
+
})
|
|
406
|
+
logger.info(f"[job:{job_id}] Sent {action_type} reminder email to {job.user_email}")
|
|
407
|
+
add_span_event("reminder_sent", {"action_type": action_type})
|
|
408
|
+
return {
|
|
409
|
+
"status": "sent",
|
|
410
|
+
"job_id": job_id,
|
|
411
|
+
"message": f"Sent {action_type} reminder to {job.user_email}"
|
|
412
|
+
}
|
|
413
|
+
else:
|
|
414
|
+
logger.error(f"[job:{job_id}] Failed to send reminder email")
|
|
415
|
+
add_span_event("send_failed")
|
|
416
|
+
return {"status": "failed", "job_id": job_id, "message": "Failed to send reminder"}
|
|
417
|
+
|
|
418
|
+
except Exception as e:
|
|
419
|
+
logger.exception(f"[job:{job_id}] Error sending reminder: {e}")
|
|
420
|
+
add_span_event("error", {"error": str(e)})
|
|
421
|
+
return {"status": "error", "job_id": job_id, "message": str(e)}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@router.get("/health")
|
|
425
|
+
async def internal_health(
|
|
426
|
+
auth_data: Tuple[str, UserType, int] = Depends(require_admin)
|
|
427
|
+
):
|
|
428
|
+
"""
|
|
429
|
+
Internal health check endpoint.
|
|
430
|
+
|
|
431
|
+
Used to verify the internal API is responsive.
|
|
432
|
+
Requires admin authentication.
|
|
433
|
+
"""
|
|
434
|
+
return {"status": "healthy", "service": "karaoke-backend-internal"}
|
|
435
|
+
|