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,348 @@
1
+ """
2
+ OAuth credential management API routes.
3
+
4
+ Provides endpoints for:
5
+ 1. Checking credential status
6
+ 2. Device authorization flow for re-authentication
7
+ 3. Credential validation before job submission
8
+ """
9
+ import logging
10
+ from typing import Optional
11
+ from datetime import datetime
12
+ from fastapi import APIRouter, HTTPException, BackgroundTasks
13
+ from pydantic import BaseModel
14
+
15
+ from backend.services.credential_manager import (
16
+ get_credential_manager,
17
+ CredentialStatus,
18
+ CredentialCheckResult,
19
+ )
20
+ from backend.config import get_settings
21
+
22
+ logger = logging.getLogger(__name__)
23
+ router = APIRouter(prefix="/auth", tags=["auth"])
24
+
25
+
26
+ # ===========================================================================
27
+ # Request/Response Models
28
+ # ===========================================================================
29
+
30
+ class CredentialStatusResponse(BaseModel):
31
+ """Response model for credential status."""
32
+ service: str
33
+ status: str
34
+ message: str
35
+ last_checked: datetime
36
+ expires_at: Optional[datetime] = None
37
+
38
+ class Config:
39
+ from_attributes = True
40
+
41
+
42
+ class AllCredentialsStatusResponse(BaseModel):
43
+ """Response model for all credentials status."""
44
+ youtube: CredentialStatusResponse
45
+ gdrive: CredentialStatusResponse
46
+ dropbox: CredentialStatusResponse
47
+ all_valid: bool
48
+ services_needing_auth: list[str]
49
+
50
+
51
+ class DeviceAuthStartRequest(BaseModel):
52
+ """Request to start device authorization flow."""
53
+ client_id: Optional[str] = None # Optional - reads from Secret Manager if not provided
54
+ client_secret: Optional[str] = None # Optional - reads from Secret Manager if not provided
55
+
56
+
57
+ class DeviceAuthStartResponse(BaseModel):
58
+ """Response with device authorization info."""
59
+ device_code: str
60
+ user_code: str
61
+ verification_url: str
62
+ expires_in: int
63
+ interval: int
64
+ instructions: str
65
+
66
+
67
+ class DeviceAuthPollResponse(BaseModel):
68
+ """Response from polling device authorization."""
69
+ status: str # "pending", "complete", "expired", "error"
70
+ message: Optional[str] = None
71
+ credentials_saved: bool = False
72
+
73
+
74
+ class CredentialValidationRequest(BaseModel):
75
+ """Request to validate credentials for specific services."""
76
+ youtube: bool = False
77
+ gdrive: bool = False
78
+ dropbox: bool = False
79
+
80
+
81
+ class CredentialValidationResponse(BaseModel):
82
+ """Response from credential validation."""
83
+ valid: bool
84
+ invalid_services: list[str]
85
+ message: str
86
+
87
+
88
+ # ===========================================================================
89
+ # Endpoints
90
+ # ===========================================================================
91
+
92
+ @router.get("/status", response_model=AllCredentialsStatusResponse)
93
+ async def get_credentials_status():
94
+ """
95
+ Get the status of all OAuth credentials.
96
+
97
+ Returns the validation status for YouTube, Google Drive, and Dropbox
98
+ credentials, including whether they are valid, expired, or need
99
+ re-authorization.
100
+ """
101
+ manager = get_credential_manager()
102
+ results = manager.check_all_credentials()
103
+
104
+ def to_response(result: CredentialCheckResult) -> CredentialStatusResponse:
105
+ return CredentialStatusResponse(
106
+ service=result.service,
107
+ status=result.status.value,
108
+ message=result.message,
109
+ last_checked=result.last_checked,
110
+ expires_at=result.expires_at
111
+ )
112
+
113
+ services_needing_auth = [
114
+ name for name, result in results.items()
115
+ if result.status in (CredentialStatus.INVALID, CredentialStatus.EXPIRED, CredentialStatus.NOT_CONFIGURED)
116
+ ]
117
+
118
+ return AllCredentialsStatusResponse(
119
+ youtube=to_response(results["youtube"]),
120
+ gdrive=to_response(results["gdrive"]),
121
+ dropbox=to_response(results["dropbox"]),
122
+ all_valid=len(services_needing_auth) == 0,
123
+ services_needing_auth=services_needing_auth
124
+ )
125
+
126
+
127
+ @router.get("/status/{service}", response_model=CredentialStatusResponse)
128
+ async def get_service_credential_status(service: str):
129
+ """
130
+ Get the status of a specific service's OAuth credentials.
131
+
132
+ Args:
133
+ service: One of "youtube", "gdrive", "dropbox"
134
+ """
135
+ manager = get_credential_manager()
136
+
137
+ if service == "youtube":
138
+ result = manager.check_youtube_credentials()
139
+ elif service == "gdrive":
140
+ result = manager.check_gdrive_credentials()
141
+ elif service == "dropbox":
142
+ result = manager.check_dropbox_credentials()
143
+ else:
144
+ raise HTTPException(status_code=400, detail=f"Unknown service: {service}")
145
+
146
+ return CredentialStatusResponse(
147
+ service=result.service,
148
+ status=result.status.value,
149
+ message=result.message,
150
+ last_checked=result.last_checked,
151
+ expires_at=result.expires_at
152
+ )
153
+
154
+
155
+ @router.post("/validate", response_model=CredentialValidationResponse)
156
+ async def validate_credentials(request: CredentialValidationRequest):
157
+ """
158
+ Validate that credentials are available for requested services.
159
+
160
+ Use this before submitting a job to ensure all required
161
+ credentials are valid.
162
+ """
163
+ manager = get_credential_manager()
164
+ invalid_services = []
165
+
166
+ if request.youtube:
167
+ result = manager.check_youtube_credentials()
168
+ if result.status != CredentialStatus.VALID:
169
+ invalid_services.append("youtube")
170
+
171
+ if request.gdrive:
172
+ result = manager.check_gdrive_credentials()
173
+ if result.status != CredentialStatus.VALID:
174
+ invalid_services.append("gdrive")
175
+
176
+ if request.dropbox:
177
+ result = manager.check_dropbox_credentials()
178
+ if result.status != CredentialStatus.VALID:
179
+ invalid_services.append("dropbox")
180
+
181
+ if invalid_services:
182
+ return CredentialValidationResponse(
183
+ valid=False,
184
+ invalid_services=invalid_services,
185
+ message=f"The following services need re-authorization: {', '.join(invalid_services)}"
186
+ )
187
+
188
+ return CredentialValidationResponse(
189
+ valid=True,
190
+ invalid_services=[],
191
+ message="All requested credentials are valid"
192
+ )
193
+
194
+
195
+ # ===========================================================================
196
+ # Device Authorization Flow Endpoints
197
+ # ===========================================================================
198
+
199
+ @router.post("/youtube/device", response_model=DeviceAuthStartResponse)
200
+ async def start_youtube_device_auth(request: Optional[DeviceAuthStartRequest] = None):
201
+ """
202
+ Start YouTube device authorization flow.
203
+
204
+ This initiates a device auth flow that allows authorization
205
+ from any device. The user must visit the verification URL
206
+ and enter the user code.
207
+
208
+ Client credentials are loaded from Secret Manager ('youtube-client-credentials')
209
+ unless explicitly provided in the request body.
210
+
211
+ After starting, poll /auth/youtube/device/{device_code} to
212
+ check for completion.
213
+ """
214
+ manager = get_credential_manager()
215
+
216
+ try:
217
+ device_info = manager.start_youtube_device_auth(
218
+ client_id=request.client_id if request else None,
219
+ client_secret=request.client_secret if request else None
220
+ )
221
+
222
+ return DeviceAuthStartResponse(
223
+ device_code=device_info.device_code,
224
+ user_code=device_info.user_code,
225
+ verification_url=device_info.verification_url,
226
+ expires_in=device_info.expires_in,
227
+ interval=device_info.interval,
228
+ instructions=f"Visit {device_info.verification_url} and enter code: {device_info.user_code}"
229
+ )
230
+
231
+ except Exception as e:
232
+ logger.error(f"Failed to start YouTube device auth: {e}")
233
+ raise HTTPException(status_code=500, detail=str(e))
234
+
235
+
236
+ @router.get("/youtube/device/{device_code}", response_model=DeviceAuthPollResponse)
237
+ async def poll_youtube_device_auth(device_code: str):
238
+ """
239
+ Poll for YouTube device authorization completion.
240
+
241
+ Call this endpoint at the interval specified in the device auth
242
+ start response. Keep polling while status is "pending".
243
+
244
+ Status values:
245
+ - pending: User has not yet authorized
246
+ - complete: Authorization successful, credentials saved
247
+ - expired: Device code expired, start new flow
248
+ - error: An error occurred
249
+ """
250
+ manager = get_credential_manager()
251
+
252
+ status, data = manager.poll_device_auth("youtube", device_code)
253
+
254
+ return DeviceAuthPollResponse(
255
+ status=status,
256
+ message=data.get("message") if data else None,
257
+ credentials_saved=(status == "complete")
258
+ )
259
+
260
+
261
+ @router.post("/gdrive/device", response_model=DeviceAuthStartResponse)
262
+ async def start_gdrive_device_auth(request: Optional[DeviceAuthStartRequest] = None):
263
+ """
264
+ Start Google Drive device authorization flow.
265
+
266
+ Client credentials are loaded from Secret Manager ('gdrive-client-credentials')
267
+ unless explicitly provided in the request body.
268
+
269
+ After starting, poll /auth/gdrive/device/{device_code} to check for completion.
270
+ """
271
+ manager = get_credential_manager()
272
+
273
+ try:
274
+ device_info = manager.start_gdrive_device_auth(
275
+ client_id=request.client_id if request else None,
276
+ client_secret=request.client_secret if request else None
277
+ )
278
+
279
+ return DeviceAuthStartResponse(
280
+ device_code=device_info.device_code,
281
+ user_code=device_info.user_code,
282
+ verification_url=device_info.verification_url,
283
+ expires_in=device_info.expires_in,
284
+ interval=device_info.interval,
285
+ instructions=f"Visit {device_info.verification_url} and enter code: {device_info.user_code}"
286
+ )
287
+
288
+ except Exception as e:
289
+ logger.error(f"Failed to start Google Drive device auth: {e}")
290
+ raise HTTPException(status_code=500, detail=str(e))
291
+
292
+
293
+ @router.get("/gdrive/device/{device_code}", response_model=DeviceAuthPollResponse)
294
+ async def poll_gdrive_device_auth(device_code: str):
295
+ """
296
+ Poll for Google Drive device authorization completion.
297
+ """
298
+ manager = get_credential_manager()
299
+
300
+ status, data = manager.poll_device_auth("gdrive", device_code)
301
+
302
+ return DeviceAuthPollResponse(
303
+ status=status,
304
+ message=data.get("message") if data else None,
305
+ credentials_saved=(status == "complete")
306
+ )
307
+
308
+
309
+ # ===========================================================================
310
+ # Alert Testing
311
+ # ===========================================================================
312
+
313
+ @router.post("/test-alert")
314
+ async def test_credential_alert(background_tasks: BackgroundTasks):
315
+ """
316
+ Test the credential alert mechanism.
317
+
318
+ Sends a test alert to Discord if configured.
319
+ """
320
+ settings = get_settings()
321
+ discord_url = settings.get_secret("discord-alert-webhook")
322
+
323
+ if not discord_url:
324
+ raise HTTPException(
325
+ status_code=400,
326
+ detail="Discord alert webhook not configured"
327
+ )
328
+
329
+ manager = get_credential_manager()
330
+
331
+ # Create a fake invalid result for testing
332
+ from backend.services.credential_manager import CredentialCheckResult
333
+ test_results = [
334
+ CredentialCheckResult(
335
+ service="test",
336
+ status=CredentialStatus.INVALID,
337
+ message="This is a test alert",
338
+ last_checked=datetime.utcnow()
339
+ )
340
+ ]
341
+
342
+ background_tasks.add_task(
343
+ manager.send_credential_alert,
344
+ test_results,
345
+ discord_url
346
+ )
347
+
348
+ return {"message": "Test alert queued"}