karaoke-gen 0.99.3__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 +512 -1
- backend/api/routes/audio_search.py +13 -2
- backend/api/routes/file_upload.py +42 -1
- backend/api/routes/internal.py +6 -0
- backend/api/routes/jobs.py +9 -1
- backend/api/routes/review.py +13 -6
- backend/api/routes/tenant.py +120 -0
- backend/api/routes/users.py +167 -245
- backend/main.py +6 -1
- 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/firestore_service.py +6 -0
- backend/services/job_manager.py +32 -1
- backend/services/stripe_service.py +61 -35
- backend/services/tenant_service.py +285 -0
- backend/services/user_service.py +85 -7
- 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 +146 -1
- backend/tests/test_made_for_you.py +2086 -0
- backend/tests/test_models.py +139 -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/workers/video_worker.py +8 -3
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/METADATA +1 -1
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/RECORD +42 -28
- lyrics_transcriber/frontend/src/api.ts +13 -5
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +90 -57
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/licenses/LICENSE +0 -0
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 (
|
|
@@ -30,6 +30,7 @@ from backend.config import get_settings
|
|
|
30
30
|
from backend.api.dependencies import require_admin, require_auth, require_instrumental_auth
|
|
31
31
|
from backend.services.auth_service import UserType, AuthResult
|
|
32
32
|
from backend.services.metrics import metrics
|
|
33
|
+
from backend.middleware.tenant import get_tenant_from_request
|
|
33
34
|
from backend.utils.test_data import is_test_email
|
|
34
35
|
|
|
35
36
|
|
|
@@ -190,6 +191,7 @@ def _check_job_ownership(job: Job, auth_result: AuthResult) -> bool:
|
|
|
190
191
|
|
|
191
192
|
@router.get("", response_model=List[Job])
|
|
192
193
|
async def list_jobs(
|
|
194
|
+
request: Request,
|
|
193
195
|
status: Optional[JobStatus] = None,
|
|
194
196
|
environment: Optional[str] = None,
|
|
195
197
|
client_id: Optional[str] = None,
|
|
@@ -203,6 +205,7 @@ async def list_jobs(
|
|
|
203
205
|
List jobs with optional filters.
|
|
204
206
|
|
|
205
207
|
Regular users only see their own jobs. Admins see all jobs.
|
|
208
|
+
Users on tenant portals only see jobs from their tenant.
|
|
206
209
|
|
|
207
210
|
Args:
|
|
208
211
|
status: Filter by job status (pending, complete, failed, etc.)
|
|
@@ -247,6 +250,10 @@ async def list_jobs(
|
|
|
247
250
|
logger.warning("Non-admin auth without user_email, returning empty job list")
|
|
248
251
|
return []
|
|
249
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
|
+
|
|
250
257
|
jobs = job_manager.list_jobs(
|
|
251
258
|
status=status,
|
|
252
259
|
environment=environment,
|
|
@@ -254,6 +261,7 @@ async def list_jobs(
|
|
|
254
261
|
created_after=created_after_dt,
|
|
255
262
|
created_before=created_before_dt,
|
|
256
263
|
user_email=user_email_filter,
|
|
264
|
+
tenant_id=tenant_id,
|
|
257
265
|
limit=limit
|
|
258
266
|
)
|
|
259
267
|
|
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"]:
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tenant API routes for white-label portal configuration.
|
|
3
|
+
|
|
4
|
+
These endpoints are public (no auth required) since the frontend needs
|
|
5
|
+
to fetch tenant branding before the user logs in.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from fastapi import APIRouter, Query, Request
|
|
12
|
+
|
|
13
|
+
from backend.models.tenant import TenantConfigResponse, TenantPublicConfig
|
|
14
|
+
from backend.services.tenant_service import get_tenant_service
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
router = APIRouter(prefix="/api/tenant", tags=["tenant"])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.get("/config", response_model=TenantConfigResponse)
|
|
22
|
+
async def get_tenant_config(
|
|
23
|
+
request: Request,
|
|
24
|
+
tenant: Optional[str] = Query(
|
|
25
|
+
None,
|
|
26
|
+
description="Tenant ID override (for development). If not provided, detected from subdomain.",
|
|
27
|
+
),
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Get tenant configuration for the current portal.
|
|
31
|
+
|
|
32
|
+
This endpoint detects the tenant from:
|
|
33
|
+
1. Query parameter `tenant` (for development/testing)
|
|
34
|
+
2. X-Tenant-ID header (set by frontend)
|
|
35
|
+
3. Host header subdomain
|
|
36
|
+
|
|
37
|
+
Returns the public tenant configuration (branding, features, defaults)
|
|
38
|
+
or indicates this is the default Nomad Karaoke portal.
|
|
39
|
+
"""
|
|
40
|
+
tenant_service = get_tenant_service()
|
|
41
|
+
|
|
42
|
+
# Priority 1: Query parameter (for dev/testing)
|
|
43
|
+
tenant_id = tenant
|
|
44
|
+
|
|
45
|
+
# Priority 2: X-Tenant-ID header
|
|
46
|
+
if not tenant_id:
|
|
47
|
+
tenant_id = request.headers.get("X-Tenant-ID")
|
|
48
|
+
|
|
49
|
+
# Priority 3: Detect from Host header
|
|
50
|
+
if not tenant_id:
|
|
51
|
+
host = request.headers.get("Host", "")
|
|
52
|
+
# Check if this is a tenant subdomain
|
|
53
|
+
# Pattern: {tenant}.nomadkaraoke.com or {tenant}.gen.nomadkaraoke.com
|
|
54
|
+
if host and "nomadkaraoke.com" in host.lower():
|
|
55
|
+
parts = host.lower().split(".")
|
|
56
|
+
# Skip known non-tenant subdomains
|
|
57
|
+
if parts[0] not in ["gen", "api", "www", "buy", "admin"]:
|
|
58
|
+
potential_tenant = parts[0]
|
|
59
|
+
if tenant_service.tenant_exists(potential_tenant):
|
|
60
|
+
tenant_id = potential_tenant
|
|
61
|
+
|
|
62
|
+
# If no tenant detected, return default config
|
|
63
|
+
if not tenant_id:
|
|
64
|
+
logger.debug("No tenant detected, returning default config")
|
|
65
|
+
return TenantConfigResponse(tenant=None, is_default=True)
|
|
66
|
+
|
|
67
|
+
# Load tenant config
|
|
68
|
+
public_config = tenant_service.get_public_config(tenant_id)
|
|
69
|
+
|
|
70
|
+
if not public_config:
|
|
71
|
+
logger.warning(f"Tenant not found: {tenant_id}")
|
|
72
|
+
return TenantConfigResponse(tenant=None, is_default=True)
|
|
73
|
+
|
|
74
|
+
if not public_config.is_active:
|
|
75
|
+
logger.warning(f"Tenant is inactive: {tenant_id}")
|
|
76
|
+
return TenantConfigResponse(tenant=None, is_default=True)
|
|
77
|
+
|
|
78
|
+
logger.info(f"Returning config for tenant: {tenant_id}")
|
|
79
|
+
return TenantConfigResponse(tenant=public_config, is_default=False)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@router.get("/config/{tenant_id}", response_model=TenantConfigResponse)
|
|
83
|
+
async def get_tenant_config_by_id(tenant_id: str):
|
|
84
|
+
"""
|
|
85
|
+
Get tenant configuration by explicit tenant ID.
|
|
86
|
+
|
|
87
|
+
This is useful for admin tools or debugging.
|
|
88
|
+
"""
|
|
89
|
+
tenant_service = get_tenant_service()
|
|
90
|
+
public_config = tenant_service.get_public_config(tenant_id)
|
|
91
|
+
|
|
92
|
+
if not public_config:
|
|
93
|
+
return TenantConfigResponse(tenant=None, is_default=True)
|
|
94
|
+
|
|
95
|
+
# Check if tenant is active
|
|
96
|
+
if not public_config.is_active:
|
|
97
|
+
logger.warning(f"Tenant is inactive: {tenant_id}")
|
|
98
|
+
return TenantConfigResponse(tenant=None, is_default=True)
|
|
99
|
+
|
|
100
|
+
return TenantConfigResponse(tenant=public_config, is_default=False)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@router.get("/asset/{tenant_id}/{asset_name}")
|
|
104
|
+
async def get_tenant_asset(tenant_id: str, asset_name: str):
|
|
105
|
+
"""
|
|
106
|
+
Get a signed URL for a tenant asset (logo, favicon, etc.).
|
|
107
|
+
|
|
108
|
+
This redirects to the signed GCS URL for the asset.
|
|
109
|
+
"""
|
|
110
|
+
from fastapi.responses import RedirectResponse
|
|
111
|
+
|
|
112
|
+
tenant_service = get_tenant_service()
|
|
113
|
+
url = tenant_service.get_asset_url(tenant_id, asset_name)
|
|
114
|
+
|
|
115
|
+
if not url:
|
|
116
|
+
from fastapi import HTTPException
|
|
117
|
+
|
|
118
|
+
raise HTTPException(status_code=404, detail="Asset not found")
|
|
119
|
+
|
|
120
|
+
return RedirectResponse(url=url)
|