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.
Files changed (197) hide show
  1. backend/.coveragerc +20 -0
  2. backend/.gitignore +37 -0
  3. backend/Dockerfile +43 -0
  4. backend/Dockerfile.base +74 -0
  5. backend/README.md +242 -0
  6. backend/__init__.py +0 -0
  7. backend/api/__init__.py +0 -0
  8. backend/api/dependencies.py +457 -0
  9. backend/api/routes/__init__.py +0 -0
  10. backend/api/routes/admin.py +835 -0
  11. backend/api/routes/audio_search.py +913 -0
  12. backend/api/routes/auth.py +348 -0
  13. backend/api/routes/file_upload.py +2112 -0
  14. backend/api/routes/health.py +409 -0
  15. backend/api/routes/internal.py +435 -0
  16. backend/api/routes/jobs.py +1629 -0
  17. backend/api/routes/review.py +652 -0
  18. backend/api/routes/themes.py +162 -0
  19. backend/api/routes/users.py +1513 -0
  20. backend/config.py +172 -0
  21. backend/main.py +157 -0
  22. backend/middleware/__init__.py +5 -0
  23. backend/middleware/audit_logging.py +124 -0
  24. backend/models/__init__.py +0 -0
  25. backend/models/job.py +519 -0
  26. backend/models/requests.py +123 -0
  27. backend/models/theme.py +153 -0
  28. backend/models/user.py +254 -0
  29. backend/models/worker_log.py +164 -0
  30. backend/pyproject.toml +29 -0
  31. backend/quick-check.sh +93 -0
  32. backend/requirements.txt +29 -0
  33. backend/run_tests.sh +60 -0
  34. backend/services/__init__.py +0 -0
  35. backend/services/audio_analysis_service.py +243 -0
  36. backend/services/audio_editing_service.py +278 -0
  37. backend/services/audio_search_service.py +702 -0
  38. backend/services/auth_service.py +630 -0
  39. backend/services/credential_manager.py +792 -0
  40. backend/services/discord_service.py +172 -0
  41. backend/services/dropbox_service.py +301 -0
  42. backend/services/email_service.py +1093 -0
  43. backend/services/encoding_interface.py +454 -0
  44. backend/services/encoding_service.py +502 -0
  45. backend/services/firestore_service.py +512 -0
  46. backend/services/flacfetch_client.py +573 -0
  47. backend/services/gce_encoding/README.md +72 -0
  48. backend/services/gce_encoding/__init__.py +22 -0
  49. backend/services/gce_encoding/main.py +589 -0
  50. backend/services/gce_encoding/requirements.txt +16 -0
  51. backend/services/gdrive_service.py +356 -0
  52. backend/services/job_logging.py +258 -0
  53. backend/services/job_manager.py +853 -0
  54. backend/services/job_notification_service.py +271 -0
  55. backend/services/langfuse_preloader.py +98 -0
  56. backend/services/local_encoding_service.py +590 -0
  57. backend/services/local_preview_encoding_service.py +407 -0
  58. backend/services/lyrics_cache_service.py +216 -0
  59. backend/services/metrics.py +413 -0
  60. backend/services/nltk_preloader.py +122 -0
  61. backend/services/packaging_service.py +287 -0
  62. backend/services/rclone_service.py +106 -0
  63. backend/services/spacy_preloader.py +65 -0
  64. backend/services/storage_service.py +209 -0
  65. backend/services/stripe_service.py +371 -0
  66. backend/services/structured_logging.py +254 -0
  67. backend/services/template_service.py +330 -0
  68. backend/services/theme_service.py +469 -0
  69. backend/services/tracing.py +543 -0
  70. backend/services/user_service.py +721 -0
  71. backend/services/worker_service.py +558 -0
  72. backend/services/youtube_service.py +112 -0
  73. backend/services/youtube_upload_service.py +445 -0
  74. backend/tests/__init__.py +4 -0
  75. backend/tests/conftest.py +224 -0
  76. backend/tests/emulator/__init__.py +7 -0
  77. backend/tests/emulator/conftest.py +109 -0
  78. backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
  79. backend/tests/emulator/test_emulator_integration.py +356 -0
  80. backend/tests/emulator/test_style_loading_direct.py +436 -0
  81. backend/tests/emulator/test_worker_logs_direct.py +229 -0
  82. backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
  83. backend/tests/requirements-test.txt +10 -0
  84. backend/tests/requirements.txt +6 -0
  85. backend/tests/test_admin_email_endpoints.py +411 -0
  86. backend/tests/test_api_integration.py +460 -0
  87. backend/tests/test_api_routes.py +93 -0
  88. backend/tests/test_audio_analysis_service.py +294 -0
  89. backend/tests/test_audio_editing_service.py +386 -0
  90. backend/tests/test_audio_search.py +1398 -0
  91. backend/tests/test_audio_services.py +378 -0
  92. backend/tests/test_auth_firestore.py +231 -0
  93. backend/tests/test_config_extended.py +68 -0
  94. backend/tests/test_credential_manager.py +377 -0
  95. backend/tests/test_dependencies.py +54 -0
  96. backend/tests/test_discord_service.py +244 -0
  97. backend/tests/test_distribution_services.py +820 -0
  98. backend/tests/test_dropbox_service.py +472 -0
  99. backend/tests/test_email_service.py +492 -0
  100. backend/tests/test_emulator_integration.py +322 -0
  101. backend/tests/test_encoding_interface.py +412 -0
  102. backend/tests/test_file_upload.py +1739 -0
  103. backend/tests/test_flacfetch_client.py +632 -0
  104. backend/tests/test_gdrive_service.py +524 -0
  105. backend/tests/test_instrumental_api.py +431 -0
  106. backend/tests/test_internal_api.py +343 -0
  107. backend/tests/test_job_creation_regression.py +583 -0
  108. backend/tests/test_job_manager.py +356 -0
  109. backend/tests/test_job_manager_notifications.py +329 -0
  110. backend/tests/test_job_notification_service.py +443 -0
  111. backend/tests/test_jobs_api.py +283 -0
  112. backend/tests/test_local_encoding_service.py +423 -0
  113. backend/tests/test_local_preview_encoding_service.py +567 -0
  114. backend/tests/test_main.py +87 -0
  115. backend/tests/test_models.py +918 -0
  116. backend/tests/test_packaging_service.py +382 -0
  117. backend/tests/test_requests.py +201 -0
  118. backend/tests/test_routes_jobs.py +282 -0
  119. backend/tests/test_routes_review.py +337 -0
  120. backend/tests/test_services.py +556 -0
  121. backend/tests/test_services_extended.py +112 -0
  122. backend/tests/test_spacy_preloader.py +119 -0
  123. backend/tests/test_storage_service.py +448 -0
  124. backend/tests/test_style_upload.py +261 -0
  125. backend/tests/test_template_service.py +295 -0
  126. backend/tests/test_theme_service.py +516 -0
  127. backend/tests/test_unicode_sanitization.py +522 -0
  128. backend/tests/test_upload_api.py +256 -0
  129. backend/tests/test_validate.py +156 -0
  130. backend/tests/test_video_worker_orchestrator.py +847 -0
  131. backend/tests/test_worker_log_subcollection.py +509 -0
  132. backend/tests/test_worker_logging.py +365 -0
  133. backend/tests/test_workers.py +1116 -0
  134. backend/tests/test_workers_extended.py +178 -0
  135. backend/tests/test_youtube_service.py +247 -0
  136. backend/tests/test_youtube_upload_service.py +568 -0
  137. backend/utils/test_data.py +27 -0
  138. backend/validate.py +173 -0
  139. backend/version.py +27 -0
  140. backend/workers/README.md +597 -0
  141. backend/workers/__init__.py +11 -0
  142. backend/workers/audio_worker.py +618 -0
  143. backend/workers/lyrics_worker.py +683 -0
  144. backend/workers/render_video_worker.py +483 -0
  145. backend/workers/screens_worker.py +535 -0
  146. backend/workers/style_helper.py +198 -0
  147. backend/workers/video_worker.py +1277 -0
  148. backend/workers/video_worker_orchestrator.py +701 -0
  149. backend/workers/worker_logging.py +278 -0
  150. karaoke_gen/instrumental_review/static/index.html +7 -4
  151. karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
  152. karaoke_gen/utils/__init__.py +163 -8
  153. karaoke_gen/video_background_processor.py +9 -4
  154. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/METADATA +1 -1
  155. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/RECORD +196 -46
  156. lyrics_transcriber/correction/agentic/agent.py +17 -6
  157. lyrics_transcriber/correction/agentic/providers/config.py +9 -5
  158. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +96 -93
  159. lyrics_transcriber/correction/agentic/providers/model_factory.py +27 -6
  160. lyrics_transcriber/correction/anchor_sequence.py +151 -37
  161. lyrics_transcriber/correction/corrector.py +192 -130
  162. lyrics_transcriber/correction/handlers/syllables_match.py +44 -2
  163. lyrics_transcriber/correction/operations.py +24 -9
  164. lyrics_transcriber/correction/phrase_analyzer.py +18 -0
  165. lyrics_transcriber/frontend/package-lock.json +2 -2
  166. lyrics_transcriber/frontend/package.json +1 -1
  167. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
  168. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
  169. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
  170. lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
  171. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
  172. lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
  173. lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
  174. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
  175. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
  176. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
  177. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
  178. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
  179. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
  180. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
  181. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
  182. lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
  183. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
  184. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
  185. lyrics_transcriber/frontend/src/theme.ts +42 -15
  186. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  187. lyrics_transcriber/frontend/vite.config.js +5 -0
  188. lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
  189. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
  190. lyrics_transcriber/frontend/web_assets/index.html +6 -2
  191. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
  192. lyrics_transcriber/output/generator.py +17 -3
  193. lyrics_transcriber/output/video.py +60 -95
  194. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
  195. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/WHEEL +0 -0
  196. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/entry_points.txt +0 -0
  197. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/licenses/LICENSE +0 -0
@@ -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
+