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
backend/models/theme.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Theme data models for karaoke-gen.
|
|
3
|
+
|
|
4
|
+
Themes are pre-created style configurations stored in GCS that users can select
|
|
5
|
+
when creating karaoke jobs. Each theme includes:
|
|
6
|
+
- Style parameters (fonts, colors, backgrounds, layouts)
|
|
7
|
+
- Asset files (images, fonts)
|
|
8
|
+
- Preview images
|
|
9
|
+
- Optional YouTube description template
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ThemeSummary(BaseModel):
|
|
19
|
+
"""Summary of a theme for listing purposes."""
|
|
20
|
+
|
|
21
|
+
id: str = Field(..., description="Unique theme identifier (e.g., 'nomad', 'default')")
|
|
22
|
+
name: str = Field(..., description="Human-readable theme name")
|
|
23
|
+
description: str = Field(..., description="Brief description of the theme style")
|
|
24
|
+
preview_url: Optional[str] = Field(None, description="Signed URL for preview image")
|
|
25
|
+
thumbnail_url: Optional[str] = Field(None, description="Signed URL for smaller thumbnail")
|
|
26
|
+
is_default: bool = Field(False, description="Whether this is the default theme")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ThemeMetadata(BaseModel):
|
|
30
|
+
"""Metadata for a single theme in the registry."""
|
|
31
|
+
|
|
32
|
+
id: str
|
|
33
|
+
name: str
|
|
34
|
+
description: str
|
|
35
|
+
is_default: bool = False
|
|
36
|
+
created_at: Optional[datetime] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ThemeRegistry(BaseModel):
|
|
40
|
+
"""Registry of all available themes, stored as _metadata.json in GCS."""
|
|
41
|
+
|
|
42
|
+
version: int = 1
|
|
43
|
+
themes: List[ThemeMetadata] = Field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ThemeDetail(BaseModel):
|
|
47
|
+
"""Full theme details including style parameters."""
|
|
48
|
+
|
|
49
|
+
id: str
|
|
50
|
+
name: str
|
|
51
|
+
description: str
|
|
52
|
+
preview_url: Optional[str] = None
|
|
53
|
+
is_default: bool = False
|
|
54
|
+
style_params: Dict[str, Any] = Field(default_factory=dict)
|
|
55
|
+
has_youtube_description: bool = False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ColorOverrides(BaseModel):
|
|
59
|
+
"""
|
|
60
|
+
User-customizable color overrides that can be applied on top of a theme.
|
|
61
|
+
|
|
62
|
+
All colors are hex format (#RRGGBB). When applied:
|
|
63
|
+
- artist_color: Applied to intro.artist_color, end.artist_color, cdg.artist_color
|
|
64
|
+
- title_color: Applied to intro.title_color, end.title_color, cdg.title_color
|
|
65
|
+
- sung_lyrics_color: Applied to karaoke.primary_color (converted to RGBA), cdg.active_fill
|
|
66
|
+
- unsung_lyrics_color: Applied to karaoke.secondary_color (converted to RGBA), cdg.inactive_fill
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
artist_color: Optional[str] = Field(
|
|
70
|
+
None,
|
|
71
|
+
pattern=r"^#[0-9A-Fa-f]{6}$",
|
|
72
|
+
description="Hex color for artist name on intro/end screens",
|
|
73
|
+
)
|
|
74
|
+
title_color: Optional[str] = Field(
|
|
75
|
+
None,
|
|
76
|
+
pattern=r"^#[0-9A-Fa-f]{6}$",
|
|
77
|
+
description="Hex color for song title on intro/end screens",
|
|
78
|
+
)
|
|
79
|
+
sung_lyrics_color: Optional[str] = Field(
|
|
80
|
+
None,
|
|
81
|
+
pattern=r"^#[0-9A-Fa-f]{6}$",
|
|
82
|
+
description="Hex color for lyrics being sung (highlighted)",
|
|
83
|
+
)
|
|
84
|
+
unsung_lyrics_color: Optional[str] = Field(
|
|
85
|
+
None,
|
|
86
|
+
pattern=r"^#[0-9A-Fa-f]{6}$",
|
|
87
|
+
description="Hex color for lyrics not yet sung",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def has_overrides(self) -> bool:
|
|
91
|
+
"""Check if any color overrides are set."""
|
|
92
|
+
return any(
|
|
93
|
+
[
|
|
94
|
+
self.artist_color,
|
|
95
|
+
self.title_color,
|
|
96
|
+
self.sung_lyrics_color,
|
|
97
|
+
self.unsung_lyrics_color,
|
|
98
|
+
]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def to_dict(self) -> Dict[str, str]:
|
|
102
|
+
"""Convert to dict, excluding None values."""
|
|
103
|
+
return {k: v for k, v in self.model_dump().items() if v is not None}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ThemesListResponse(BaseModel):
|
|
107
|
+
"""Response from GET /api/themes endpoint."""
|
|
108
|
+
|
|
109
|
+
themes: List[ThemeSummary]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ThemeDetailResponse(BaseModel):
|
|
113
|
+
"""Response from GET /api/themes/{theme_id} endpoint."""
|
|
114
|
+
|
|
115
|
+
theme: ThemeDetail
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Utility functions for color conversion
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def hex_to_rgba(hex_color: str, alpha: int = 255) -> str:
|
|
122
|
+
"""
|
|
123
|
+
Convert hex color (#RRGGBB) to RGBA format for ASS subtitles.
|
|
124
|
+
|
|
125
|
+
ASS subtitle format uses "R, G, B, A" string format.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
hex_color: Hex color string like "#7070F7"
|
|
129
|
+
alpha: Alpha value 0-255 (default 255 = opaque)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
RGBA string like "112, 112, 247, 255"
|
|
133
|
+
"""
|
|
134
|
+
hex_color = hex_color.lstrip("#")
|
|
135
|
+
r = int(hex_color[0:2], 16)
|
|
136
|
+
g = int(hex_color[2:4], 16)
|
|
137
|
+
b = int(hex_color[4:6], 16)
|
|
138
|
+
return f"{r}, {g}, {b}, {alpha}"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def rgba_to_hex(rgba_str: str) -> str:
|
|
142
|
+
"""
|
|
143
|
+
Convert RGBA format to hex color.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
rgba_str: RGBA string like "112, 112, 247, 255"
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Hex color string like "#7070F7"
|
|
150
|
+
"""
|
|
151
|
+
parts = [int(x.strip()) for x in rgba_str.split(",")]
|
|
152
|
+
r, g, b = parts[0], parts[1], parts[2]
|
|
153
|
+
return f"#{r:02x}{g:02x}{b:02x}"
|
backend/models/user.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User model for authentication and credits.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- Magic link authentication (email-based)
|
|
6
|
+
- Credit system for karaoke generation
|
|
7
|
+
- Role-based access control (user/admin)
|
|
8
|
+
- Stripe integration for payments
|
|
9
|
+
- Beta tester program with feedback collection
|
|
10
|
+
"""
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Optional, List
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UserRole(str, Enum):
|
|
18
|
+
"""User roles for access control."""
|
|
19
|
+
USER = "user"
|
|
20
|
+
ADMIN = "admin"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BetaTesterStatus(str, Enum):
|
|
24
|
+
"""Status of beta tester participation."""
|
|
25
|
+
ACTIVE = "active" # Enrolled, free credits available
|
|
26
|
+
PENDING_FEEDBACK = "pending_feedback" # Job completed, awaiting feedback
|
|
27
|
+
COMPLETED = "completed" # Feedback submitted
|
|
28
|
+
EXPIRED = "expired" # 24hr deadline passed without feedback
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CreditTransaction(BaseModel):
|
|
32
|
+
"""Record of a credit transaction."""
|
|
33
|
+
id: str
|
|
34
|
+
amount: int # Positive = add, negative = deduct
|
|
35
|
+
reason: str # e.g., "purchase", "refund", "admin_grant", "job_creation"
|
|
36
|
+
job_id: Optional[str] = None
|
|
37
|
+
stripe_session_id: Optional[str] = None
|
|
38
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
39
|
+
created_by: Optional[str] = None # Admin email if granted by admin
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class User(BaseModel):
|
|
43
|
+
"""
|
|
44
|
+
User model stored in Firestore.
|
|
45
|
+
|
|
46
|
+
Users are identified by email address. Authentication is via magic links
|
|
47
|
+
(no passwords). Credits are consumed when creating karaoke jobs.
|
|
48
|
+
"""
|
|
49
|
+
email: str # Primary identifier
|
|
50
|
+
role: UserRole = UserRole.USER
|
|
51
|
+
credits: int = 0
|
|
52
|
+
|
|
53
|
+
# Stripe integration
|
|
54
|
+
stripe_customer_id: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
# Account state
|
|
57
|
+
is_active: bool = True
|
|
58
|
+
email_verified: bool = False
|
|
59
|
+
|
|
60
|
+
# Timestamps
|
|
61
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
62
|
+
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
63
|
+
last_login_at: Optional[datetime] = None
|
|
64
|
+
|
|
65
|
+
# Credit history (last 100 transactions)
|
|
66
|
+
credit_transactions: List[CreditTransaction] = Field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
# Job tracking
|
|
69
|
+
total_jobs_created: int = 0
|
|
70
|
+
total_jobs_completed: int = 0
|
|
71
|
+
|
|
72
|
+
# Optional profile fields for future use
|
|
73
|
+
display_name: Optional[str] = None
|
|
74
|
+
|
|
75
|
+
# Beta tester program
|
|
76
|
+
is_beta_tester: bool = False
|
|
77
|
+
beta_tester_status: Optional[BetaTesterStatus] = None
|
|
78
|
+
beta_enrolled_at: Optional[datetime] = None
|
|
79
|
+
beta_promise_text: Optional[str] = None # User's promise statement
|
|
80
|
+
beta_feedback_due_at: Optional[datetime] = None # 24hr after job completion
|
|
81
|
+
beta_feedback_email_sent: bool = False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MagicLinkToken(BaseModel):
|
|
85
|
+
"""
|
|
86
|
+
Magic link token for passwordless authentication.
|
|
87
|
+
|
|
88
|
+
Tokens are short-lived (15 minutes) and single-use.
|
|
89
|
+
Stored in Firestore with automatic TTL.
|
|
90
|
+
"""
|
|
91
|
+
token: str # Secure random token
|
|
92
|
+
email: str
|
|
93
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
94
|
+
expires_at: datetime
|
|
95
|
+
used: bool = False
|
|
96
|
+
used_at: Optional[datetime] = None
|
|
97
|
+
ip_address: Optional[str] = None
|
|
98
|
+
user_agent: Optional[str] = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Session(BaseModel):
|
|
102
|
+
"""
|
|
103
|
+
User session for authenticated requests.
|
|
104
|
+
|
|
105
|
+
Sessions are created after successful magic link verification.
|
|
106
|
+
Sessions expire after 7 days of inactivity or 30 days absolute.
|
|
107
|
+
"""
|
|
108
|
+
token: str # Secure random session token
|
|
109
|
+
user_email: str
|
|
110
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
111
|
+
expires_at: datetime
|
|
112
|
+
last_activity_at: datetime = Field(default_factory=datetime.utcnow)
|
|
113
|
+
ip_address: Optional[str] = None
|
|
114
|
+
user_agent: Optional[str] = None
|
|
115
|
+
is_active: bool = True
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Pydantic models for API requests/responses
|
|
119
|
+
|
|
120
|
+
class SendMagicLinkRequest(BaseModel):
|
|
121
|
+
"""Request to send a magic link email."""
|
|
122
|
+
email: str
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class SendMagicLinkResponse(BaseModel):
|
|
126
|
+
"""Response after sending magic link."""
|
|
127
|
+
status: str
|
|
128
|
+
message: str
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class VerifyMagicLinkRequest(BaseModel):
|
|
132
|
+
"""Request to verify a magic link token."""
|
|
133
|
+
token: str
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class VerifyMagicLinkResponse(BaseModel):
|
|
137
|
+
"""Response after verifying magic link."""
|
|
138
|
+
status: str
|
|
139
|
+
session_token: str
|
|
140
|
+
user: "UserPublic"
|
|
141
|
+
message: str
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class UserPublic(BaseModel):
|
|
145
|
+
"""Public user information (safe to expose to frontend)."""
|
|
146
|
+
email: str
|
|
147
|
+
role: UserRole
|
|
148
|
+
credits: int
|
|
149
|
+
display_name: Optional[str] = None
|
|
150
|
+
total_jobs_created: int = 0
|
|
151
|
+
total_jobs_completed: int = 0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class AddCreditsRequest(BaseModel):
|
|
155
|
+
"""Admin request to add credits to a user."""
|
|
156
|
+
email: str
|
|
157
|
+
amount: int
|
|
158
|
+
reason: str = "admin_grant"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class AddCreditsResponse(BaseModel):
|
|
162
|
+
"""Response after adding credits."""
|
|
163
|
+
status: str
|
|
164
|
+
email: str
|
|
165
|
+
credits_added: int
|
|
166
|
+
new_balance: int
|
|
167
|
+
message: str
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class UserListResponse(BaseModel):
|
|
171
|
+
"""Response for listing users (admin only)."""
|
|
172
|
+
users: List[UserPublic]
|
|
173
|
+
total: int
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ============================================================================
|
|
177
|
+
# Beta Tester Program Models
|
|
178
|
+
# ============================================================================
|
|
179
|
+
|
|
180
|
+
class BetaTesterFeedback(BaseModel):
|
|
181
|
+
"""
|
|
182
|
+
Feedback from a beta tester after using the service.
|
|
183
|
+
|
|
184
|
+
Stored in Firestore 'beta_feedback' collection.
|
|
185
|
+
"""
|
|
186
|
+
id: str # Unique feedback ID
|
|
187
|
+
user_email: str
|
|
188
|
+
job_id: Optional[str] = None # The job they're providing feedback on
|
|
189
|
+
|
|
190
|
+
# Ratings (1-5 scale)
|
|
191
|
+
overall_rating: int = Field(ge=1, le=5)
|
|
192
|
+
ease_of_use_rating: int = Field(ge=1, le=5)
|
|
193
|
+
lyrics_accuracy_rating: int = Field(ge=1, le=5)
|
|
194
|
+
correction_experience_rating: int = Field(ge=1, le=5)
|
|
195
|
+
|
|
196
|
+
# Open-ended feedback
|
|
197
|
+
what_went_well: Optional[str] = None
|
|
198
|
+
what_could_improve: Optional[str] = None
|
|
199
|
+
additional_comments: Optional[str] = None
|
|
200
|
+
|
|
201
|
+
# Would they recommend / use again?
|
|
202
|
+
would_recommend: bool = True
|
|
203
|
+
would_use_again: bool = True
|
|
204
|
+
|
|
205
|
+
# Metadata
|
|
206
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
207
|
+
submitted_via: str = "web" # 'web' or 'email'
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class BetaTesterEnrollRequest(BaseModel):
|
|
211
|
+
"""Request to enroll as a beta tester."""
|
|
212
|
+
email: str
|
|
213
|
+
promise_text: str = Field(
|
|
214
|
+
min_length=10,
|
|
215
|
+
description="User's promise to provide feedback (min 10 chars)"
|
|
216
|
+
)
|
|
217
|
+
accept_corrections_work: bool = Field(
|
|
218
|
+
description="User accepts they may need to review/correct lyrics"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class BetaTesterEnrollResponse(BaseModel):
|
|
223
|
+
"""Response after enrolling as beta tester."""
|
|
224
|
+
status: str
|
|
225
|
+
message: str
|
|
226
|
+
credits_granted: int
|
|
227
|
+
session_token: Optional[str] = None # If new user, provide session
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class BetaFeedbackRequest(BaseModel):
|
|
231
|
+
"""Request to submit beta tester feedback."""
|
|
232
|
+
job_id: Optional[str] = None
|
|
233
|
+
|
|
234
|
+
# Ratings (1-5 scale)
|
|
235
|
+
overall_rating: int = Field(ge=1, le=5)
|
|
236
|
+
ease_of_use_rating: int = Field(ge=1, le=5)
|
|
237
|
+
lyrics_accuracy_rating: int = Field(ge=1, le=5)
|
|
238
|
+
correction_experience_rating: int = Field(ge=1, le=5)
|
|
239
|
+
|
|
240
|
+
# Open-ended feedback
|
|
241
|
+
what_went_well: Optional[str] = None
|
|
242
|
+
what_could_improve: Optional[str] = None
|
|
243
|
+
additional_comments: Optional[str] = None
|
|
244
|
+
|
|
245
|
+
# Would they recommend / use again?
|
|
246
|
+
would_recommend: bool = True
|
|
247
|
+
would_use_again: bool = True
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class BetaFeedbackResponse(BaseModel):
|
|
251
|
+
"""Response after submitting feedback."""
|
|
252
|
+
status: str
|
|
253
|
+
message: str
|
|
254
|
+
bonus_credits: int = 0 # Bonus credits for great feedback
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Worker log entry model for subcollection storage.
|
|
3
|
+
|
|
4
|
+
This module defines the LogEntry model for storing worker logs in a Firestore
|
|
5
|
+
subcollection instead of an embedded array. This avoids the 1MB document size
|
|
6
|
+
limit that caused job 501258e1 to fail when logs reached 1.26 MB.
|
|
7
|
+
"""
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime, timezone, timedelta
|
|
10
|
+
from typing import Optional, Dict, Any
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Default TTL for log entries (30 days)
|
|
15
|
+
DEFAULT_LOG_TTL_DAYS = 30
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class WorkerLogEntry:
|
|
20
|
+
"""
|
|
21
|
+
Worker log entry for Firestore subcollection storage.
|
|
22
|
+
|
|
23
|
+
Stored at: jobs/{job_id}/logs/{log_id}
|
|
24
|
+
|
|
25
|
+
This model is separate from the legacy LogEntry (in job.py) which is
|
|
26
|
+
stored as an embedded array in the job document. This new model supports
|
|
27
|
+
the subcollection approach with TTL and richer metadata.
|
|
28
|
+
"""
|
|
29
|
+
# Core fields (required)
|
|
30
|
+
timestamp: datetime
|
|
31
|
+
level: str # "DEBUG", "INFO", "WARNING", "ERROR"
|
|
32
|
+
worker: str # "audio", "lyrics", "screens", "video", "render", "distribution"
|
|
33
|
+
message: str
|
|
34
|
+
|
|
35
|
+
# Identifiers
|
|
36
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
37
|
+
job_id: str = "" # Set when writing to Firestore
|
|
38
|
+
|
|
39
|
+
# TTL for automatic cleanup
|
|
40
|
+
ttl_expiry: datetime = field(
|
|
41
|
+
default_factory=lambda: datetime.now(timezone.utc) + timedelta(days=DEFAULT_LOG_TTL_DAYS)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Optional metadata for debugging
|
|
45
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def create(
|
|
49
|
+
cls,
|
|
50
|
+
job_id: str,
|
|
51
|
+
worker: str,
|
|
52
|
+
level: str,
|
|
53
|
+
message: str,
|
|
54
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
55
|
+
ttl_days: int = DEFAULT_LOG_TTL_DAYS
|
|
56
|
+
) -> "WorkerLogEntry":
|
|
57
|
+
"""
|
|
58
|
+
Factory method to create a new log entry.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
job_id: Job ID this log belongs to
|
|
62
|
+
worker: Worker name (audio, lyrics, screens, video, render, distribution)
|
|
63
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR)
|
|
64
|
+
message: Log message (truncated to 1000 chars)
|
|
65
|
+
metadata: Optional additional metadata
|
|
66
|
+
ttl_days: Days until log expires (default 30)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
New WorkerLogEntry instance
|
|
70
|
+
"""
|
|
71
|
+
now = datetime.now(timezone.utc)
|
|
72
|
+
return cls(
|
|
73
|
+
id=str(uuid.uuid4()),
|
|
74
|
+
job_id=job_id,
|
|
75
|
+
timestamp=now,
|
|
76
|
+
level=level.upper(),
|
|
77
|
+
worker=worker,
|
|
78
|
+
message=message[:1000], # Truncate long messages
|
|
79
|
+
metadata=metadata,
|
|
80
|
+
ttl_expiry=now + timedelta(days=ttl_days)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
84
|
+
"""
|
|
85
|
+
Convert to dictionary for Firestore storage.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Dictionary representation for Firestore
|
|
89
|
+
"""
|
|
90
|
+
result = {
|
|
91
|
+
"id": self.id,
|
|
92
|
+
"job_id": self.job_id,
|
|
93
|
+
"timestamp": self.timestamp,
|
|
94
|
+
"level": self.level,
|
|
95
|
+
"worker": self.worker,
|
|
96
|
+
"message": self.message,
|
|
97
|
+
"ttl_expiry": self.ttl_expiry
|
|
98
|
+
}
|
|
99
|
+
if self.metadata:
|
|
100
|
+
result["metadata"] = self.metadata
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
def to_legacy_dict(self) -> Dict[str, Any]:
|
|
104
|
+
"""
|
|
105
|
+
Convert to legacy format for backward compatibility with API responses.
|
|
106
|
+
|
|
107
|
+
The existing API returns logs in this format:
|
|
108
|
+
{"timestamp": "...", "level": "INFO", "worker": "audio", "message": "..."}
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Dictionary in legacy format
|
|
112
|
+
"""
|
|
113
|
+
# Format timestamp: use "Z" suffix for UTC, isoformat() for others
|
|
114
|
+
if self.timestamp.tzinfo is None:
|
|
115
|
+
# Naive datetime - use as-is
|
|
116
|
+
timestamp_str = self.timestamp.isoformat()
|
|
117
|
+
elif self.timestamp.utcoffset() == timedelta(0):
|
|
118
|
+
# UTC datetime - replace +00:00 with Z for cleaner format
|
|
119
|
+
timestamp_str = self.timestamp.replace(tzinfo=None).isoformat() + "Z"
|
|
120
|
+
else:
|
|
121
|
+
# Non-UTC timezone-aware - use full isoformat with offset
|
|
122
|
+
timestamp_str = self.timestamp.isoformat()
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
"timestamp": timestamp_str,
|
|
126
|
+
"level": self.level,
|
|
127
|
+
"worker": self.worker,
|
|
128
|
+
"message": self.message
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def from_dict(cls, data: Dict[str, Any]) -> "WorkerLogEntry":
|
|
133
|
+
"""
|
|
134
|
+
Create from Firestore document data.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
data: Dictionary from Firestore document
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
WorkerLogEntry instance
|
|
141
|
+
"""
|
|
142
|
+
# Handle timestamp conversion (Firestore returns datetime objects)
|
|
143
|
+
timestamp = data.get("timestamp")
|
|
144
|
+
if isinstance(timestamp, str):
|
|
145
|
+
# Parse ISO format string
|
|
146
|
+
timestamp = datetime.fromisoformat(timestamp.rstrip("Z")).replace(tzinfo=timezone.utc)
|
|
147
|
+
|
|
148
|
+
ttl_expiry = data.get("ttl_expiry")
|
|
149
|
+
if isinstance(ttl_expiry, str):
|
|
150
|
+
ttl_expiry = datetime.fromisoformat(ttl_expiry.rstrip("Z")).replace(tzinfo=timezone.utc)
|
|
151
|
+
elif ttl_expiry is None:
|
|
152
|
+
# Default TTL if not set
|
|
153
|
+
ttl_expiry = datetime.now(timezone.utc) + timedelta(days=DEFAULT_LOG_TTL_DAYS)
|
|
154
|
+
|
|
155
|
+
return cls(
|
|
156
|
+
id=data.get("id", str(uuid.uuid4())),
|
|
157
|
+
job_id=data.get("job_id", ""),
|
|
158
|
+
timestamp=timestamp or datetime.now(timezone.utc),
|
|
159
|
+
level=data.get("level", "INFO"),
|
|
160
|
+
worker=data.get("worker", "unknown"),
|
|
161
|
+
message=data.get("message", ""),
|
|
162
|
+
metadata=data.get("metadata"),
|
|
163
|
+
ttl_expiry=ttl_expiry
|
|
164
|
+
)
|
backend/pyproject.toml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[tool.pytest.ini_options]
|
|
2
|
+
testpaths = ["tests"]
|
|
3
|
+
python_files = ["test_*.py"]
|
|
4
|
+
python_classes = ["Test*"]
|
|
5
|
+
python_functions = ["test_*"]
|
|
6
|
+
addopts = [
|
|
7
|
+
"-v",
|
|
8
|
+
"--tb=short",
|
|
9
|
+
"--strict-markers",
|
|
10
|
+
"-ra",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
markers = [
|
|
14
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
15
|
+
"integration: marks tests as integration tests requiring live service",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
# Timeout for slow tests (in seconds)
|
|
19
|
+
timeout = 300
|
|
20
|
+
|
|
21
|
+
[tool.coverage.run]
|
|
22
|
+
source = ["backend"]
|
|
23
|
+
omit = ["*/tests/*", "*/test_*"]
|
|
24
|
+
|
|
25
|
+
[tool.coverage.report]
|
|
26
|
+
precision = 2
|
|
27
|
+
show_missing = true
|
|
28
|
+
skip_covered = false
|
|
29
|
+
|
backend/quick-check.sh
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Quick syntax and structure check (no dependencies required)
|
|
3
|
+
# Run this before every deployment!
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
echo "🔍 Quick Backend Check (no dependencies required)"
|
|
8
|
+
echo "=================================================="
|
|
9
|
+
echo ""
|
|
10
|
+
|
|
11
|
+
# Change to backend directory
|
|
12
|
+
cd "$(dirname "$0")"
|
|
13
|
+
|
|
14
|
+
# Colors
|
|
15
|
+
RED='\033[0;31m'
|
|
16
|
+
GREEN='\033[0;32m'
|
|
17
|
+
NC='\033[0m'
|
|
18
|
+
|
|
19
|
+
ERRORS=0
|
|
20
|
+
|
|
21
|
+
# Check 1: Python syntax
|
|
22
|
+
echo "1. Checking Python syntax..."
|
|
23
|
+
for file in $(find . -name "*.py" -not -path "./__pycache__/*" -not -path "./venv/*"); do
|
|
24
|
+
if ! python3 -m py_compile "$file" 2>/dev/null; then
|
|
25
|
+
echo -e " ${RED}❌ Syntax error in $file${NC}"
|
|
26
|
+
python3 -m py_compile "$file"
|
|
27
|
+
ERRORS=$((ERRORS+1))
|
|
28
|
+
fi
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
if [ $ERRORS -eq 0 ]; then
|
|
32
|
+
echo -e " ${GREEN}✅ All Python files have valid syntax${NC}"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Check 2: No missing imports (basic check)
|
|
36
|
+
echo ""
|
|
37
|
+
echo "2. Checking for obvious import issues..."
|
|
38
|
+
|
|
39
|
+
# Check that we don't import deleted modules
|
|
40
|
+
if grep -r "from backend.services.processing_service import" --include="*.py" . 2>/dev/null; then
|
|
41
|
+
echo -e " ${RED}❌ Found import of deleted processing_service${NC}"
|
|
42
|
+
ERRORS=$((ERRORS+1))
|
|
43
|
+
else
|
|
44
|
+
echo -e " ${GREEN}✅ No imports of deleted modules${NC}"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Check 3: Required files exist
|
|
48
|
+
echo ""
|
|
49
|
+
echo "3. Checking required files exist..."
|
|
50
|
+
REQUIRED_FILES=(
|
|
51
|
+
"main.py"
|
|
52
|
+
"config.py"
|
|
53
|
+
"requirements.txt"
|
|
54
|
+
"Dockerfile"
|
|
55
|
+
"api/routes/health.py"
|
|
56
|
+
"api/routes/jobs.py"
|
|
57
|
+
"api/routes/internal.py"
|
|
58
|
+
"api/routes/file_upload.py"
|
|
59
|
+
"services/job_manager.py"
|
|
60
|
+
"services/firestore_service.py"
|
|
61
|
+
"services/storage_service.py"
|
|
62
|
+
"services/worker_service.py"
|
|
63
|
+
"services/auth_service.py"
|
|
64
|
+
"workers/audio_worker.py"
|
|
65
|
+
"workers/lyrics_worker.py"
|
|
66
|
+
"workers/screens_worker.py"
|
|
67
|
+
"workers/video_worker.py"
|
|
68
|
+
"models/job.py"
|
|
69
|
+
"models/requests.py"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
for file in "${REQUIRED_FILES[@]}"; do
|
|
73
|
+
if [ ! -f "$file" ]; then
|
|
74
|
+
echo -e " ${RED}❌ Missing required file: $file${NC}"
|
|
75
|
+
ERRORS=$((ERRORS+1))
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
if [ $ERRORS -eq 0 ]; then
|
|
80
|
+
echo -e " ${GREEN}✅ All required files exist${NC}"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# Summary
|
|
84
|
+
echo ""
|
|
85
|
+
echo "=================================================="
|
|
86
|
+
if [ $ERRORS -eq 0 ]; then
|
|
87
|
+
echo -e "${GREEN}✅ All checks passed! Safe to deploy.${NC}"
|
|
88
|
+
exit 0
|
|
89
|
+
else
|
|
90
|
+
echo -e "${RED}❌ $ERRORS error(s) found. Fix before deploying!${NC}"
|
|
91
|
+
exit 1
|
|
92
|
+
fi
|
|
93
|
+
|