karaoke-gen 0.96.0__py3-none-any.whl → 0.101.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/api/routes/admin.py +696 -92
- backend/api/routes/audio_search.py +29 -8
- backend/api/routes/file_upload.py +99 -22
- backend/api/routes/health.py +65 -0
- backend/api/routes/internal.py +6 -0
- backend/api/routes/jobs.py +28 -1
- backend/api/routes/review.py +13 -6
- backend/api/routes/tenant.py +120 -0
- backend/api/routes/users.py +472 -51
- backend/main.py +31 -2
- backend/middleware/__init__.py +7 -1
- backend/middleware/tenant.py +192 -0
- backend/models/job.py +19 -3
- backend/models/tenant.py +208 -0
- backend/models/user.py +18 -0
- backend/services/email_service.py +253 -6
- backend/services/encoding_service.py +128 -31
- backend/services/firestore_service.py +6 -0
- backend/services/job_manager.py +44 -2
- backend/services/langfuse_preloader.py +98 -0
- backend/services/nltk_preloader.py +122 -0
- backend/services/spacy_preloader.py +65 -0
- backend/services/stripe_service.py +133 -11
- backend/services/tenant_service.py +285 -0
- backend/services/user_service.py +85 -7
- backend/tests/emulator/conftest.py +22 -1
- backend/tests/emulator/test_made_for_you_integration.py +167 -0
- backend/tests/test_admin_job_files.py +337 -0
- backend/tests/test_admin_job_reset.py +384 -0
- backend/tests/test_admin_job_update.py +326 -0
- backend/tests/test_email_service.py +233 -0
- backend/tests/test_impersonation.py +223 -0
- backend/tests/test_job_creation_regression.py +4 -0
- backend/tests/test_job_manager.py +171 -9
- backend/tests/test_jobs_api.py +11 -1
- backend/tests/test_made_for_you.py +2086 -0
- backend/tests/test_models.py +139 -0
- backend/tests/test_spacy_preloader.py +119 -0
- backend/tests/test_tenant_api.py +350 -0
- backend/tests/test_tenant_middleware.py +345 -0
- backend/tests/test_tenant_models.py +406 -0
- backend/tests/test_tenant_service.py +418 -0
- backend/utils/test_data.py +27 -0
- backend/workers/screens_worker.py +16 -6
- backend/workers/video_worker.py +8 -3
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.dist-info}/METADATA +1 -1
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.dist-info}/RECORD +58 -39
- lyrics_transcriber/correction/agentic/agent.py +17 -6
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +96 -43
- lyrics_transcriber/correction/agentic/providers/model_factory.py +27 -6
- lyrics_transcriber/correction/anchor_sequence.py +151 -37
- lyrics_transcriber/correction/handlers/syllables_match.py +44 -2
- lyrics_transcriber/correction/phrase_analyzer.py +18 -0
- lyrics_transcriber/frontend/src/api.ts +13 -5
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +90 -57
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -33,10 +33,12 @@ from backend.services.audio_search_service import (
|
|
|
33
33
|
NoResultsError,
|
|
34
34
|
DownloadError,
|
|
35
35
|
)
|
|
36
|
+
from backend.services.theme_service import get_theme_service
|
|
36
37
|
from backend.config import get_settings
|
|
37
38
|
from backend.version import VERSION
|
|
38
39
|
from backend.api.dependencies import require_auth
|
|
39
40
|
from backend.services.auth_service import UserType, AuthResult
|
|
41
|
+
from backend.middleware.tenant import get_tenant_config_from_request
|
|
40
42
|
from pathlib import Path
|
|
41
43
|
|
|
42
44
|
logger = logging.getLogger(__name__)
|
|
@@ -478,17 +480,25 @@ async def search_audio(
|
|
|
478
480
|
):
|
|
479
481
|
"""
|
|
480
482
|
Search for audio by artist and title, creating a new job.
|
|
481
|
-
|
|
483
|
+
|
|
482
484
|
This endpoint:
|
|
483
485
|
1. Creates a job in PENDING state
|
|
484
486
|
2. Searches for audio using flacfetch
|
|
485
487
|
3. Either returns search results for user selection, or
|
|
486
488
|
4. If auto_download=True, automatically selects best and starts processing
|
|
487
|
-
|
|
489
|
+
|
|
488
490
|
Use cases:
|
|
489
491
|
- Interactive mode (default): Returns results, user calls /select endpoint
|
|
490
492
|
- Auto mode (auto_download=True): Automatically selects and downloads best
|
|
491
493
|
"""
|
|
494
|
+
# Check tenant feature flag
|
|
495
|
+
tenant_config = get_tenant_config_from_request(request)
|
|
496
|
+
if tenant_config and not tenant_config.features.audio_search:
|
|
497
|
+
raise HTTPException(
|
|
498
|
+
status_code=403,
|
|
499
|
+
detail="Audio search is not available for this portal"
|
|
500
|
+
)
|
|
501
|
+
|
|
492
502
|
try:
|
|
493
503
|
# Apply default distribution settings
|
|
494
504
|
settings = get_settings()
|
|
@@ -534,9 +544,18 @@ async def search_audio(
|
|
|
534
544
|
# Extract request metadata
|
|
535
545
|
request_metadata = extract_request_metadata(request, created_from="audio_search")
|
|
536
546
|
|
|
547
|
+
# Apply default theme if none specified
|
|
548
|
+
# This ensures all karaoke videos use the Nomad theme by default
|
|
549
|
+
effective_theme_id = body.theme_id
|
|
550
|
+
if effective_theme_id is None:
|
|
551
|
+
theme_service = get_theme_service()
|
|
552
|
+
effective_theme_id = theme_service.get_default_theme_id()
|
|
553
|
+
if effective_theme_id:
|
|
554
|
+
logger.info(f"Applying default theme: {effective_theme_id}")
|
|
555
|
+
|
|
537
556
|
# Resolve CDG/TXT defaults based on theme
|
|
538
557
|
resolved_cdg, resolved_txt = _resolve_cdg_txt_defaults(
|
|
539
|
-
|
|
558
|
+
effective_theme_id, body.enable_cdg, body.enable_txt
|
|
540
559
|
)
|
|
541
560
|
|
|
542
561
|
# Use authenticated user's email
|
|
@@ -552,7 +571,7 @@ async def search_audio(
|
|
|
552
571
|
job_create = JobCreate(
|
|
553
572
|
artist=effective_display_artist, # Display value for title screens, filenames
|
|
554
573
|
title=effective_display_title, # Display value for title screens, filenames
|
|
555
|
-
theme_id=
|
|
574
|
+
theme_id=effective_theme_id,
|
|
556
575
|
color_overrides=body.color_overrides or {},
|
|
557
576
|
enable_cdg=resolved_cdg,
|
|
558
577
|
enable_txt=resolved_txt,
|
|
@@ -575,6 +594,8 @@ async def search_audio(
|
|
|
575
594
|
auto_download=body.auto_download,
|
|
576
595
|
request_metadata=request_metadata,
|
|
577
596
|
non_interactive=body.non_interactive,
|
|
597
|
+
# Tenant scoping
|
|
598
|
+
tenant_id=tenant_config.id if tenant_config else None,
|
|
578
599
|
)
|
|
579
600
|
job = job_manager.create_job(job_create)
|
|
580
601
|
job_id = job.job_id
|
|
@@ -591,11 +612,11 @@ async def search_audio(
|
|
|
591
612
|
# If theme is set and no custom style files are being uploaded, prepare theme style now
|
|
592
613
|
# This copies the theme's style_params.json to the job folder so LyricsTranscriber
|
|
593
614
|
# can access the style configuration for preview videos
|
|
594
|
-
if
|
|
615
|
+
if effective_theme_id and not body.style_files:
|
|
595
616
|
from backend.api.routes.file_upload import _prepare_theme_for_job
|
|
596
617
|
try:
|
|
597
618
|
style_params_path, theme_style_assets, youtube_desc = _prepare_theme_for_job(
|
|
598
|
-
job_id,
|
|
619
|
+
job_id, effective_theme_id, body.color_overrides
|
|
599
620
|
)
|
|
600
621
|
theme_update = {
|
|
601
622
|
'style_params_gcs_path': style_params_path,
|
|
@@ -604,9 +625,9 @@ async def search_audio(
|
|
|
604
625
|
if youtube_desc and not effective_youtube_description:
|
|
605
626
|
theme_update['youtube_description_template'] = youtube_desc
|
|
606
627
|
job_manager.update_job(job_id, theme_update)
|
|
607
|
-
logger.info(f"Applied theme '{
|
|
628
|
+
logger.info(f"Applied theme '{effective_theme_id}' to job {job_id}")
|
|
608
629
|
except Exception as e:
|
|
609
|
-
logger.warning(f"Failed to prepare theme '{
|
|
630
|
+
logger.warning(f"Failed to prepare theme '{effective_theme_id}' for job {job_id}: {e}")
|
|
610
631
|
# Continue without theme - job can still be processed with defaults
|
|
611
632
|
|
|
612
633
|
# Handle style file uploads if provided
|
|
@@ -34,6 +34,7 @@ from backend.version import VERSION
|
|
|
34
34
|
from backend.services.metrics import metrics
|
|
35
35
|
from backend.api.dependencies import require_auth
|
|
36
36
|
from backend.services.auth_service import UserType, AuthResult
|
|
37
|
+
from backend.middleware.tenant import get_tenant_config_from_request
|
|
37
38
|
|
|
38
39
|
logger = logging.getLogger(__name__)
|
|
39
40
|
router = APIRouter(tags=["jobs"])
|
|
@@ -446,6 +447,14 @@ async def upload_and_create_job(
|
|
|
446
447
|
The style_params JSON can reference the uploaded images/fonts by their original
|
|
447
448
|
filenames, and the backend will update the paths to GCS locations.
|
|
448
449
|
"""
|
|
450
|
+
# Check tenant feature flag
|
|
451
|
+
tenant_config = get_tenant_config_from_request(request)
|
|
452
|
+
if tenant_config and not tenant_config.features.file_upload:
|
|
453
|
+
raise HTTPException(
|
|
454
|
+
status_code=403,
|
|
455
|
+
detail="File upload is not available for this portal"
|
|
456
|
+
)
|
|
457
|
+
|
|
449
458
|
try:
|
|
450
459
|
# Validate main audio file type
|
|
451
460
|
file_ext = Path(file.filename).suffix.lower()
|
|
@@ -562,9 +571,18 @@ async def upload_and_create_job(
|
|
|
562
571
|
detail=f"Invalid color_overrides JSON: {e}"
|
|
563
572
|
)
|
|
564
573
|
|
|
574
|
+
# Apply default theme if none specified
|
|
575
|
+
# This ensures all karaoke videos use the Nomad theme by default
|
|
576
|
+
effective_theme_id = theme_id
|
|
577
|
+
if effective_theme_id is None:
|
|
578
|
+
theme_service = get_theme_service()
|
|
579
|
+
effective_theme_id = theme_service.get_default_theme_id()
|
|
580
|
+
if effective_theme_id:
|
|
581
|
+
logger.info(f"Applying default theme: {effective_theme_id}")
|
|
582
|
+
|
|
565
583
|
# Resolve CDG/TXT defaults based on theme
|
|
566
584
|
resolved_cdg, resolved_txt = _resolve_cdg_txt_defaults(
|
|
567
|
-
|
|
585
|
+
effective_theme_id, enable_cdg, enable_txt
|
|
568
586
|
)
|
|
569
587
|
|
|
570
588
|
# Check if any custom style files are being uploaded (overrides theme)
|
|
@@ -596,7 +614,7 @@ async def upload_and_create_job(
|
|
|
596
614
|
artist=artist,
|
|
597
615
|
title=title,
|
|
598
616
|
filename=file.filename,
|
|
599
|
-
theme_id=
|
|
617
|
+
theme_id=effective_theme_id,
|
|
600
618
|
color_overrides=parsed_color_overrides,
|
|
601
619
|
enable_cdg=resolved_cdg,
|
|
602
620
|
enable_txt=resolved_txt,
|
|
@@ -623,10 +641,12 @@ async def upload_and_create_job(
|
|
|
623
641
|
request_metadata=request_metadata,
|
|
624
642
|
# Non-interactive mode
|
|
625
643
|
non_interactive=non_interactive,
|
|
644
|
+
# Tenant scoping
|
|
645
|
+
tenant_id=tenant_config.id if tenant_config else None,
|
|
626
646
|
)
|
|
627
647
|
job = job_manager.create_job(job_create)
|
|
628
648
|
job_id = job.job_id
|
|
629
|
-
|
|
649
|
+
|
|
630
650
|
# Record job creation metric
|
|
631
651
|
metrics.record_job_created(job_id, source="upload")
|
|
632
652
|
|
|
@@ -638,16 +658,16 @@ async def upload_and_create_job(
|
|
|
638
658
|
theme_style_params_path = None
|
|
639
659
|
theme_style_assets = {}
|
|
640
660
|
theme_youtube_desc = None
|
|
641
|
-
if
|
|
661
|
+
if effective_theme_id and not has_custom_style_files:
|
|
642
662
|
try:
|
|
643
663
|
theme_style_params_path, theme_style_assets, theme_youtube_desc = _prepare_theme_for_job(
|
|
644
|
-
job_id,
|
|
664
|
+
job_id, effective_theme_id, parsed_color_overrides or None
|
|
645
665
|
)
|
|
646
|
-
logger.info(f"Applied theme '{
|
|
666
|
+
logger.info(f"Applied theme '{effective_theme_id}' to job {job_id}")
|
|
647
667
|
except HTTPException:
|
|
648
668
|
raise # Re-raise validation errors (e.g., theme not found)
|
|
649
669
|
except Exception as e:
|
|
650
|
-
logger.warning(f"Failed to prepare theme '{
|
|
670
|
+
logger.warning(f"Failed to prepare theme '{effective_theme_id}' for job {job_id}: {e}")
|
|
651
671
|
# Continue without theme - job can still be processed with defaults
|
|
652
672
|
|
|
653
673
|
# Upload main audio file to GCS
|
|
@@ -1020,6 +1040,14 @@ async def create_job_with_upload_urls(
|
|
|
1020
1040
|
- Works with any HTTP client (no HTTP/2 required)
|
|
1021
1041
|
- Resumable uploads possible with GCS
|
|
1022
1042
|
"""
|
|
1043
|
+
# Check tenant feature flag
|
|
1044
|
+
tenant_config = get_tenant_config_from_request(request)
|
|
1045
|
+
if tenant_config and not tenant_config.features.file_upload:
|
|
1046
|
+
raise HTTPException(
|
|
1047
|
+
status_code=403,
|
|
1048
|
+
detail="File upload is not available for this portal"
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1023
1051
|
try:
|
|
1024
1052
|
# Validate files list
|
|
1025
1053
|
if not body.files:
|
|
@@ -1097,9 +1125,18 @@ async def create_job_with_upload_urls(
|
|
|
1097
1125
|
# Get original audio filename
|
|
1098
1126
|
audio_file = audio_files[0]
|
|
1099
1127
|
|
|
1128
|
+
# Apply default theme if none specified
|
|
1129
|
+
# This ensures all karaoke videos use the Nomad theme by default
|
|
1130
|
+
effective_theme_id = body.theme_id
|
|
1131
|
+
if effective_theme_id is None:
|
|
1132
|
+
theme_service = get_theme_service()
|
|
1133
|
+
effective_theme_id = theme_service.get_default_theme_id()
|
|
1134
|
+
if effective_theme_id:
|
|
1135
|
+
logger.info(f"Applying default theme: {effective_theme_id}")
|
|
1136
|
+
|
|
1100
1137
|
# Resolve CDG/TXT defaults based on theme
|
|
1101
1138
|
resolved_cdg, resolved_txt = _resolve_cdg_txt_defaults(
|
|
1102
|
-
|
|
1139
|
+
effective_theme_id, body.enable_cdg, body.enable_txt
|
|
1103
1140
|
)
|
|
1104
1141
|
|
|
1105
1142
|
# Check if style_params is being uploaded (overrides theme)
|
|
@@ -1113,7 +1150,7 @@ async def create_job_with_upload_urls(
|
|
|
1113
1150
|
artist=body.artist,
|
|
1114
1151
|
title=body.title,
|
|
1115
1152
|
filename=audio_file.filename,
|
|
1116
|
-
theme_id=
|
|
1153
|
+
theme_id=effective_theme_id,
|
|
1117
1154
|
color_overrides=body.color_overrides or {},
|
|
1118
1155
|
enable_cdg=resolved_cdg,
|
|
1119
1156
|
enable_txt=resolved_txt,
|
|
@@ -1135,6 +1172,8 @@ async def create_job_with_upload_urls(
|
|
|
1135
1172
|
other_stems_models=body.other_stems_models,
|
|
1136
1173
|
request_metadata=request_metadata,
|
|
1137
1174
|
non_interactive=body.non_interactive,
|
|
1175
|
+
# Tenant scoping
|
|
1176
|
+
tenant_id=tenant_config.id if tenant_config else None,
|
|
1138
1177
|
)
|
|
1139
1178
|
job = job_manager.create_job(job_create)
|
|
1140
1179
|
job_id = job.job_id
|
|
@@ -1145,9 +1184,9 @@ async def create_job_with_upload_urls(
|
|
|
1145
1184
|
logger.info(f"Created job {job_id} for {body.artist} - {body.title} (signed URL upload flow)")
|
|
1146
1185
|
|
|
1147
1186
|
# If theme is set and no style_params uploaded, prepare theme style now
|
|
1148
|
-
if
|
|
1187
|
+
if effective_theme_id and not has_style_params_upload:
|
|
1149
1188
|
style_params_path, style_assets, youtube_desc = _prepare_theme_for_job(
|
|
1150
|
-
job_id,
|
|
1189
|
+
job_id, effective_theme_id, body.color_overrides
|
|
1151
1190
|
)
|
|
1152
1191
|
# Update job with theme style data
|
|
1153
1192
|
update_data = {
|
|
@@ -1157,7 +1196,7 @@ async def create_job_with_upload_urls(
|
|
|
1157
1196
|
if youtube_desc and not body.youtube_description:
|
|
1158
1197
|
update_data['youtube_description_template'] = youtube_desc
|
|
1159
1198
|
job_manager.update_job(job_id, update_data)
|
|
1160
|
-
logger.info(f"Applied theme '{
|
|
1199
|
+
logger.info(f"Applied theme '{effective_theme_id}' to job {job_id}")
|
|
1161
1200
|
|
|
1162
1201
|
# Generate signed upload URLs for each file
|
|
1163
1202
|
upload_urls = []
|
|
@@ -1463,6 +1502,14 @@ async def create_job_from_url(
|
|
|
1463
1502
|
Note: YouTube rate limiting may cause occasional download failures.
|
|
1464
1503
|
The backend will retry automatically.
|
|
1465
1504
|
"""
|
|
1505
|
+
# Check tenant feature flag
|
|
1506
|
+
tenant_config = get_tenant_config_from_request(request)
|
|
1507
|
+
if tenant_config and not tenant_config.features.youtube_url:
|
|
1508
|
+
raise HTTPException(
|
|
1509
|
+
status_code=403,
|
|
1510
|
+
detail="URL-based job creation is not available for this portal"
|
|
1511
|
+
)
|
|
1512
|
+
|
|
1466
1513
|
try:
|
|
1467
1514
|
# Validate URL
|
|
1468
1515
|
if not _validate_url(body.url):
|
|
@@ -1521,9 +1568,18 @@ async def create_job_from_url(
|
|
|
1521
1568
|
artist = body.artist
|
|
1522
1569
|
title = body.title
|
|
1523
1570
|
|
|
1571
|
+
# Apply default theme if none specified
|
|
1572
|
+
# This ensures all karaoke videos use the Nomad theme by default
|
|
1573
|
+
effective_theme_id = body.theme_id
|
|
1574
|
+
if effective_theme_id is None:
|
|
1575
|
+
theme_service = get_theme_service()
|
|
1576
|
+
effective_theme_id = theme_service.get_default_theme_id()
|
|
1577
|
+
if effective_theme_id:
|
|
1578
|
+
logger.info(f"Applying default theme: {effective_theme_id}")
|
|
1579
|
+
|
|
1524
1580
|
# Resolve CDG/TXT defaults based on theme
|
|
1525
1581
|
resolved_cdg, resolved_txt = _resolve_cdg_txt_defaults(
|
|
1526
|
-
|
|
1582
|
+
effective_theme_id, body.enable_cdg, body.enable_txt
|
|
1527
1583
|
)
|
|
1528
1584
|
|
|
1529
1585
|
# Prefer authenticated user's email over request body
|
|
@@ -1535,7 +1591,7 @@ async def create_job_from_url(
|
|
|
1535
1591
|
artist=artist,
|
|
1536
1592
|
title=title,
|
|
1537
1593
|
filename=None, # No file uploaded
|
|
1538
|
-
theme_id=
|
|
1594
|
+
theme_id=effective_theme_id,
|
|
1539
1595
|
color_overrides=body.color_overrides or {},
|
|
1540
1596
|
enable_cdg=resolved_cdg,
|
|
1541
1597
|
enable_txt=resolved_txt,
|
|
@@ -1556,6 +1612,8 @@ async def create_job_from_url(
|
|
|
1556
1612
|
other_stems_models=body.other_stems_models,
|
|
1557
1613
|
request_metadata=request_metadata,
|
|
1558
1614
|
non_interactive=body.non_interactive,
|
|
1615
|
+
# Tenant scoping
|
|
1616
|
+
tenant_id=tenant_config.id if tenant_config else None,
|
|
1559
1617
|
)
|
|
1560
1618
|
job = job_manager.create_job(job_create)
|
|
1561
1619
|
job_id = job.job_id
|
|
@@ -1564,9 +1622,9 @@ async def create_job_from_url(
|
|
|
1564
1622
|
metrics.record_job_created(job_id, source="url")
|
|
1565
1623
|
|
|
1566
1624
|
# If theme is set, prepare theme style now
|
|
1567
|
-
if
|
|
1625
|
+
if effective_theme_id:
|
|
1568
1626
|
style_params_path, style_assets, youtube_desc = _prepare_theme_for_job(
|
|
1569
|
-
job_id,
|
|
1627
|
+
job_id, effective_theme_id, body.color_overrides
|
|
1570
1628
|
)
|
|
1571
1629
|
# Update job with theme style data
|
|
1572
1630
|
update_data = {
|
|
@@ -1576,7 +1634,7 @@ async def create_job_from_url(
|
|
|
1576
1634
|
if youtube_desc and not body.youtube_description:
|
|
1577
1635
|
update_data['youtube_description_template'] = youtube_desc
|
|
1578
1636
|
job_manager.update_job(job_id, update_data)
|
|
1579
|
-
logger.info(f"Applied theme '{
|
|
1637
|
+
logger.info(f"Applied theme '{effective_theme_id}' to job {job_id}")
|
|
1580
1638
|
|
|
1581
1639
|
logger.info(f"Created URL-based job {job_id} for URL: {body.url}")
|
|
1582
1640
|
if artist:
|
|
@@ -1676,6 +1734,14 @@ async def create_finalise_only_job(
|
|
|
1676
1734
|
|
|
1677
1735
|
The endpoint returns signed URLs for uploading all the prep files.
|
|
1678
1736
|
"""
|
|
1737
|
+
# Check tenant feature flag - finalise-only requires file upload capability
|
|
1738
|
+
tenant_config = get_tenant_config_from_request(request)
|
|
1739
|
+
if tenant_config and not tenant_config.features.file_upload:
|
|
1740
|
+
raise HTTPException(
|
|
1741
|
+
status_code=403,
|
|
1742
|
+
detail="File upload is not available for this portal"
|
|
1743
|
+
)
|
|
1744
|
+
|
|
1679
1745
|
try:
|
|
1680
1746
|
# Validate files list
|
|
1681
1747
|
if not body.files:
|
|
@@ -1768,9 +1834,18 @@ async def create_finalise_only_job(
|
|
|
1768
1834
|
# Extract request metadata
|
|
1769
1835
|
request_metadata = extract_request_metadata(request, created_from="finalise_only_upload")
|
|
1770
1836
|
|
|
1837
|
+
# Apply default theme if none specified
|
|
1838
|
+
# This ensures all karaoke videos use the Nomad theme by default
|
|
1839
|
+
effective_theme_id = body.theme_id
|
|
1840
|
+
if effective_theme_id is None:
|
|
1841
|
+
theme_service = get_theme_service()
|
|
1842
|
+
effective_theme_id = theme_service.get_default_theme_id()
|
|
1843
|
+
if effective_theme_id:
|
|
1844
|
+
logger.info(f"Applying default theme: {effective_theme_id}")
|
|
1845
|
+
|
|
1771
1846
|
# Resolve CDG/TXT defaults based on theme
|
|
1772
1847
|
resolved_cdg, resolved_txt = _resolve_cdg_txt_defaults(
|
|
1773
|
-
|
|
1848
|
+
effective_theme_id, body.enable_cdg, body.enable_txt
|
|
1774
1849
|
)
|
|
1775
1850
|
|
|
1776
1851
|
# Check if style_params is being uploaded (overrides theme)
|
|
@@ -1784,7 +1859,7 @@ async def create_finalise_only_job(
|
|
|
1784
1859
|
artist=body.artist,
|
|
1785
1860
|
title=body.title,
|
|
1786
1861
|
filename="finalise_only", # No single audio file - using prep outputs
|
|
1787
|
-
theme_id=
|
|
1862
|
+
theme_id=effective_theme_id,
|
|
1788
1863
|
color_overrides=body.color_overrides or {},
|
|
1789
1864
|
enable_cdg=resolved_cdg,
|
|
1790
1865
|
enable_txt=resolved_txt,
|
|
@@ -1798,6 +1873,8 @@ async def create_finalise_only_job(
|
|
|
1798
1873
|
finalise_only=True,
|
|
1799
1874
|
keep_brand_code=body.keep_brand_code,
|
|
1800
1875
|
request_metadata=request_metadata,
|
|
1876
|
+
# Tenant scoping
|
|
1877
|
+
tenant_id=tenant_config.id if tenant_config else None,
|
|
1801
1878
|
)
|
|
1802
1879
|
job = job_manager.create_job(job_create)
|
|
1803
1880
|
job_id = job.job_id
|
|
@@ -1808,9 +1885,9 @@ async def create_finalise_only_job(
|
|
|
1808
1885
|
logger.info(f"Created finalise-only job {job_id} for {body.artist} - {body.title}")
|
|
1809
1886
|
|
|
1810
1887
|
# If theme is set and no style_params uploaded, prepare theme style now
|
|
1811
|
-
if
|
|
1888
|
+
if effective_theme_id and not has_style_params_upload:
|
|
1812
1889
|
style_params_path, style_assets, youtube_desc = _prepare_theme_for_job(
|
|
1813
|
-
job_id,
|
|
1890
|
+
job_id, effective_theme_id, body.color_overrides
|
|
1814
1891
|
)
|
|
1815
1892
|
# Update job with theme style data
|
|
1816
1893
|
update_data = {
|
|
@@ -1820,7 +1897,7 @@ async def create_finalise_only_job(
|
|
|
1820
1897
|
if youtube_desc and not body.youtube_description:
|
|
1821
1898
|
update_data['youtube_description_template'] = youtube_desc
|
|
1822
1899
|
job_manager.update_job(job_id, update_data)
|
|
1823
|
-
logger.info(f"Applied theme '{
|
|
1900
|
+
logger.info(f"Applied theme '{effective_theme_id}' to job {job_id}")
|
|
1824
1901
|
|
|
1825
1902
|
# Generate signed upload URLs for each file
|
|
1826
1903
|
upload_urls = []
|
backend/api/routes/health.py
CHANGED
|
@@ -13,6 +13,9 @@ from backend.services.flacfetch_client import get_flacfetch_client
|
|
|
13
13
|
from backend.services.email_service import get_email_service
|
|
14
14
|
from backend.services.stripe_service import get_stripe_service
|
|
15
15
|
from backend.services.encoding_service import get_encoding_service
|
|
16
|
+
from backend.services.spacy_preloader import get_preloaded_model, is_model_preloaded
|
|
17
|
+
from backend.services.nltk_preloader import get_preloaded_cmudict, is_cmudict_preloaded
|
|
18
|
+
from backend.services.langfuse_preloader import get_preloaded_langfuse_handler, is_langfuse_preloaded, is_langfuse_configured
|
|
16
19
|
|
|
17
20
|
router = APIRouter()
|
|
18
21
|
logger = logging.getLogger(__name__)
|
|
@@ -334,6 +337,68 @@ async def detailed_health_check() -> Dict[str, Any]:
|
|
|
334
337
|
}
|
|
335
338
|
|
|
336
339
|
|
|
340
|
+
@router.get("/health/preload-status")
|
|
341
|
+
async def preload_status() -> Dict[str, Any]:
|
|
342
|
+
"""
|
|
343
|
+
Check status of preloaded resources for performance optimization.
|
|
344
|
+
|
|
345
|
+
Use this endpoint to verify that NLTK, SpaCy, and Langfuse resources
|
|
346
|
+
were successfully preloaded at container startup. If any show as
|
|
347
|
+
not preloaded, check Cloud Run startup logs for errors.
|
|
348
|
+
|
|
349
|
+
Expected state after successful deployment:
|
|
350
|
+
- spacy.preloaded: true
|
|
351
|
+
- nltk.preloaded: true
|
|
352
|
+
- langfuse.preloaded: true (if configured) or configured: false
|
|
353
|
+
"""
|
|
354
|
+
# SpaCy status
|
|
355
|
+
spacy_model = get_preloaded_model("en_core_web_sm")
|
|
356
|
+
spacy_status = {
|
|
357
|
+
"preloaded": is_model_preloaded("en_core_web_sm"),
|
|
358
|
+
"model": "en_core_web_sm",
|
|
359
|
+
}
|
|
360
|
+
if spacy_model:
|
|
361
|
+
spacy_status["vocab_size"] = len(spacy_model.vocab)
|
|
362
|
+
|
|
363
|
+
# NLTK status
|
|
364
|
+
cmudict = get_preloaded_cmudict()
|
|
365
|
+
nltk_status = {
|
|
366
|
+
"preloaded": is_cmudict_preloaded(),
|
|
367
|
+
"resource": "cmudict",
|
|
368
|
+
}
|
|
369
|
+
if cmudict:
|
|
370
|
+
nltk_status["entries"] = len(cmudict)
|
|
371
|
+
|
|
372
|
+
# Langfuse status
|
|
373
|
+
langfuse_handler = get_preloaded_langfuse_handler()
|
|
374
|
+
langfuse_status = {
|
|
375
|
+
"configured": is_langfuse_configured(),
|
|
376
|
+
"preloaded": is_langfuse_preloaded(),
|
|
377
|
+
}
|
|
378
|
+
if langfuse_handler:
|
|
379
|
+
langfuse_status["handler_type"] = type(langfuse_handler).__name__
|
|
380
|
+
|
|
381
|
+
# Overall status
|
|
382
|
+
all_preloaded = (
|
|
383
|
+
spacy_status["preloaded"]
|
|
384
|
+
and nltk_status["preloaded"]
|
|
385
|
+
and (langfuse_status["preloaded"] or not langfuse_status["configured"])
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
"status": "ok" if all_preloaded else "degraded",
|
|
390
|
+
"message": "All resources preloaded" if all_preloaded else "Some resources not preloaded - check startup logs",
|
|
391
|
+
"spacy": spacy_status,
|
|
392
|
+
"nltk": nltk_status,
|
|
393
|
+
"langfuse": langfuse_status,
|
|
394
|
+
"performance_impact": {
|
|
395
|
+
"spacy_preload": "Saves ~60s on first lyrics correction",
|
|
396
|
+
"nltk_preload": "Saves ~100-150s on SyllablesMatchHandler init",
|
|
397
|
+
"langfuse_preload": "Saves ~200s on AgenticCorrector init",
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
|
|
337
402
|
@router.get("/readiness")
|
|
338
403
|
async def readiness_check() -> Dict[str, str]:
|
|
339
404
|
"""Readiness check endpoint for Cloud Run."""
|
backend/api/routes/internal.py
CHANGED
|
@@ -371,6 +371,12 @@ async def check_idle_reminder(
|
|
|
371
371
|
add_span_event("already_sent")
|
|
372
372
|
return {"status": "already_sent", "job_id": job_id, "message": "Reminder already sent"}
|
|
373
373
|
|
|
374
|
+
# Skip reminders for made-for-you jobs (admin handles these directly, no intermediate customer emails)
|
|
375
|
+
if getattr(job, 'made_for_you', False):
|
|
376
|
+
logger.info(f"[job:{job_id}] Made-for-you job, skipping customer reminder (admin handles)")
|
|
377
|
+
add_span_event("made_for_you_skip")
|
|
378
|
+
return {"status": "skipped", "job_id": job_id, "message": "Made-for-you job - admin handles directly"}
|
|
379
|
+
|
|
374
380
|
# Check if user has an email
|
|
375
381
|
if not job.user_email:
|
|
376
382
|
logger.warning(f"[job:{job_id}] No user email, cannot send reminder")
|
backend/api/routes/jobs.py
CHANGED
|
@@ -11,7 +11,7 @@ import asyncio
|
|
|
11
11
|
import logging
|
|
12
12
|
import httpx
|
|
13
13
|
from typing import List, Optional, Dict, Any, Tuple
|
|
14
|
-
from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends
|
|
14
|
+
from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends, Request
|
|
15
15
|
|
|
16
16
|
from backend.models.job import Job, JobCreate, JobResponse, JobStatus
|
|
17
17
|
from backend.models.requests import (
|
|
@@ -25,10 +25,13 @@ from backend.models.requests import (
|
|
|
25
25
|
from backend.services.job_manager import JobManager
|
|
26
26
|
from backend.services.worker_service import get_worker_service
|
|
27
27
|
from backend.services.storage_service import StorageService
|
|
28
|
+
from backend.services.theme_service import get_theme_service
|
|
28
29
|
from backend.config import get_settings
|
|
29
30
|
from backend.api.dependencies import require_admin, require_auth, require_instrumental_auth
|
|
30
31
|
from backend.services.auth_service import UserType, AuthResult
|
|
31
32
|
from backend.services.metrics import metrics
|
|
33
|
+
from backend.middleware.tenant import get_tenant_from_request
|
|
34
|
+
from backend.utils.test_data import is_test_email
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
logger = logging.getLogger(__name__)
|
|
@@ -92,11 +95,22 @@ async def create_job(
|
|
|
92
95
|
settings = get_settings()
|
|
93
96
|
effective_enable_youtube_upload = request.enable_youtube_upload if request.enable_youtube_upload is not None else settings.default_enable_youtube_upload
|
|
94
97
|
|
|
98
|
+
# Apply default theme - all jobs require a theme
|
|
99
|
+
theme_service = get_theme_service()
|
|
100
|
+
effective_theme_id = theme_service.get_default_theme_id()
|
|
101
|
+
if not effective_theme_id:
|
|
102
|
+
raise HTTPException(
|
|
103
|
+
status_code=422,
|
|
104
|
+
detail="No default theme configured. Please contact support or specify a theme_id."
|
|
105
|
+
)
|
|
106
|
+
logger.info(f"Applying default theme: {effective_theme_id}")
|
|
107
|
+
|
|
95
108
|
# Create job with all preferences
|
|
96
109
|
job_create = JobCreate(
|
|
97
110
|
url=str(request.url),
|
|
98
111
|
artist=request.artist,
|
|
99
112
|
title=request.title,
|
|
113
|
+
theme_id=effective_theme_id, # Required - all jobs must have a theme
|
|
100
114
|
enable_cdg=request.enable_cdg,
|
|
101
115
|
enable_txt=request.enable_txt,
|
|
102
116
|
enable_youtube_upload=effective_enable_youtube_upload,
|
|
@@ -177,11 +191,13 @@ def _check_job_ownership(job: Job, auth_result: AuthResult) -> bool:
|
|
|
177
191
|
|
|
178
192
|
@router.get("", response_model=List[Job])
|
|
179
193
|
async def list_jobs(
|
|
194
|
+
request: Request,
|
|
180
195
|
status: Optional[JobStatus] = None,
|
|
181
196
|
environment: Optional[str] = None,
|
|
182
197
|
client_id: Optional[str] = None,
|
|
183
198
|
created_after: Optional[str] = None,
|
|
184
199
|
created_before: Optional[str] = None,
|
|
200
|
+
exclude_test: bool = True,
|
|
185
201
|
limit: int = 100,
|
|
186
202
|
auth_result: AuthResult = Depends(require_auth)
|
|
187
203
|
) -> List[Job]:
|
|
@@ -189,6 +205,7 @@ async def list_jobs(
|
|
|
189
205
|
List jobs with optional filters.
|
|
190
206
|
|
|
191
207
|
Regular users only see their own jobs. Admins see all jobs.
|
|
208
|
+
Users on tenant portals only see jobs from their tenant.
|
|
192
209
|
|
|
193
210
|
Args:
|
|
194
211
|
status: Filter by job status (pending, complete, failed, etc.)
|
|
@@ -196,6 +213,7 @@ async def list_jobs(
|
|
|
196
213
|
client_id: Filter by request_metadata.client_id (customer identifier)
|
|
197
214
|
created_after: Filter jobs created after this ISO datetime (e.g., 2024-01-01T00:00:00Z)
|
|
198
215
|
created_before: Filter jobs created before this ISO datetime
|
|
216
|
+
exclude_test: If True (default), exclude jobs from test users (admin only)
|
|
199
217
|
limit: Maximum number of jobs to return (default 100)
|
|
200
218
|
|
|
201
219
|
Returns:
|
|
@@ -232,6 +250,10 @@ async def list_jobs(
|
|
|
232
250
|
logger.warning("Non-admin auth without user_email, returning empty job list")
|
|
233
251
|
return []
|
|
234
252
|
|
|
253
|
+
# Get tenant_id from request for portal scoping
|
|
254
|
+
# Tenant users only see jobs from their tenant
|
|
255
|
+
tenant_id = get_tenant_from_request(request)
|
|
256
|
+
|
|
235
257
|
jobs = job_manager.list_jobs(
|
|
236
258
|
status=status,
|
|
237
259
|
environment=environment,
|
|
@@ -239,9 +261,14 @@ async def list_jobs(
|
|
|
239
261
|
created_after=created_after_dt,
|
|
240
262
|
created_before=created_before_dt,
|
|
241
263
|
user_email=user_email_filter,
|
|
264
|
+
tenant_id=tenant_id,
|
|
242
265
|
limit=limit
|
|
243
266
|
)
|
|
244
267
|
|
|
268
|
+
# Filter out test user jobs if exclude_test is True (admin only)
|
|
269
|
+
if exclude_test and auth_result.is_admin:
|
|
270
|
+
jobs = [j for j in jobs if not is_test_email(j.user_email or "")]
|
|
271
|
+
|
|
245
272
|
logger.debug(f"Listed {len(jobs)} jobs for user={auth_result.user_email}, admin={auth_result.is_admin}")
|
|
246
273
|
return jobs
|
|
247
274
|
except HTTPException:
|
backend/api/routes/review.py
CHANGED
|
@@ -412,7 +412,10 @@ async def generate_preview_video(
|
|
|
412
412
|
|
|
413
413
|
# Check if GCE preview encoding is enabled
|
|
414
414
|
use_gce_preview = encoding_service.is_preview_enabled
|
|
415
|
-
|
|
415
|
+
|
|
416
|
+
# Check if user wants theme background image (slower) or black background (faster, default)
|
|
417
|
+
use_background_image = updated_data.get("use_background_image", False)
|
|
418
|
+
logger.info(f"Job {job_id}: Generating preview video (GCE: {use_gce_preview}, background image: {use_background_image})")
|
|
416
419
|
|
|
417
420
|
# Use tracing and job_log_context for full observability
|
|
418
421
|
with create_span("generate-preview-video", {"job_id": job_id, "use_gce": use_gce_preview}) as span:
|
|
@@ -498,12 +501,16 @@ async def generate_preview_video(
|
|
|
498
501
|
# Get background image and font from style assets if available
|
|
499
502
|
style_assets = job.style_assets or {}
|
|
500
503
|
|
|
504
|
+
# Only use background image if user explicitly requested it
|
|
505
|
+
# Default is black background for faster preview generation (~10s vs ~30-60s)
|
|
501
506
|
background_image_gcs_path = None
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
+
if use_background_image:
|
|
508
|
+
for key in ["karaoke_background", "style_karaoke_background"]:
|
|
509
|
+
if key in style_assets:
|
|
510
|
+
background_image_gcs_path = f"gs://{bucket_name}/{style_assets[key]}"
|
|
511
|
+
gce_span.set_attribute("background_image", background_image_gcs_path)
|
|
512
|
+
break
|
|
513
|
+
gce_span.set_attribute("use_background_image", use_background_image)
|
|
507
514
|
|
|
508
515
|
font_gcs_path = None
|
|
509
516
|
for key in ["font", "style_font"]:
|