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.
Files changed (42) hide show
  1. backend/api/routes/admin.py +512 -1
  2. backend/api/routes/audio_search.py +13 -2
  3. backend/api/routes/file_upload.py +42 -1
  4. backend/api/routes/internal.py +6 -0
  5. backend/api/routes/jobs.py +9 -1
  6. backend/api/routes/review.py +13 -6
  7. backend/api/routes/tenant.py +120 -0
  8. backend/api/routes/users.py +167 -245
  9. backend/main.py +6 -1
  10. backend/middleware/__init__.py +7 -1
  11. backend/middleware/tenant.py +192 -0
  12. backend/models/job.py +19 -3
  13. backend/models/tenant.py +208 -0
  14. backend/models/user.py +18 -0
  15. backend/services/email_service.py +253 -6
  16. backend/services/firestore_service.py +6 -0
  17. backend/services/job_manager.py +32 -1
  18. backend/services/stripe_service.py +61 -35
  19. backend/services/tenant_service.py +285 -0
  20. backend/services/user_service.py +85 -7
  21. backend/tests/emulator/test_made_for_you_integration.py +167 -0
  22. backend/tests/test_admin_job_files.py +337 -0
  23. backend/tests/test_admin_job_reset.py +384 -0
  24. backend/tests/test_admin_job_update.py +326 -0
  25. backend/tests/test_email_service.py +233 -0
  26. backend/tests/test_impersonation.py +223 -0
  27. backend/tests/test_job_creation_regression.py +4 -0
  28. backend/tests/test_job_manager.py +146 -1
  29. backend/tests/test_made_for_you.py +2086 -0
  30. backend/tests/test_models.py +139 -0
  31. backend/tests/test_tenant_api.py +350 -0
  32. backend/tests/test_tenant_middleware.py +345 -0
  33. backend/tests/test_tenant_models.py +406 -0
  34. backend/tests/test_tenant_service.py +418 -0
  35. backend/workers/video_worker.py +8 -3
  36. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/METADATA +1 -1
  37. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/RECORD +42 -28
  38. lyrics_transcriber/frontend/src/api.ts +13 -5
  39. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +90 -57
  40. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/WHEEL +0 -0
  41. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/entry_points.txt +0 -0
  42. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
 
@@ -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
- logger.info(f"Job {job_id}: Generating preview video (GCE preview: {use_gce_preview})")
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
- for key in ["karaoke_background", "style_karaoke_background"]:
503
- if key in style_assets:
504
- background_image_gcs_path = f"gs://{bucket_name}/{style_assets[key]}"
505
- gce_span.set_attribute("background_image", background_image_gcs_path)
506
- break
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)