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
backend/config.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for the karaoke generation backend.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Optional, Dict
|
|
7
|
+
from pydantic_settings import BaseSettings
|
|
8
|
+
from google.cloud import secretmanager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Settings(BaseSettings):
|
|
15
|
+
"""Application settings."""
|
|
16
|
+
|
|
17
|
+
# Google Cloud
|
|
18
|
+
google_cloud_project: str = os.getenv("GOOGLE_CLOUD_PROJECT", "")
|
|
19
|
+
gcs_bucket_name: str = os.getenv("GCS_BUCKET_NAME", "karaoke-gen-storage")
|
|
20
|
+
gcs_temp_bucket: str = os.getenv("GCS_TEMP_BUCKET", "karaoke-gen-temp")
|
|
21
|
+
gcs_output_bucket: str = os.getenv("GCS_OUTPUT_BUCKET", "karaoke-gen-outputs")
|
|
22
|
+
firestore_collection: str = os.getenv("FIRESTORE_COLLECTION", "jobs")
|
|
23
|
+
|
|
24
|
+
# Audio Separator API (for GPU processing)
|
|
25
|
+
audio_separator_api_url: Optional[str] = os.getenv("AUDIO_SEPARATOR_API_URL")
|
|
26
|
+
|
|
27
|
+
# External APIs (can be set via env or Secret Manager)
|
|
28
|
+
audioshake_api_key: Optional[str] = os.getenv("AUDIOSHAKE_API_KEY")
|
|
29
|
+
genius_api_key: Optional[str] = os.getenv("GENIUS_API_KEY")
|
|
30
|
+
spotify_cookie: Optional[str] = os.getenv("SPOTIFY_COOKIE_SP_DC")
|
|
31
|
+
rapidapi_key: Optional[str] = os.getenv("RAPIDAPI_KEY")
|
|
32
|
+
|
|
33
|
+
# Authentication
|
|
34
|
+
admin_tokens: Optional[str] = os.getenv("ADMIN_TOKENS") # Comma-separated list
|
|
35
|
+
|
|
36
|
+
# Application
|
|
37
|
+
environment: str = os.getenv("ENVIRONMENT", "development")
|
|
38
|
+
log_level: str = os.getenv("LOG_LEVEL", "INFO")
|
|
39
|
+
|
|
40
|
+
# Processing
|
|
41
|
+
max_concurrent_jobs: int = int(os.getenv("MAX_CONCURRENT_JOBS", "5"))
|
|
42
|
+
job_timeout_seconds: int = int(os.getenv("JOB_TIMEOUT_SECONDS", "3600"))
|
|
43
|
+
|
|
44
|
+
# Agentic AI Correction (for lyrics correction via LLM)
|
|
45
|
+
# When enabled, uses Gemini via Vertex AI for intelligent lyrics correction
|
|
46
|
+
use_agentic_ai: bool = os.getenv("USE_AGENTIC_AI", "true").lower() in ("true", "1", "yes")
|
|
47
|
+
agentic_ai_model: str = os.getenv("AGENTIC_AI_MODEL", "vertexai/gemini-3-flash-preview")
|
|
48
|
+
# Timeout for agentic correction in seconds. If correction takes longer, abort and
|
|
49
|
+
# use uncorrected transcription - human review will fix any issues.
|
|
50
|
+
agentic_correction_timeout_seconds: int = int(os.getenv("AGENTIC_CORRECTION_TIMEOUT_SECONDS", "180"))
|
|
51
|
+
|
|
52
|
+
# Cloud Tasks (for scalable worker coordination)
|
|
53
|
+
# When enabled, workers are triggered via Cloud Tasks for guaranteed delivery
|
|
54
|
+
# When disabled (default), workers are triggered via direct HTTP (for development)
|
|
55
|
+
enable_cloud_tasks: bool = os.getenv("ENABLE_CLOUD_TASKS", "false").lower() in ("true", "1", "yes")
|
|
56
|
+
gcp_region: str = os.getenv("GCP_REGION", "us-central1")
|
|
57
|
+
|
|
58
|
+
# Cloud Run Jobs (for long-running video encoding)
|
|
59
|
+
# When enabled AND enable_cloud_tasks is true, video worker uses Cloud Run Jobs
|
|
60
|
+
# instead of Cloud Tasks. This supports encoding times >30 minutes (up to 24 hours).
|
|
61
|
+
# Default is false - Cloud Tasks is sufficient for most videos (15-20 min).
|
|
62
|
+
use_cloud_run_jobs_for_video: bool = os.getenv("USE_CLOUD_RUN_JOBS_FOR_VIDEO", "false").lower() in ("true", "1", "yes")
|
|
63
|
+
|
|
64
|
+
# GCE Encoding Worker (for high-performance video encoding)
|
|
65
|
+
# When enabled, video encoding is offloaded to a dedicated C4 GCE instance
|
|
66
|
+
# with faster CPU (Intel Granite Rapids 3.9 GHz) instead of Cloud Run.
|
|
67
|
+
# This provides 2-3x faster encoding times for CPU-bound FFmpeg libx264 encoding.
|
|
68
|
+
use_gce_encoding: bool = os.getenv("USE_GCE_ENCODING", "false").lower() in ("true", "1", "yes")
|
|
69
|
+
encoding_worker_url: Optional[str] = os.getenv("ENCODING_WORKER_URL") # e.g., http://136.119.50.148:8080
|
|
70
|
+
encoding_worker_api_key: Optional[str] = os.getenv("ENCODING_WORKER_API_KEY")
|
|
71
|
+
|
|
72
|
+
# GCE Preview Encoding (for faster preview video generation)
|
|
73
|
+
# When enabled, preview video encoding during lyrics review is offloaded to the GCE worker.
|
|
74
|
+
# This reduces preview generation time from 60+ seconds to ~15-20 seconds.
|
|
75
|
+
# Requires use_gce_encoding to be enabled and the GCE worker to support /encode-preview endpoint.
|
|
76
|
+
use_gce_preview_encoding: bool = os.getenv("USE_GCE_PREVIEW_ENCODING", "false").lower() in ("true", "1", "yes")
|
|
77
|
+
|
|
78
|
+
# Storage paths
|
|
79
|
+
temp_dir: str = os.getenv("TEMP_DIR", "/tmp/karaoke-gen")
|
|
80
|
+
|
|
81
|
+
# Worker logs storage mode
|
|
82
|
+
# When enabled, worker logs are stored in a Firestore subcollection (jobs/{job_id}/logs)
|
|
83
|
+
# instead of an embedded array. This avoids the 1MB document size limit.
|
|
84
|
+
# Default is true for new deployments.
|
|
85
|
+
use_log_subcollection: bool = os.getenv("USE_LOG_SUBCOLLECTION", "true").lower() in ("true", "1", "yes")
|
|
86
|
+
|
|
87
|
+
# Flacfetch remote service (for torrent downloads)
|
|
88
|
+
# When configured, audio search uses the remote flacfetch HTTP API instead of local flacfetch.
|
|
89
|
+
# This is required for torrent downloads since Cloud Run doesn't support BitTorrent.
|
|
90
|
+
flacfetch_api_url: Optional[str] = os.getenv("FLACFETCH_API_URL") # e.g., http://10.0.0.5:8080
|
|
91
|
+
flacfetch_api_key: Optional[str] = os.getenv("FLACFETCH_API_KEY")
|
|
92
|
+
|
|
93
|
+
# Default distribution settings (can be overridden per-request)
|
|
94
|
+
default_dropbox_path: Optional[str] = os.getenv("DEFAULT_DROPBOX_PATH")
|
|
95
|
+
default_gdrive_folder_id: Optional[str] = os.getenv("DEFAULT_GDRIVE_FOLDER_ID")
|
|
96
|
+
# Strip whitespace/newlines from webhook URL - common issue when env vars are set with trailing newlines
|
|
97
|
+
default_discord_webhook_url: Optional[str] = (
|
|
98
|
+
os.getenv("DEFAULT_DISCORD_WEBHOOK_URL", "").strip() or None
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Default values for web service jobs (YouTube/Dropbox distribution)
|
|
102
|
+
default_enable_youtube_upload: bool = os.getenv("DEFAULT_ENABLE_YOUTUBE_UPLOAD", "false").lower() in ("true", "1", "yes")
|
|
103
|
+
default_brand_prefix: Optional[str] = os.getenv("DEFAULT_BRAND_PREFIX")
|
|
104
|
+
default_youtube_description: str = os.getenv(
|
|
105
|
+
"DEFAULT_YOUTUBE_DESCRIPTION",
|
|
106
|
+
"Karaoke video created with Nomad Karaoke (https://nomadkaraoke.com)\n\n"
|
|
107
|
+
"AI-powered vocal separation and synchronized lyrics.\n\n"
|
|
108
|
+
"#karaoke #music #singing #instrumental #lyrics"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Secret Manager cache
|
|
112
|
+
_secret_cache: Dict[str, str] = {}
|
|
113
|
+
|
|
114
|
+
class Config:
|
|
115
|
+
env_file = ".env"
|
|
116
|
+
case_sensitive = False
|
|
117
|
+
|
|
118
|
+
def get_secret(self, secret_id: str) -> Optional[str]:
|
|
119
|
+
"""
|
|
120
|
+
Get a secret from Google Secret Manager.
|
|
121
|
+
|
|
122
|
+
Caches secrets in memory to avoid repeated API calls.
|
|
123
|
+
Falls back to environment variables if Secret Manager unavailable.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
secret_id: Secret name (e.g., "audioshake-api-key")
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Secret value or None if not found
|
|
130
|
+
"""
|
|
131
|
+
# Check cache first
|
|
132
|
+
if secret_id in self._secret_cache:
|
|
133
|
+
return self._secret_cache[secret_id]
|
|
134
|
+
|
|
135
|
+
# Check environment variable (development mode)
|
|
136
|
+
env_var = secret_id.upper().replace('-', '_')
|
|
137
|
+
env_value = os.getenv(env_var)
|
|
138
|
+
if env_value:
|
|
139
|
+
logger.debug(f"Using {secret_id} from environment variable")
|
|
140
|
+
self._secret_cache[secret_id] = env_value
|
|
141
|
+
return env_value
|
|
142
|
+
|
|
143
|
+
# Try Secret Manager (production mode)
|
|
144
|
+
if not self.google_cloud_project:
|
|
145
|
+
logger.warning(f"Cannot fetch secret {secret_id}: No GCP project configured")
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
client = secretmanager.SecretManagerServiceClient()
|
|
150
|
+
name = f"projects/{self.google_cloud_project}/secrets/{secret_id}/versions/latest"
|
|
151
|
+
response = client.access_secret_version(request={"name": name})
|
|
152
|
+
# Strip whitespace/newlines - common issue when secrets are created with trailing newlines
|
|
153
|
+
secret_value = response.payload.data.decode('UTF-8').strip()
|
|
154
|
+
|
|
155
|
+
# Cache it
|
|
156
|
+
self._secret_cache[secret_id] = secret_value
|
|
157
|
+
logger.info(f"Loaded secret {secret_id} from Secret Manager")
|
|
158
|
+
return secret_value
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error(f"Failed to load secret {secret_id} from Secret Manager: {e}")
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Global settings instance
|
|
166
|
+
settings = Settings()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_settings() -> Settings:
|
|
170
|
+
"""Get the global settings instance."""
|
|
171
|
+
return settings
|
|
172
|
+
|
backend/main.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI application entry point for karaoke generation backend.
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
8
|
+
|
|
9
|
+
from backend.config import settings
|
|
10
|
+
from backend.api.routes import health, jobs, internal, file_upload, review, auth, audio_search, themes, users, admin
|
|
11
|
+
from backend.services.tracing import setup_tracing, instrument_app, get_current_trace_id
|
|
12
|
+
from backend.services.structured_logging import setup_structured_logging
|
|
13
|
+
from backend.middleware.audit_logging import AuditLoggingMiddleware
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from backend.version import VERSION
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Configure structured logging (JSON in Cloud Run, human-readable locally)
|
|
20
|
+
# This must happen before any logging calls
|
|
21
|
+
setup_structured_logging()
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Initialize OpenTelemetry tracing (must happen before app creation)
|
|
25
|
+
tracing_enabled = setup_tracing(
|
|
26
|
+
service_name="karaoke-backend",
|
|
27
|
+
service_version=VERSION,
|
|
28
|
+
enable_in_dev=False, # Set to True to enable tracing locally
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def validate_credentials_on_startup():
|
|
33
|
+
"""Validate OAuth credentials on startup and send alerts if needed."""
|
|
34
|
+
try:
|
|
35
|
+
from backend.services.credential_manager import get_credential_manager, CredentialStatus
|
|
36
|
+
|
|
37
|
+
manager = get_credential_manager()
|
|
38
|
+
results = manager.check_all_credentials()
|
|
39
|
+
|
|
40
|
+
invalid_services = [
|
|
41
|
+
result for result in results.values()
|
|
42
|
+
if result.status in (CredentialStatus.INVALID, CredentialStatus.EXPIRED)
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
if invalid_services:
|
|
46
|
+
logger.warning(f"Some OAuth credentials need attention:")
|
|
47
|
+
for result in invalid_services:
|
|
48
|
+
logger.warning(f" - {result.service}: {result.message}")
|
|
49
|
+
|
|
50
|
+
# Try to send Discord alert
|
|
51
|
+
discord_url = settings.get_secret("discord-alert-webhook") if hasattr(settings, 'get_secret') else None
|
|
52
|
+
if discord_url:
|
|
53
|
+
manager.send_credential_alert(invalid_services, discord_url)
|
|
54
|
+
logger.info("Sent credential alert to Discord")
|
|
55
|
+
else:
|
|
56
|
+
logger.info("All OAuth credentials validated successfully")
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error(f"Failed to validate credentials on startup: {e}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@asynccontextmanager
|
|
63
|
+
async def lifespan(app: FastAPI):
|
|
64
|
+
"""Lifespan event handler for startup and shutdown."""
|
|
65
|
+
# Startup
|
|
66
|
+
logger.info("Starting karaoke generation backend")
|
|
67
|
+
logger.info(f"Environment: {settings.environment}")
|
|
68
|
+
logger.info(f"GCS Bucket: {settings.gcs_bucket_name}")
|
|
69
|
+
logger.info(f"Tracing enabled: {tracing_enabled}")
|
|
70
|
+
|
|
71
|
+
# Validate OAuth credentials (non-blocking)
|
|
72
|
+
try:
|
|
73
|
+
await validate_credentials_on_startup()
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error(f"Credential validation failed: {e}")
|
|
76
|
+
|
|
77
|
+
yield
|
|
78
|
+
|
|
79
|
+
# Shutdown
|
|
80
|
+
logger.info("Shutting down karaoke generation backend")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Create FastAPI app
|
|
84
|
+
app = FastAPI(
|
|
85
|
+
title="Karaoke Generator API",
|
|
86
|
+
description="Backend API for web-based karaoke video generation",
|
|
87
|
+
version=VERSION,
|
|
88
|
+
lifespan=lifespan
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Instrument FastAPI with OpenTelemetry (adds automatic spans for all requests)
|
|
92
|
+
if tracing_enabled:
|
|
93
|
+
instrument_app(app)
|
|
94
|
+
|
|
95
|
+
# Configure CORS
|
|
96
|
+
app.add_middleware(
|
|
97
|
+
CORSMiddleware,
|
|
98
|
+
allow_origins=["*"], # Configure this properly for production
|
|
99
|
+
allow_credentials=True,
|
|
100
|
+
allow_methods=["*"],
|
|
101
|
+
allow_headers=["*"],
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Add audit logging middleware (captures all requests with request_id for correlation)
|
|
105
|
+
app.add_middleware(AuditLoggingMiddleware)
|
|
106
|
+
|
|
107
|
+
# Include routers
|
|
108
|
+
app.include_router(health.router, prefix="/api")
|
|
109
|
+
app.include_router(jobs.router, prefix="/api")
|
|
110
|
+
app.include_router(file_upload.router, prefix="/api") # File upload endpoint
|
|
111
|
+
app.include_router(internal.router, prefix="/api") # Internal worker endpoints
|
|
112
|
+
app.include_router(review.router, prefix="/api") # Review UI compatibility endpoints
|
|
113
|
+
app.include_router(auth.router, prefix="/api") # OAuth credential management
|
|
114
|
+
app.include_router(audio_search.router, prefix="/api") # Audio search (artist+title mode)
|
|
115
|
+
app.include_router(themes.router, prefix="/api") # Theme selection for styles
|
|
116
|
+
app.include_router(users.router, prefix="/api") # User auth, credits, and Stripe webhooks
|
|
117
|
+
app.include_router(admin.router, prefix="/api") # Admin dashboard and management
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.get("/")
|
|
121
|
+
async def root():
|
|
122
|
+
"""Root endpoint."""
|
|
123
|
+
return {
|
|
124
|
+
"service": "karaoke-gen-backend",
|
|
125
|
+
"version": VERSION,
|
|
126
|
+
"status": "running"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
import uvicorn
|
|
132
|
+
uvicorn.run(app, host="0.0.0.0", port=8080)
|
|
133
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audit logging middleware for tracking all HTTP requests.
|
|
3
|
+
|
|
4
|
+
This middleware captures request metadata and logs it to Cloud Logging
|
|
5
|
+
for audit trail and user activity investigation purposes.
|
|
6
|
+
"""
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
11
|
+
from starlette.requests import Request
|
|
12
|
+
from starlette.responses import Response
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("audit")
|
|
16
|
+
|
|
17
|
+
# Endpoints to exclude from audit logging (high-frequency health checks)
|
|
18
|
+
EXCLUDED_PATHS = {
|
|
19
|
+
"/",
|
|
20
|
+
"/api/health",
|
|
21
|
+
"/api/health/detailed",
|
|
22
|
+
"/api/readiness",
|
|
23
|
+
"/healthz",
|
|
24
|
+
"/ready",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AuditLoggingMiddleware(BaseHTTPMiddleware):
|
|
29
|
+
"""
|
|
30
|
+
Middleware that logs all HTTP requests for audit purposes.
|
|
31
|
+
|
|
32
|
+
Captures:
|
|
33
|
+
- request_id: UUID for correlating with auth logs
|
|
34
|
+
- method: HTTP method
|
|
35
|
+
- path: Request path
|
|
36
|
+
- status_code: Response status
|
|
37
|
+
- latency_ms: Request duration
|
|
38
|
+
- client_ip: Client IP (from X-Forwarded-For for proxied requests)
|
|
39
|
+
- user_agent: Browser/client identifier
|
|
40
|
+
|
|
41
|
+
The request_id is stored in request.state and added to response headers,
|
|
42
|
+
allowing correlation with auth logs that capture user_email.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
async def dispatch(self, request: Request, call_next) -> Response:
|
|
46
|
+
# Skip excluded paths (health checks)
|
|
47
|
+
if request.url.path in EXCLUDED_PATHS:
|
|
48
|
+
return await call_next(request)
|
|
49
|
+
|
|
50
|
+
# Generate unique request ID for correlation
|
|
51
|
+
request_id = str(uuid.uuid4())
|
|
52
|
+
request.state.request_id = request_id
|
|
53
|
+
|
|
54
|
+
# Capture start time
|
|
55
|
+
start_time = time.time()
|
|
56
|
+
|
|
57
|
+
# Extract client info
|
|
58
|
+
client_ip = self._get_client_ip(request)
|
|
59
|
+
user_agent = request.headers.get("user-agent", "")
|
|
60
|
+
|
|
61
|
+
# Process request
|
|
62
|
+
try:
|
|
63
|
+
response = await call_next(request)
|
|
64
|
+
except Exception:
|
|
65
|
+
# Log failed requests too (exception() auto-includes stack trace)
|
|
66
|
+
latency_ms = int((time.time() - start_time) * 1000)
|
|
67
|
+
logger.exception(
|
|
68
|
+
"request_audit_error",
|
|
69
|
+
extra={
|
|
70
|
+
"request_id": request_id,
|
|
71
|
+
"method": request.method,
|
|
72
|
+
"path": request.url.path,
|
|
73
|
+
"query_string": str(request.query_params) if request.query_params else None,
|
|
74
|
+
"latency_ms": latency_ms,
|
|
75
|
+
"client_ip": client_ip,
|
|
76
|
+
"user_agent": user_agent[:200] if user_agent else None,
|
|
77
|
+
"audit_type": "http_request",
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
raise
|
|
81
|
+
|
|
82
|
+
# Calculate latency
|
|
83
|
+
latency_ms = int((time.time() - start_time) * 1000)
|
|
84
|
+
|
|
85
|
+
# Log audit entry (INFO level for successful requests)
|
|
86
|
+
log_level = logging.WARNING if response.status_code >= 400 else logging.INFO
|
|
87
|
+
logger.log(
|
|
88
|
+
log_level,
|
|
89
|
+
"request_audit",
|
|
90
|
+
extra={
|
|
91
|
+
"request_id": request_id,
|
|
92
|
+
"method": request.method,
|
|
93
|
+
"path": request.url.path,
|
|
94
|
+
"query_string": str(request.query_params) if request.query_params else None,
|
|
95
|
+
"status_code": response.status_code,
|
|
96
|
+
"latency_ms": latency_ms,
|
|
97
|
+
"client_ip": client_ip,
|
|
98
|
+
"user_agent": user_agent[:200] if user_agent else None,
|
|
99
|
+
"audit_type": "http_request",
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Add request_id to response headers for debugging/correlation
|
|
104
|
+
response.headers["X-Request-ID"] = request_id
|
|
105
|
+
|
|
106
|
+
return response
|
|
107
|
+
|
|
108
|
+
def _get_client_ip(self, request: Request) -> str:
|
|
109
|
+
"""
|
|
110
|
+
Extract client IP address, handling proxy scenarios.
|
|
111
|
+
|
|
112
|
+
Cloud Run and other proxies set X-Forwarded-For header.
|
|
113
|
+
"""
|
|
114
|
+
# Check X-Forwarded-For for proxy scenarios (Cloud Run, load balancers)
|
|
115
|
+
forwarded = request.headers.get("x-forwarded-for", "")
|
|
116
|
+
if forwarded:
|
|
117
|
+
# First IP is the original client
|
|
118
|
+
return forwarded.split(",")[0].strip()
|
|
119
|
+
|
|
120
|
+
# Fall back to direct connection
|
|
121
|
+
if request.client:
|
|
122
|
+
return request.client.host
|
|
123
|
+
|
|
124
|
+
return "unknown"
|
|
File without changes
|