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,409 @@
1
+ """
2
+ Health check routes.
3
+ """
4
+ import os
5
+ import logging
6
+ import subprocess
7
+ import platform
8
+ from fastapi import APIRouter
9
+ from typing import Dict, Any, Optional
10
+
11
+ from backend.version import VERSION
12
+ from backend.services.flacfetch_client import get_flacfetch_client
13
+ from backend.services.email_service import get_email_service
14
+ from backend.services.stripe_service import get_stripe_service
15
+ from backend.services.encoding_service import get_encoding_service
16
+ from backend.services.spacy_preloader import get_preloaded_model, is_model_preloaded
17
+ from backend.services.nltk_preloader import get_preloaded_cmudict, is_cmudict_preloaded
18
+ from backend.services.langfuse_preloader import get_preloaded_langfuse_handler, is_langfuse_preloaded, is_langfuse_configured
19
+
20
+ router = APIRouter()
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def get_system_info() -> Dict[str, Any]:
25
+ """Get system information for performance analysis."""
26
+ info = {
27
+ "platform": platform.system(),
28
+ "platform_release": platform.release(),
29
+ "python_version": platform.python_version(),
30
+ }
31
+
32
+ # Get CPU info
33
+ try:
34
+ cpu_count = os.cpu_count() or 0
35
+ info["cpu_count"] = cpu_count
36
+
37
+ # Try to get more detailed CPU info on Linux
38
+ if platform.system() == "Linux":
39
+ try:
40
+ with open("/proc/cpuinfo", "r") as f:
41
+ for line in f:
42
+ if "model name" in line:
43
+ info["cpu_model"] = line.split(":")[1].strip()
44
+ break
45
+ except Exception:
46
+ pass
47
+ except Exception as e:
48
+ info["cpu_error"] = str(e)
49
+
50
+ # Get memory info
51
+ try:
52
+ if platform.system() == "Linux":
53
+ with open("/proc/meminfo", "r") as f:
54
+ for line in f:
55
+ if line.startswith("MemTotal:"):
56
+ mem_kb = int(line.split()[1])
57
+ info["memory_gb"] = round(mem_kb / (1024 * 1024), 1)
58
+ break
59
+ except Exception as e:
60
+ info["memory_error"] = str(e)
61
+
62
+ return info
63
+
64
+
65
+ def get_ffmpeg_info() -> Dict[str, Any]:
66
+ """Get FFmpeg version and configuration for debugging encoding performance."""
67
+ try:
68
+ # Get FFmpeg version
69
+ result = subprocess.run(
70
+ ["ffmpeg", "-version"],
71
+ capture_output=True,
72
+ text=True,
73
+ timeout=10
74
+ )
75
+ if result.returncode == 0:
76
+ lines = result.stdout.split("\n")
77
+ version_line = lines[0] if lines else "unknown"
78
+
79
+ # Check for hardware acceleration support
80
+ hw_accel = {}
81
+ config_result = subprocess.run(
82
+ ["ffmpeg", "-hwaccels"],
83
+ capture_output=True,
84
+ text=True,
85
+ timeout=10
86
+ )
87
+ if config_result.returncode == 0:
88
+ hw_lines = config_result.stdout.strip().split("\n")
89
+ hw_accel["available"] = [h.strip() for h in hw_lines[1:] if h.strip()]
90
+
91
+ # Check encoder availability
92
+ encoders = {}
93
+ encoder_result = subprocess.run(
94
+ ["ffmpeg", "-encoders"],
95
+ capture_output=True,
96
+ text=True,
97
+ timeout=10
98
+ )
99
+ if encoder_result.returncode == 0:
100
+ encoder_output = encoder_result.stdout
101
+ # Check for specific encoders we care about
102
+ encoders["libx264"] = "libx264" in encoder_output
103
+ encoders["h264_nvenc"] = "h264_nvenc" in encoder_output
104
+ encoders["h264_vaapi"] = "h264_vaapi" in encoder_output
105
+ encoders["hevc_nvenc"] = "hevc_nvenc" in encoder_output
106
+
107
+ return {
108
+ "available": True,
109
+ "version": version_line,
110
+ "hw_acceleration": hw_accel,
111
+ "encoders": encoders,
112
+ }
113
+ else:
114
+ return {
115
+ "available": False,
116
+ "error": result.stderr[:500] if result.stderr else "Unknown error",
117
+ }
118
+ except subprocess.TimeoutExpired:
119
+ return {"available": False, "error": "Timeout getting FFmpeg info"}
120
+ except FileNotFoundError:
121
+ return {"available": False, "error": "FFmpeg not found"}
122
+ except Exception as e:
123
+ return {"available": False, "error": str(e)}
124
+
125
+
126
+ def check_transmission_status() -> Dict[str, Any]:
127
+ """Check if Transmission daemon is available and responsive."""
128
+ try:
129
+ import transmission_rpc
130
+ host = os.environ.get("TRANSMISSION_HOST", "localhost")
131
+ port = int(os.environ.get("TRANSMISSION_PORT", "9091"))
132
+
133
+ client = transmission_rpc.Client(host=host, port=port, timeout=5)
134
+ stats = client.session_stats()
135
+
136
+ # Get detailed torrent info
137
+ torrents = client.get_torrents()
138
+ torrent_details = []
139
+ for t in torrents:
140
+ torrent_info = {
141
+ "name": t.name,
142
+ "progress": round(t.progress, 1),
143
+ "status": str(t.status),
144
+ "peers": t.peers_connected if hasattr(t, 'peers_connected') else 0,
145
+ "download_speed": round(t.rate_download / 1024, 1) if hasattr(t, 'rate_download') else 0, # KB/s
146
+ }
147
+ # Check if stalled (downloading but no progress and no peers)
148
+ if t.status.downloading and t.progress < 100 and torrent_info['peers'] == 0:
149
+ torrent_info['stalled'] = True
150
+ torrent_details.append(torrent_info)
151
+
152
+ return {
153
+ "available": True,
154
+ "host": host,
155
+ "port": port,
156
+ "download_dir": stats.download_dir if hasattr(stats, 'download_dir') else None,
157
+ "active_torrent_count": stats.active_torrent_count if hasattr(stats, 'active_torrent_count') else 0,
158
+ "torrents": torrent_details,
159
+ }
160
+ except ImportError as e:
161
+ return {
162
+ "available": False,
163
+ "error": f"transmission_rpc not installed: {e}",
164
+ }
165
+ except Exception as e:
166
+ return {
167
+ "available": False,
168
+ "host": os.environ.get("TRANSMISSION_HOST", "localhost"),
169
+ "port": int(os.environ.get("TRANSMISSION_PORT", "9091")),
170
+ "error": str(e),
171
+ }
172
+
173
+
174
+ @router.get("/health")
175
+ async def health_check() -> Dict[str, str]:
176
+ """Health check endpoint."""
177
+ return {
178
+ "status": "healthy",
179
+ "service": "karaoke-gen-backend"
180
+ }
181
+
182
+
183
+ async def check_flacfetch_service_status() -> Dict[str, Any]:
184
+ """Check if remote flacfetch service is available and healthy."""
185
+ client = get_flacfetch_client()
186
+
187
+ if not client:
188
+ return {
189
+ "configured": False,
190
+ "message": "Remote flacfetch service not configured (FLACFETCH_API_URL not set)",
191
+ }
192
+
193
+ try:
194
+ health = await client.health_check()
195
+ return {
196
+ "configured": True,
197
+ "available": True,
198
+ "status": health.get("status"),
199
+ "version": health.get("version"),
200
+ "transmission": health.get("transmission", {}),
201
+ "disk": health.get("disk", {}),
202
+ "providers": health.get("providers", {}),
203
+ }
204
+ except Exception as e:
205
+ return {
206
+ "configured": True,
207
+ "available": False,
208
+ "error": str(e),
209
+ }
210
+
211
+
212
+ async def check_encoding_worker_status() -> Dict[str, Any]:
213
+ """Check if GCE encoding worker is available and healthy."""
214
+ encoding_service = get_encoding_service()
215
+
216
+ if not encoding_service.is_configured:
217
+ return {
218
+ "configured": False,
219
+ "enabled": encoding_service.settings.use_gce_encoding,
220
+ "message": "GCE encoding worker not configured (ENCODING_WORKER_URL or API key not set)",
221
+ }
222
+
223
+ try:
224
+ health = await encoding_service.health_check()
225
+ return {
226
+ "configured": True,
227
+ "enabled": encoding_service.is_enabled,
228
+ "available": health.get("status") == "ok",
229
+ "status": health.get("status"),
230
+ "active_jobs": health.get("active_jobs", 0),
231
+ "queue_length": health.get("queue_length", 0),
232
+ "ffmpeg_version": health.get("ffmpeg_version"),
233
+ "wheel_version": health.get("wheel_version"),
234
+ }
235
+ except Exception as e:
236
+ return {
237
+ "configured": True,
238
+ "enabled": encoding_service.is_enabled,
239
+ "available": False,
240
+ "error": str(e),
241
+ }
242
+
243
+
244
+ @router.get("/health/encoding-worker")
245
+ async def encoding_worker_health() -> Dict[str, Any]:
246
+ """
247
+ Lightweight endpoint to check encoding worker status.
248
+
249
+ Returns minimal info for frontend footer display.
250
+ No authentication required.
251
+ """
252
+ status = await check_encoding_worker_status()
253
+
254
+ # Return simplified response for frontend
255
+ if not status.get("configured"):
256
+ return {
257
+ "available": False,
258
+ "status": "not_configured",
259
+ }
260
+
261
+ if not status.get("available"):
262
+ return {
263
+ "available": False,
264
+ "status": "offline",
265
+ "error": status.get("error"),
266
+ }
267
+
268
+ return {
269
+ "available": True,
270
+ "status": "ok",
271
+ "version": status.get("wheel_version"),
272
+ "active_jobs": status.get("active_jobs", 0),
273
+ "queue_length": status.get("queue_length", 0),
274
+ }
275
+
276
+
277
+ @router.get("/health/detailed")
278
+ async def detailed_health_check() -> Dict[str, Any]:
279
+ """
280
+ Detailed health check including dependencies.
281
+
282
+ Use this to debug issues with Transmission, flacfetch service, etc.
283
+ """
284
+ transmission_status = check_transmission_status()
285
+ flacfetch_status = await check_flacfetch_service_status()
286
+ encoding_status = await check_encoding_worker_status()
287
+
288
+ # Check email service
289
+ email_service = get_email_service()
290
+ email_status = {
291
+ "configured": email_service.is_configured(),
292
+ "provider": type(email_service.provider).__name__,
293
+ }
294
+
295
+ # Check Stripe service
296
+ stripe_service = get_stripe_service()
297
+ stripe_status = {
298
+ "configured": stripe_service.is_configured(),
299
+ }
300
+
301
+ # Log for debugging
302
+ if not transmission_status.get("available"):
303
+ logger.warning(f"Local Transmission not available: {transmission_status.get('error')}")
304
+ else:
305
+ logger.info(f"Local Transmission available at {transmission_status.get('host')}:{transmission_status.get('port')}")
306
+
307
+ if flacfetch_status.get("configured") and not flacfetch_status.get("available"):
308
+ logger.warning(f"Remote flacfetch service not available: {flacfetch_status.get('error')}")
309
+ elif flacfetch_status.get("available"):
310
+ logger.info(f"Remote flacfetch service healthy: {flacfetch_status.get('status')}")
311
+
312
+ if not email_status["configured"]:
313
+ logger.warning("Email service not configured - magic links will not work")
314
+
315
+ if not stripe_status["configured"]:
316
+ logger.warning("Stripe service not configured - payments will not work")
317
+
318
+ # Get system and FFmpeg info for performance analysis
319
+ system_info = get_system_info()
320
+ ffmpeg_info = get_ffmpeg_info()
321
+
322
+ return {
323
+ "status": "healthy",
324
+ "service": "karaoke-gen-backend",
325
+ "version": VERSION,
326
+ "system": system_info,
327
+ "ffmpeg": ffmpeg_info,
328
+ "dependencies": {
329
+ "transmission_local": transmission_status,
330
+ "flacfetch_remote": flacfetch_status,
331
+ "encoding_worker": encoding_status,
332
+ },
333
+ "services": {
334
+ "email": email_status,
335
+ "stripe": stripe_status,
336
+ }
337
+ }
338
+
339
+
340
+ @router.get("/health/preload-status")
341
+ async def preload_status() -> Dict[str, Any]:
342
+ """
343
+ Check status of preloaded resources for performance optimization.
344
+
345
+ Use this endpoint to verify that NLTK, SpaCy, and Langfuse resources
346
+ were successfully preloaded at container startup. If any show as
347
+ not preloaded, check Cloud Run startup logs for errors.
348
+
349
+ Expected state after successful deployment:
350
+ - spacy.preloaded: true
351
+ - nltk.preloaded: true
352
+ - langfuse.preloaded: true (if configured) or configured: false
353
+ """
354
+ # SpaCy status
355
+ spacy_model = get_preloaded_model("en_core_web_sm")
356
+ spacy_status = {
357
+ "preloaded": is_model_preloaded("en_core_web_sm"),
358
+ "model": "en_core_web_sm",
359
+ }
360
+ if spacy_model:
361
+ spacy_status["vocab_size"] = len(spacy_model.vocab)
362
+
363
+ # NLTK status
364
+ cmudict = get_preloaded_cmudict()
365
+ nltk_status = {
366
+ "preloaded": is_cmudict_preloaded(),
367
+ "resource": "cmudict",
368
+ }
369
+ if cmudict:
370
+ nltk_status["entries"] = len(cmudict)
371
+
372
+ # Langfuse status
373
+ langfuse_handler = get_preloaded_langfuse_handler()
374
+ langfuse_status = {
375
+ "configured": is_langfuse_configured(),
376
+ "preloaded": is_langfuse_preloaded(),
377
+ }
378
+ if langfuse_handler:
379
+ langfuse_status["handler_type"] = type(langfuse_handler).__name__
380
+
381
+ # Overall status
382
+ all_preloaded = (
383
+ spacy_status["preloaded"]
384
+ and nltk_status["preloaded"]
385
+ and (langfuse_status["preloaded"] or not langfuse_status["configured"])
386
+ )
387
+
388
+ return {
389
+ "status": "ok" if all_preloaded else "degraded",
390
+ "message": "All resources preloaded" if all_preloaded else "Some resources not preloaded - check startup logs",
391
+ "spacy": spacy_status,
392
+ "nltk": nltk_status,
393
+ "langfuse": langfuse_status,
394
+ "performance_impact": {
395
+ "spacy_preload": "Saves ~60s on first lyrics correction",
396
+ "nltk_preload": "Saves ~100-150s on SyllablesMatchHandler init",
397
+ "langfuse_preload": "Saves ~200s on AgenticCorrector init",
398
+ }
399
+ }
400
+
401
+
402
+ @router.get("/readiness")
403
+ async def readiness_check() -> Dict[str, str]:
404
+ """Readiness check endpoint for Cloud Run."""
405
+ return {
406
+ "status": "ready",
407
+ "service": "karaoke-gen-backend"
408
+ }
409
+