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.
Files changed (187) 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 +742 -0
  11. backend/api/routes/audio_search.py +903 -0
  12. backend/api/routes/auth.py +348 -0
  13. backend/api/routes/file_upload.py +2076 -0
  14. backend/api/routes/health.py +344 -0
  15. backend/api/routes/internal.py +435 -0
  16. backend/api/routes/jobs.py +1610 -0
  17. backend/api/routes/review.py +652 -0
  18. backend/api/routes/themes.py +162 -0
  19. backend/api/routes/users.py +1014 -0
  20. backend/config.py +172 -0
  21. backend/main.py +133 -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 +405 -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 +842 -0
  54. backend/services/job_notification_service.py +271 -0
  55. backend/services/local_encoding_service.py +590 -0
  56. backend/services/local_preview_encoding_service.py +407 -0
  57. backend/services/lyrics_cache_service.py +216 -0
  58. backend/services/metrics.py +413 -0
  59. backend/services/packaging_service.py +287 -0
  60. backend/services/rclone_service.py +106 -0
  61. backend/services/storage_service.py +209 -0
  62. backend/services/stripe_service.py +275 -0
  63. backend/services/structured_logging.py +254 -0
  64. backend/services/template_service.py +330 -0
  65. backend/services/theme_service.py +469 -0
  66. backend/services/tracing.py +543 -0
  67. backend/services/user_service.py +721 -0
  68. backend/services/worker_service.py +558 -0
  69. backend/services/youtube_service.py +112 -0
  70. backend/services/youtube_upload_service.py +445 -0
  71. backend/tests/__init__.py +4 -0
  72. backend/tests/conftest.py +224 -0
  73. backend/tests/emulator/__init__.py +7 -0
  74. backend/tests/emulator/conftest.py +88 -0
  75. backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
  76. backend/tests/emulator/test_emulator_integration.py +356 -0
  77. backend/tests/emulator/test_style_loading_direct.py +436 -0
  78. backend/tests/emulator/test_worker_logs_direct.py +229 -0
  79. backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
  80. backend/tests/requirements-test.txt +10 -0
  81. backend/tests/requirements.txt +6 -0
  82. backend/tests/test_admin_email_endpoints.py +411 -0
  83. backend/tests/test_api_integration.py +460 -0
  84. backend/tests/test_api_routes.py +93 -0
  85. backend/tests/test_audio_analysis_service.py +294 -0
  86. backend/tests/test_audio_editing_service.py +386 -0
  87. backend/tests/test_audio_search.py +1398 -0
  88. backend/tests/test_audio_services.py +378 -0
  89. backend/tests/test_auth_firestore.py +231 -0
  90. backend/tests/test_config_extended.py +68 -0
  91. backend/tests/test_credential_manager.py +377 -0
  92. backend/tests/test_dependencies.py +54 -0
  93. backend/tests/test_discord_service.py +244 -0
  94. backend/tests/test_distribution_services.py +820 -0
  95. backend/tests/test_dropbox_service.py +472 -0
  96. backend/tests/test_email_service.py +492 -0
  97. backend/tests/test_emulator_integration.py +322 -0
  98. backend/tests/test_encoding_interface.py +412 -0
  99. backend/tests/test_file_upload.py +1739 -0
  100. backend/tests/test_flacfetch_client.py +632 -0
  101. backend/tests/test_gdrive_service.py +524 -0
  102. backend/tests/test_instrumental_api.py +431 -0
  103. backend/tests/test_internal_api.py +343 -0
  104. backend/tests/test_job_creation_regression.py +583 -0
  105. backend/tests/test_job_manager.py +339 -0
  106. backend/tests/test_job_manager_notifications.py +329 -0
  107. backend/tests/test_job_notification_service.py +443 -0
  108. backend/tests/test_jobs_api.py +273 -0
  109. backend/tests/test_local_encoding_service.py +423 -0
  110. backend/tests/test_local_preview_encoding_service.py +567 -0
  111. backend/tests/test_main.py +87 -0
  112. backend/tests/test_models.py +918 -0
  113. backend/tests/test_packaging_service.py +382 -0
  114. backend/tests/test_requests.py +201 -0
  115. backend/tests/test_routes_jobs.py +282 -0
  116. backend/tests/test_routes_review.py +337 -0
  117. backend/tests/test_services.py +556 -0
  118. backend/tests/test_services_extended.py +112 -0
  119. backend/tests/test_storage_service.py +448 -0
  120. backend/tests/test_style_upload.py +261 -0
  121. backend/tests/test_template_service.py +295 -0
  122. backend/tests/test_theme_service.py +516 -0
  123. backend/tests/test_unicode_sanitization.py +522 -0
  124. backend/tests/test_upload_api.py +256 -0
  125. backend/tests/test_validate.py +156 -0
  126. backend/tests/test_video_worker_orchestrator.py +847 -0
  127. backend/tests/test_worker_log_subcollection.py +509 -0
  128. backend/tests/test_worker_logging.py +365 -0
  129. backend/tests/test_workers.py +1116 -0
  130. backend/tests/test_workers_extended.py +178 -0
  131. backend/tests/test_youtube_service.py +247 -0
  132. backend/tests/test_youtube_upload_service.py +568 -0
  133. backend/validate.py +173 -0
  134. backend/version.py +27 -0
  135. backend/workers/README.md +597 -0
  136. backend/workers/__init__.py +11 -0
  137. backend/workers/audio_worker.py +618 -0
  138. backend/workers/lyrics_worker.py +683 -0
  139. backend/workers/render_video_worker.py +483 -0
  140. backend/workers/screens_worker.py +525 -0
  141. backend/workers/style_helper.py +198 -0
  142. backend/workers/video_worker.py +1277 -0
  143. backend/workers/video_worker_orchestrator.py +701 -0
  144. backend/workers/worker_logging.py +278 -0
  145. karaoke_gen/instrumental_review/static/index.html +7 -4
  146. karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
  147. karaoke_gen/utils/__init__.py +163 -8
  148. karaoke_gen/video_background_processor.py +9 -4
  149. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/METADATA +1 -1
  150. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/RECORD +186 -41
  151. lyrics_transcriber/correction/agentic/providers/config.py +9 -5
  152. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +1 -51
  153. lyrics_transcriber/correction/corrector.py +192 -130
  154. lyrics_transcriber/correction/operations.py +24 -9
  155. lyrics_transcriber/frontend/package-lock.json +2 -2
  156. lyrics_transcriber/frontend/package.json +1 -1
  157. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
  158. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
  159. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
  160. lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
  161. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
  162. lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
  163. lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
  164. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
  165. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
  166. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
  167. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
  168. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
  169. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
  170. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
  171. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
  172. lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
  173. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
  174. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
  175. lyrics_transcriber/frontend/src/theme.ts +42 -15
  176. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  177. lyrics_transcriber/frontend/vite.config.js +5 -0
  178. lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
  179. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
  180. lyrics_transcriber/frontend/web_assets/index.html +6 -2
  181. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
  182. lyrics_transcriber/output/generator.py +17 -3
  183. lyrics_transcriber/output/video.py +60 -95
  184. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
  185. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/WHEEL +0 -0
  186. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/entry_points.txt +0 -0
  187. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,271 @@
1
+ """
2
+ Job notification service for sending emails on job state changes.
3
+
4
+ Handles:
5
+ - Job completion emails (when job enters COMPLETE status)
6
+ - Action reminder emails (when user is idle at blocking states)
7
+
8
+ This service orchestrates between the template service and email service,
9
+ building the complete notification flow for jobs.
10
+ """
11
+ import logging
12
+ import os
13
+ from typing import Optional, Dict, Any
14
+
15
+ from backend.services.email_service import get_email_service
16
+ from backend.services.template_service import get_template_service
17
+
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ # Environment variable to enable/disable auto emails
23
+ ENABLE_AUTO_EMAILS = os.getenv("ENABLE_AUTO_EMAILS", "true").lower() == "true"
24
+
25
+ # Feedback form URL (configured per environment, empty by default to avoid placeholder in emails)
26
+ FEEDBACK_FORM_URL = os.getenv("FEEDBACK_FORM_URL", "")
27
+
28
+
29
+ def _mask_email(email: str) -> str:
30
+ """Mask email for logging to protect PII. Shows first char + domain."""
31
+ if not email or "@" not in email:
32
+ return "***"
33
+ local, domain = email.split("@", 1)
34
+ if len(local) <= 1:
35
+ return f"*@{domain}"
36
+ return f"{local[0]}***@{domain}"
37
+
38
+
39
+ class JobNotificationService:
40
+ """
41
+ Service for sending job-related email notifications.
42
+
43
+ Coordinates between template service (for message rendering) and
44
+ email service (for sending).
45
+ """
46
+
47
+ def __init__(self):
48
+ """Initialize notification service."""
49
+ self.email_service = get_email_service()
50
+ self.template_service = get_template_service()
51
+ self.frontend_url = os.getenv("FRONTEND_URL", "https://gen.nomadkaraoke.com")
52
+ self.backend_url = os.getenv("BACKEND_URL", "https://api.nomadkaraoke.com")
53
+
54
+ def _build_review_url(self, job_id: str, audio_hash: Optional[str] = None, review_token: Optional[str] = None) -> str:
55
+ """Build the lyrics review URL for a job."""
56
+ import urllib.parse
57
+
58
+ review_ui_url = os.getenv("REVIEW_UI_URL", f"{self.frontend_url}/lyrics/")
59
+ base_api_url = f"{self.backend_url}/api/review/{job_id}"
60
+ encoded_api_url = urllib.parse.quote(base_api_url, safe='')
61
+
62
+ url = f"{review_ui_url}?baseApiUrl={encoded_api_url}"
63
+ if audio_hash:
64
+ url += f"&audioHash={urllib.parse.quote(audio_hash, safe='')}"
65
+ if review_token:
66
+ url += f"&reviewToken={urllib.parse.quote(review_token, safe='')}"
67
+ return url
68
+
69
+ def _build_instrumental_url(self, job_id: str, instrumental_token: Optional[str] = None) -> str:
70
+ """Build the instrumental selection URL for a job."""
71
+ base_api_url = f"{self.backend_url}/api/jobs/{job_id}"
72
+
73
+ import urllib.parse
74
+ encoded_api_url = urllib.parse.quote(base_api_url, safe='')
75
+
76
+ url = f"{self.frontend_url}/instrumental/?baseApiUrl={encoded_api_url}"
77
+ if instrumental_token:
78
+ url += f"&instrumentalToken={urllib.parse.quote(instrumental_token, safe='')}"
79
+ return url
80
+
81
+ async def send_job_completion_email(
82
+ self,
83
+ job_id: str,
84
+ user_email: str,
85
+ user_name: Optional[str] = None,
86
+ artist: Optional[str] = None,
87
+ title: Optional[str] = None,
88
+ youtube_url: Optional[str] = None,
89
+ dropbox_url: Optional[str] = None,
90
+ brand_code: Optional[str] = None,
91
+ ) -> bool:
92
+ """
93
+ Send job completion email to user.
94
+
95
+ Args:
96
+ job_id: Job ID
97
+ user_email: User's email address
98
+ user_name: User's display name (optional)
99
+ artist: Artist name
100
+ title: Song title
101
+ youtube_url: YouTube video URL
102
+ dropbox_url: Dropbox folder URL
103
+ brand_code: Release ID (e.g., "NOMAD-1178")
104
+
105
+ Returns:
106
+ True if email was sent successfully
107
+ """
108
+ if not ENABLE_AUTO_EMAILS:
109
+ logger.info(f"Auto emails disabled, skipping completion email for job {job_id}")
110
+ return False
111
+
112
+ if not user_email:
113
+ logger.warning(f"No user email for job {job_id}, skipping completion email")
114
+ return False
115
+
116
+ try:
117
+ # Render the completion message using template service
118
+ message_content = self.template_service.render_job_completion(
119
+ name=user_name,
120
+ youtube_url=youtube_url,
121
+ dropbox_url=dropbox_url,
122
+ artist=artist,
123
+ title=title,
124
+ job_id=job_id,
125
+ feedback_url=FEEDBACK_FORM_URL,
126
+ )
127
+
128
+ # Send the email with CC to admin
129
+ success = self.email_service.send_job_completion(
130
+ to_email=user_email,
131
+ message_content=message_content,
132
+ artist=artist,
133
+ title=title,
134
+ brand_code=brand_code,
135
+ cc_admin=True,
136
+ )
137
+
138
+ if success:
139
+ logger.info(f"Sent completion email for job {job_id} to {_mask_email(user_email)}")
140
+ else:
141
+ logger.error(f"Failed to send completion email for job {job_id}")
142
+
143
+ return success
144
+
145
+ except Exception as e:
146
+ logger.exception(f"Error sending completion email for job {job_id}: {e}")
147
+ return False
148
+
149
+ async def send_action_reminder_email(
150
+ self,
151
+ job_id: str,
152
+ user_email: str,
153
+ action_type: str,
154
+ user_name: Optional[str] = None,
155
+ artist: Optional[str] = None,
156
+ title: Optional[str] = None,
157
+ audio_hash: Optional[str] = None,
158
+ review_token: Optional[str] = None,
159
+ instrumental_token: Optional[str] = None,
160
+ ) -> bool:
161
+ """
162
+ Send action-needed reminder email to user.
163
+
164
+ Args:
165
+ job_id: Job ID
166
+ user_email: User's email address
167
+ action_type: Type of action needed ("lyrics" or "instrumental")
168
+ user_name: User's display name
169
+ artist: Artist name
170
+ title: Song title
171
+ audio_hash: Audio hash for review URL
172
+ review_token: Review token for URL
173
+ instrumental_token: Instrumental token for URL
174
+
175
+ Returns:
176
+ True if email was sent successfully
177
+ """
178
+ if not ENABLE_AUTO_EMAILS:
179
+ logger.info(f"Auto emails disabled, skipping reminder for job {job_id}")
180
+ return False
181
+
182
+ if not user_email:
183
+ logger.warning(f"No user email for job {job_id}, skipping reminder")
184
+ return False
185
+
186
+ try:
187
+ # Render the appropriate template
188
+ if action_type == "lyrics":
189
+ review_url = self._build_review_url(job_id, audio_hash, review_token)
190
+ message_content = self.template_service.render_action_needed_lyrics(
191
+ name=user_name,
192
+ artist=artist,
193
+ title=title,
194
+ review_url=review_url,
195
+ )
196
+ elif action_type == "instrumental":
197
+ instrumental_url = self._build_instrumental_url(job_id, instrumental_token)
198
+ message_content = self.template_service.render_action_needed_instrumental(
199
+ name=user_name,
200
+ artist=artist,
201
+ title=title,
202
+ instrumental_url=instrumental_url,
203
+ )
204
+ else:
205
+ logger.error(f"Unknown action type: {action_type}")
206
+ return False
207
+
208
+ # Send the reminder email (no CC)
209
+ success = self.email_service.send_action_reminder(
210
+ to_email=user_email,
211
+ message_content=message_content,
212
+ action_type=action_type,
213
+ artist=artist,
214
+ title=title,
215
+ )
216
+
217
+ if success:
218
+ logger.info(f"Sent {action_type} reminder for job {job_id} to {_mask_email(user_email)}")
219
+ else:
220
+ logger.error(f"Failed to send {action_type} reminder for job {job_id}")
221
+
222
+ return success
223
+
224
+ except Exception as e:
225
+ logger.exception(f"Error sending {action_type} reminder for job {job_id}: {e}")
226
+ return False
227
+
228
+ def get_completion_message(
229
+ self,
230
+ job_id: str,
231
+ user_name: Optional[str] = None,
232
+ artist: Optional[str] = None,
233
+ title: Optional[str] = None,
234
+ youtube_url: Optional[str] = None,
235
+ dropbox_url: Optional[str] = None,
236
+ ) -> str:
237
+ """
238
+ Get the rendered completion message for a job (for admin copy functionality).
239
+
240
+ Args:
241
+ job_id: Job ID
242
+ user_name: User's display name
243
+ artist: Artist name
244
+ title: Song title
245
+ youtube_url: YouTube video URL
246
+ dropbox_url: Dropbox folder URL
247
+
248
+ Returns:
249
+ Rendered message content as plain text
250
+ """
251
+ return self.template_service.render_job_completion(
252
+ name=user_name,
253
+ youtube_url=youtube_url,
254
+ dropbox_url=dropbox_url,
255
+ artist=artist,
256
+ title=title,
257
+ job_id=job_id,
258
+ feedback_url=FEEDBACK_FORM_URL,
259
+ )
260
+
261
+
262
+ # Global instance
263
+ _job_notification_service: Optional[JobNotificationService] = None
264
+
265
+
266
+ def get_job_notification_service() -> JobNotificationService:
267
+ """Get the global job notification service instance."""
268
+ global _job_notification_service
269
+ if _job_notification_service is None:
270
+ _job_notification_service = JobNotificationService()
271
+ return _job_notification_service