karaoke-gen 0.96.0__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 (30) hide show
  1. backend/api/routes/admin.py +184 -91
  2. backend/api/routes/audio_search.py +16 -6
  3. backend/api/routes/file_upload.py +57 -21
  4. backend/api/routes/health.py +65 -0
  5. backend/api/routes/jobs.py +19 -0
  6. backend/api/routes/users.py +543 -44
  7. backend/main.py +25 -1
  8. backend/services/encoding_service.py +128 -31
  9. backend/services/job_manager.py +12 -1
  10. backend/services/langfuse_preloader.py +98 -0
  11. backend/services/nltk_preloader.py +122 -0
  12. backend/services/spacy_preloader.py +65 -0
  13. backend/services/stripe_service.py +96 -0
  14. backend/tests/emulator/conftest.py +22 -1
  15. backend/tests/test_job_manager.py +25 -8
  16. backend/tests/test_jobs_api.py +11 -1
  17. backend/tests/test_spacy_preloader.py +119 -0
  18. backend/utils/test_data.py +27 -0
  19. backend/workers/screens_worker.py +16 -6
  20. {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.99.3.dist-info}/METADATA +1 -1
  21. {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.99.3.dist-info}/RECORD +30 -25
  22. lyrics_transcriber/correction/agentic/agent.py +17 -6
  23. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +96 -43
  24. lyrics_transcriber/correction/agentic/providers/model_factory.py +27 -6
  25. lyrics_transcriber/correction/anchor_sequence.py +151 -37
  26. lyrics_transcriber/correction/handlers/syllables_match.py +44 -2
  27. lyrics_transcriber/correction/phrase_analyzer.py +18 -0
  28. {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.99.3.dist-info}/WHEEL +0 -0
  29. {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.99.3.dist-info}/entry_points.txt +0 -0
  30. {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.99.3.dist-info}/licenses/LICENSE +0 -0
@@ -39,6 +39,24 @@ os.environ["ADMIN_TOKENS"] = "test-admin-token"
39
39
 
40
40
  # Only import app if emulators are running
41
41
  if emulators_running():
42
+ from unittest.mock import Mock
43
+
44
+ # Mock theme service BEFORE importing app to ensure all jobs get theme_id="nomad"
45
+ _mock_theme_service = Mock()
46
+ _mock_theme_service.get_default_theme_id.return_value = "nomad"
47
+ _mock_theme_service.get_theme.return_value = None
48
+
49
+ # Apply patch at module load time (before app imports the service)
50
+ _theme_patches = [
51
+ patch("backend.services.theme_service.get_theme_service", return_value=_mock_theme_service),
52
+ patch("backend.api.routes.audio_search.get_theme_service", return_value=_mock_theme_service),
53
+ patch("backend.api.routes.file_upload.get_theme_service", return_value=_mock_theme_service),
54
+ patch("backend.api.routes.users.get_theme_service", return_value=_mock_theme_service),
55
+ patch("backend.api.routes.jobs.get_theme_service", return_value=_mock_theme_service),
56
+ ]
57
+ for p in _theme_patches:
58
+ p.start()
59
+
42
60
  from fastapi.testclient import TestClient
43
61
  from backend.main import app
44
62
  else:
@@ -76,7 +94,10 @@ def mock_worker_service():
76
94
 
77
95
  @pytest.fixture(scope="session")
78
96
  def client(mock_worker_service):
79
- """Create FastAPI test client with mocked workers."""
97
+ """Create FastAPI test client with mocked workers.
98
+
99
+ Theme service is mocked at module load time (see above).
100
+ """
80
101
  with patch("backend.api.routes.file_upload.worker_service", mock_worker_service):
81
102
  return TestClient(app)
82
103
 
@@ -33,13 +33,28 @@ def job_manager(mock_firestore_service):
33
33
 
34
34
  class TestJobCreation:
35
35
  """Test job creation logic."""
36
-
36
+
37
+ def test_create_job_requires_theme_id(self, job_manager, mock_firestore_service):
38
+ """Test that jobs without theme_id are rejected."""
39
+ job_create = JobCreate(
40
+ artist="Test Artist",
41
+ title="Test Song"
42
+ # No theme_id - should fail
43
+ )
44
+
45
+ with pytest.raises(ValueError, match="theme_id is required"):
46
+ job_manager.create_job(job_create)
47
+
48
+ # Verify Firestore was NOT called
49
+ mock_firestore_service.create_job.assert_not_called()
50
+
37
51
  def test_create_job_with_url(self, job_manager, mock_firestore_service):
38
52
  """Test creating a job with YouTube URL."""
39
53
  job_create = JobCreate(
40
54
  url="https://youtube.com/watch?v=test",
41
55
  artist="Test Artist",
42
- title="Test Song"
56
+ title="Test Song",
57
+ theme_id="nomad" # Required for all jobs
43
58
  )
44
59
 
45
60
  # The actual create_job method creates the job and returns it
@@ -59,7 +74,8 @@ class TestJobCreation:
59
74
  """Test creating a job without URL (for file upload)."""
60
75
  job_create = JobCreate(
61
76
  artist="Test Artist",
62
- title="Test Song"
77
+ title="Test Song",
78
+ theme_id="nomad" # Required for all jobs
63
79
  )
64
80
 
65
81
  job = job_manager.create_job(job_create)
@@ -70,8 +86,8 @@ class TestJobCreation:
70
86
 
71
87
  def test_create_job_generates_unique_id(self, job_manager, mock_firestore_service):
72
88
  """Test that each job gets a unique ID."""
73
- job_create = JobCreate()
74
-
89
+ job_create = JobCreate(theme_id="nomad") # Required for all jobs
90
+
75
91
  # Create multiple jobs
76
92
  ids = []
77
93
  for i in range(5):
@@ -89,8 +105,8 @@ class TestJobCreation:
89
105
 
90
106
  def test_create_job_sets_initial_status(self, job_manager, mock_firestore_service):
91
107
  """Test that new jobs start with PENDING status."""
92
- job_create = JobCreate()
93
-
108
+ job_create = JobCreate(theme_id="nomad") # Required for all jobs
109
+
94
110
  mock_firestore_service.create_job.return_value = Job(
95
111
  job_id="test123",
96
112
  status=JobStatus.PENDING,
@@ -105,13 +121,14 @@ class TestJobCreation:
105
121
 
106
122
  def test_create_job_with_distribution_settings(self, job_manager, mock_firestore_service):
107
123
  """Test that distribution settings are passed from JobCreate to Job.
108
-
124
+
109
125
  This was a bug where brand_prefix, dropbox_path, gdrive_folder_id, and
110
126
  discord_webhook_url were NOT being passed to the Job constructor.
111
127
  """
112
128
  job_create = JobCreate(
113
129
  artist="Test Artist",
114
130
  title="Test Song",
131
+ theme_id="nomad", # Required for all jobs
115
132
  brand_prefix="NOMAD",
116
133
  discord_webhook_url="https://discord.com/webhook/test",
117
134
  dropbox_path="/Karaoke/Tracks-Organized",
@@ -50,7 +50,16 @@ def mock_worker_service():
50
50
 
51
51
 
52
52
  @pytest.fixture
53
- def client(mock_job_manager, mock_worker_service):
53
+ def mock_theme_service():
54
+ """Create a mock ThemeService that returns 'nomad' as default theme."""
55
+ service = MagicMock()
56
+ service.get_default_theme_id.return_value = "nomad"
57
+ service.get_theme.return_value = None
58
+ return service
59
+
60
+
61
+ @pytest.fixture
62
+ def client(mock_job_manager, mock_worker_service, mock_theme_service):
54
63
  """Create TestClient with mocked dependencies."""
55
64
  mock_creds = MagicMock()
56
65
  mock_creds.universe_domain = 'googleapis.com'
@@ -63,6 +72,7 @@ def client(mock_job_manager, mock_worker_service):
63
72
  # Also patch JobManager class used in dependencies.py for auth checks
64
73
  with patch('backend.api.routes.jobs.job_manager', mock_job_manager), \
65
74
  patch('backend.api.routes.jobs.worker_service', mock_worker_service), \
75
+ patch('backend.api.routes.jobs.get_theme_service', return_value=mock_theme_service), \
66
76
  patch('backend.services.job_manager.JobManager', mock_job_manager_factory), \
67
77
  patch('backend.services.firestore_service.firestore'), \
68
78
  patch('backend.services.storage_service.storage'), \
@@ -0,0 +1,119 @@
1
+ """Tests for SpaCy preloader service."""
2
+
3
+ import pytest
4
+ from unittest.mock import patch, MagicMock
5
+
6
+ from backend.services.spacy_preloader import (
7
+ preload_spacy_model,
8
+ get_preloaded_model,
9
+ is_model_preloaded,
10
+ clear_preloaded_models,
11
+ )
12
+
13
+
14
+ class TestSpacyPreloader:
15
+ """Tests for SpaCy preloading functionality."""
16
+
17
+ def setup_method(self):
18
+ """Clear preloaded models before each test."""
19
+ clear_preloaded_models()
20
+
21
+ def teardown_method(self):
22
+ """Clear preloaded models after each test."""
23
+ clear_preloaded_models()
24
+
25
+ def test_preload_spacy_model_loads_and_stores(self):
26
+ """GIVEN no preloaded models
27
+ WHEN preload_spacy_model is called
28
+ THEN model should be loaded and stored in singleton."""
29
+ mock_nlp = MagicMock()
30
+
31
+ with patch("spacy.load", return_value=mock_nlp) as mock_load:
32
+ preload_spacy_model("en_core_web_sm")
33
+
34
+ mock_load.assert_called_once_with("en_core_web_sm")
35
+ assert is_model_preloaded("en_core_web_sm")
36
+ assert get_preloaded_model("en_core_web_sm") is mock_nlp
37
+
38
+ def test_preload_is_idempotent(self):
39
+ """GIVEN a model already preloaded
40
+ WHEN preload_spacy_model is called again
41
+ THEN model should not be reloaded."""
42
+ mock_nlp = MagicMock()
43
+
44
+ with patch("spacy.load", return_value=mock_nlp) as mock_load:
45
+ preload_spacy_model("en_core_web_sm")
46
+ preload_spacy_model("en_core_web_sm") # Second call
47
+
48
+ # Should only load once
49
+ assert mock_load.call_count == 1
50
+
51
+ def test_get_preloaded_model_returns_none_if_not_loaded(self):
52
+ """GIVEN no preloaded models
53
+ WHEN get_preloaded_model is called
54
+ THEN should return None."""
55
+ assert get_preloaded_model("en_core_web_sm") is None
56
+ assert not is_model_preloaded("en_core_web_sm")
57
+
58
+ def test_preload_different_models(self):
59
+ """GIVEN no preloaded models
60
+ WHEN preload_spacy_model is called with different model names
61
+ THEN each model should be loaded and stored separately."""
62
+ mock_nlp_sm = MagicMock(name="en_core_web_sm")
63
+ mock_nlp_md = MagicMock(name="en_core_web_md")
64
+
65
+ def mock_load(model_name):
66
+ if model_name == "en_core_web_sm":
67
+ return mock_nlp_sm
68
+ elif model_name == "en_core_web_md":
69
+ return mock_nlp_md
70
+ raise ValueError(f"Unknown model: {model_name}")
71
+
72
+ with patch("spacy.load", side_effect=mock_load):
73
+ preload_spacy_model("en_core_web_sm")
74
+ preload_spacy_model("en_core_web_md")
75
+
76
+ assert is_model_preloaded("en_core_web_sm")
77
+ assert is_model_preloaded("en_core_web_md")
78
+ assert get_preloaded_model("en_core_web_sm") is mock_nlp_sm
79
+ assert get_preloaded_model("en_core_web_md") is mock_nlp_md
80
+
81
+ def test_clear_preloaded_models(self):
82
+ """GIVEN preloaded models
83
+ WHEN clear_preloaded_models is called
84
+ THEN all models should be removed."""
85
+ mock_nlp = MagicMock()
86
+
87
+ with patch("spacy.load", return_value=mock_nlp):
88
+ preload_spacy_model("en_core_web_sm")
89
+ assert is_model_preloaded("en_core_web_sm")
90
+
91
+ clear_preloaded_models()
92
+ assert not is_model_preloaded("en_core_web_sm")
93
+ assert get_preloaded_model("en_core_web_sm") is None
94
+
95
+ def test_preload_failure_raises_exception(self):
96
+ """GIVEN spacy.load raises an exception
97
+ WHEN preload_spacy_model is called
98
+ THEN exception should be propagated."""
99
+ with patch(
100
+ "spacy.load",
101
+ side_effect=OSError("Model not found"),
102
+ ):
103
+ with pytest.raises(OSError, match="Model not found"):
104
+ preload_spacy_model("nonexistent_model")
105
+
106
+ # Model should not be marked as preloaded
107
+ assert not is_model_preloaded("nonexistent_model")
108
+
109
+ def test_preload_uses_default_model_name(self):
110
+ """GIVEN no model name specified
111
+ WHEN preload_spacy_model is called without arguments
112
+ THEN should use en_core_web_sm as default."""
113
+ mock_nlp = MagicMock()
114
+
115
+ with patch("spacy.load", return_value=mock_nlp) as mock_load:
116
+ preload_spacy_model()
117
+
118
+ mock_load.assert_called_once_with("en_core_web_sm")
119
+ assert is_model_preloaded("en_core_web_sm")
@@ -0,0 +1,27 @@
1
+ """
2
+ Utilities for identifying test data.
3
+
4
+ Test data is generated by automated E2E tests and should be filterable
5
+ in admin dashboards to show only real user data by default.
6
+ """
7
+
8
+ # Email domains used by automated testing frameworks
9
+ TEST_EMAIL_DOMAINS = [
10
+ "inbox.testmail.app", # Used by E2E happy path tests
11
+ ]
12
+
13
+
14
+ def is_test_email(email: str) -> bool:
15
+ """
16
+ Check if an email address belongs to automated test data.
17
+
18
+ Args:
19
+ email: Email address to check
20
+
21
+ Returns:
22
+ True if the email matches a test email domain pattern
23
+ """
24
+ if not email:
25
+ return False
26
+ email_lower = email.lower()
27
+ return any(email_lower.endswith(f"@{domain}") for domain in TEST_EMAIL_DOMAINS)
@@ -218,26 +218,36 @@ async def generate_screens(job_id: str) -> bool:
218
218
  def _validate_prerequisites(job) -> bool:
219
219
  """
220
220
  Validate that both audio and lyrics processing are complete.
221
-
221
+
222
222
  Single Responsibility: Validation logic separated from main flow.
223
-
223
+
224
224
  Args:
225
225
  job: Job object
226
-
226
+
227
227
  Returns:
228
228
  True if prerequisites met, False otherwise
229
229
  """
230
+ # SAFETY NET: Enforce theme requirement at processing time
231
+ # This catches any jobs that somehow bypassed JobManager.create_job() validation
232
+ if not job.theme_id:
233
+ logger.error(
234
+ f"Job {job.job_id}: CRITICAL - No theme_id configured. "
235
+ "All jobs must have a theme to generate styled videos. "
236
+ "This job should have been rejected at creation time."
237
+ )
238
+ return False
239
+
230
240
  audio_complete = job.state_data.get('audio_complete', False)
231
241
  lyrics_complete = job.state_data.get('lyrics_complete', False)
232
-
242
+
233
243
  if not audio_complete:
234
244
  logger.error(f"Job {job.job_id}: Audio processing not complete")
235
245
  return False
236
-
246
+
237
247
  if not lyrics_complete:
238
248
  logger.error(f"Job {job.job_id}: Lyrics processing not complete")
239
249
  return False
240
-
250
+
241
251
  if not job.artist or not job.title:
242
252
  logger.error(f"Job {job.job_id}: Missing artist or title")
243
253
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karaoke-gen
3
- Version: 0.96.0
3
+ Version: 0.99.3
4
4
  Summary: Generate karaoke videos with synchronized lyrics. Handles the entire process from downloading audio and lyrics to creating the final video with title screens.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -7,18 +7,18 @@ backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  backend/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  backend/api/dependencies.py,sha256=-61nHBhiihUDSVMQd3VuHLP7uvKrUbm1y-j9RmV6_zc,16871
9
9
  backend/api/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- backend/api/routes/admin.py,sha256=QJ2B-90HySVd8la1DN2IGeKPq6Qzj4TVFZhgZOOoKBo,26060
11
- backend/api/routes/audio_search.py,sha256=wJ_5j_q3yaHs5W03M2B5FdTYUrAVqHV5tW0cBdRrzxI,39153
10
+ backend/api/routes/admin.py,sha256=r9Q7cOnDJjLnQVQ5ql3S6eUOXiVss9hEanHMz6I0Mp4,30499
11
+ backend/api/routes/audio_search.py,sha256=r8VPlyA40DUoFPcvNVM_p4faJsjv_Ta_qsY7PhvAzfw,39678
12
12
  backend/api/routes/auth.py,sha256=G1U2KwS3uqBJpgvg2PIe_mZOWCJKPFhhewrmKbW3Z_s,11531
13
- backend/api/routes/file_upload.py,sha256=Skh4ewXo5I8XReXhXlJZO7tUbvdwz1YdNxFmJqYIFnM,92290
14
- backend/api/routes/health.py,sha256=6HSLJVtNaYHA-xaXgHsXMZUJayYYXXNdX-oThDLDCcw,11609
13
+ backend/api/routes/file_upload.py,sha256=GQ_m1ZQxFOxxkrjPY6a-9K3jvBC-Jwo_1mRYjZ6cxic,94156
14
+ backend/api/routes/health.py,sha256=iZlhJpmz4vminUy_qrzkyk_1tgG1gmITz4jEWaCWJnM,14010
15
15
  backend/api/routes/internal.py,sha256=N_Fv0hYQ9gleYNehjHaZb1OKzM78PXDbcqTmHasll2w,15276
16
- backend/api/routes/jobs.py,sha256=xAIbcNrYLHlWYHvsv1C5BakdvgXdN5mpa_RRzJ3QSeI,59905
16
+ backend/api/routes/jobs.py,sha256=L0Aot-BEgOlNv-_AqEVIkO5Uv4BTP5gjmBvPzjWmlR8,60866
17
17
  backend/api/routes/review.py,sha256=hEvLUcb07DWcBc6E4fTa91zam3Z_a-x2vIvLP_OKR9o,28256
18
18
  backend/api/routes/themes.py,sha256=_fPZg9N2KN-yyr1YR2vAy8FIm8PqVowboQ4WEpFTYiQ,4721
19
- backend/api/routes/users.py,sha256=1EaBzR4qEzUklC3P3Yz-15Usm9tys3VPee0azmKzLj8,34355
19
+ backend/api/routes/users.py,sha256=9ffodZUGzxjoHXLQdOZ-PLEqK0k-wd4zK4xp_GD2wZk,56781
20
20
  backend/config.py,sha256=-HsMaacaKuRftYXUXZJr8GBHPdmacFbrNnFDMk7DHNI,8187
21
- backend/main.py,sha256=nmm-0GWYoqKaWXRQSrRxbyZ5kUnv2BGpAtTvCyhVV_Q,4675
21
+ backend/main.py,sha256=xYxdM9GT0vLVPmyXGgoa0_BW1JQMGEX5m7lXm_xYnaw,5666
22
22
  backend/middleware/__init__.py,sha256=lUSMQqQc4UxPazrE1XmyNNEvTKRk7tddAdFmxbTzz7M,157
23
23
  backend/middleware/audit_logging.py,sha256=oGdgbfH_M_3hIMAGrS5HpRvri2jjLxGFnFuA6IiU9BU,4170
24
24
  backend/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -41,7 +41,7 @@ backend/services/discord_service.py,sha256=LnEasOgaSRlmebDKsVT2yKCdWNDmskUToP3o2
41
41
  backend/services/dropbox_service.py,sha256=R47MUKCqK2BEXXmQxMLG6zkPWw02htFoES9q9xiaYAw,10717
42
42
  backend/services/email_service.py,sha256=fxwcL-n6ZRGLQ7SHXhxUQK1MkprtgE3GcuL5ulUR-90,44699
43
43
  backend/services/encoding_interface.py,sha256=Y8hPskDyX9DOyAD557F3-SdHjymaBkGXcoC9E_OW7SE,15825
44
- backend/services/encoding_service.py,sha256=d_1M9DucihOHNqXI1pR4QLj16FhmtMoONRu2YGPvhGY,14570
44
+ backend/services/encoding_service.py,sha256=XAX4hyL2wIHdpBCTc8yglcjVJboZxRVAjc07lUTxgFg,18025
45
45
  backend/services/firestore_service.py,sha256=XOuXUNd91eVeyEx82uh-A-WMwcyUa2HVxqTfYnGPw-Q,18512
46
46
  backend/services/flacfetch_client.py,sha256=zj4VToKamR8bShXZifLvjDmUVa1HHvDhZuaw_b7g4lU,19901
47
47
  backend/services/gce_encoding/README.md,sha256=frTFrp7-ugFAvUd_K9EgE4ud8Xu4Qf1V7BNirQn6qJk,2515
@@ -50,16 +50,19 @@ backend/services/gce_encoding/main.py,sha256=2A0Bd3zYzbV66ikShB3avpqfNoDFBxr8Ja6
50
50
  backend/services/gce_encoding/requirements.txt,sha256=-hW5ot-PnVoKkxrpY3yV6l68wx7AvzgnLYAWEgDGWNc,408
51
51
  backend/services/gdrive_service.py,sha256=3JTXmPBuxpKopAbWDrh6YhqR5L9BLop1R7caXBnIk2A,13266
52
52
  backend/services/job_logging.py,sha256=theCkAlwMbqyqAbu7vCCRpVRscaIpkam3UrCs_4Yi1g,8052
53
- backend/services/job_manager.py,sha256=YPXFJD9oIMSZvVl2Ed4czo-4M-HqMxJrYCfZXV_MkhE,31256
53
+ backend/services/job_manager.py,sha256=y7TCwJal7rPJm4QRuOEviv_SE9RvDiNGHz33MSgGY3M,31701
54
54
  backend/services/job_notification_service.py,sha256=ywGJUMUDqV23OSDsugOUufpXO6N_24bM9YVaPp8bOk4,9557
55
+ backend/services/langfuse_preloader.py,sha256=t-Rt_ThjZm4K8W3jlyLn2H3cEy7KNUlhrvOPmdg-gYM,3100
55
56
  backend/services/local_encoding_service.py,sha256=Q6yK2-QL9tHM_pUBoxdyfT7ln2L5xMx82JB2nC5OfNQ,20680
56
57
  backend/services/local_preview_encoding_service.py,sha256=WLuJuIx-kH6u35gcYnvogf4Gt7iEokL2IdfxlrkY5po,14394
57
58
  backend/services/lyrics_cache_service.py,sha256=53EIZuDs5jIKr6QiOhIKJjE2mx1Jd1_x15_uAeLnc4A,7598
58
59
  backend/services/metrics.py,sha256=hlEeLG4jOR1i_QbWaNQthoko9CSpe-Zn_LHJ2huEFeY,12728
60
+ backend/services/nltk_preloader.py,sha256=TZFghQrpDTi6COcW_rkjL15SlzdRNvPmrdy690fq0hw,3579
59
61
  backend/services/packaging_service.py,sha256=MUuBk0jtG3vGQhuOWbDv9u1uIMwbBC4NknLpJYFcWhs,10259
60
62
  backend/services/rclone_service.py,sha256=Q6wRAjlBiMMh9FBbbFRp4SHK1ljOQBed9zUJaJgAuhU,3646
63
+ backend/services/spacy_preloader.py,sha256=rpsMZB0CQWcoXjq8nyZCwJBkyjGheKaCFrfhxCOln5w,1836
61
64
  backend/services/storage_service.py,sha256=gA7Snz1pihzYpRKZjEZx3eDwyMUBezl3ccLuBdpTwE4,8573
62
- backend/services/stripe_service.py,sha256=gmw1mQr_TezXFHnjN77df6KOUTpEuuzYewNMmbozaGg,9379
65
+ backend/services/stripe_service.py,sha256=154TbTuysOiuUFKEmIG3RFZYKYEYhLJR2Weyb4xAhxs,13243
63
66
  backend/services/structured_logging.py,sha256=daGgZUdc1xIiOdxL9-YtWHDy51hwZqd8ELpomkh2QH4,9063
64
67
  backend/services/template_service.py,sha256=5CdSxBGhdBtxIUz24AhvltUfSWcFqZzBIxb9MjlN4YU,11212
65
68
  backend/services/theme_service.py,sha256=6lJqMpFjmRY09CMJcW9IJ6DVPffLmwE0DnpXDUKaJ0U,17374
@@ -71,7 +74,7 @@ backend/services/youtube_upload_service.py,sha256=fvo4WxLQaKY_I75ViBMLk6vm87ym2A
71
74
  backend/tests/__init__.py,sha256=jTjELMqIzpgHG9dcXwwkszTAAC4Wf-c93C6D1k-toZg,44
72
75
  backend/tests/conftest.py,sha256=c4_jH0FIHsiKeUfo3ECPN78MlbW_IARTWwKr9RKLOSM,7843
73
76
  backend/tests/emulator/__init__.py,sha256=yOGPU5e0JPNYyUpy-u13jW4gEfWQs3dCJxfincptaCY,156
74
- backend/tests/emulator/conftest.py,sha256=gBZe23yPM4N2enFVO81ow67GcfGou8HZi7qWK5SI8GU,2858
77
+ backend/tests/emulator/conftest.py,sha256=VXhoJ3NZg3uiLzSAsushQtozFS7Lt6lmjk7ym-k6V8o,3844
75
78
  backend/tests/emulator/test_e2e_cli_backend.py,sha256=BTrWDv4sRTeOlheZ-d4R37tZsvJ2MhdFLbDb9tjbF08,41018
76
79
  backend/tests/emulator/test_emulator_integration.py,sha256=pm_JzgA2bmjcnHmGA-mZfJhUAYwGx10jv48A2deujRs,11822
77
80
  backend/tests/emulator/test_style_loading_direct.py,sha256=e-FzlCVW_-ealM9hTGa2qgpwoVRiYKrcXDWB1FjkwXc,16842
@@ -102,10 +105,10 @@ backend/tests/test_gdrive_service.py,sha256=fh02KPaFqLEWfPaDQvSehn6r6RLq0bgt6MdX
102
105
  backend/tests/test_instrumental_api.py,sha256=2eWaoEAYVY02HZgY4esn5echuWGv_sznoO6FYWIgTbQ,17210
103
106
  backend/tests/test_internal_api.py,sha256=qMGSDCkYd5OY_Z85vQu10xjqJn1JtaQcaKSnSBpVijw,12980
104
107
  backend/tests/test_job_creation_regression.py,sha256=C1KgyNhpJzchl10QK8trfmACs2BrEwYd-WK1YMMS-7I,22602
105
- backend/tests/test_job_manager.py,sha256=Y1MqanjqARh7HUSntzu2LkG3TFt0UYdIQHZisY7DP2A,12452
108
+ backend/tests/test_job_manager.py,sha256=Y5De6L0BvK4yk6D4w-pDBKBk2R7w89SfSyUz5krgphE,13187
106
109
  backend/tests/test_job_manager_notifications.py,sha256=WlzXXxTCmHHCrD4XWu2j2fDYODlHGKThOQpZQeyhPXM,12996
107
110
  backend/tests/test_job_notification_service.py,sha256=YbXqCWbsK8O5WICItb3VCrEY8qKqoJ3NBEZtwCs7AYM,17892
108
- backend/tests/test_jobs_api.py,sha256=HOdP-ULWQFgrL7JDRaaE14HF9xiuHHRt4-5UrgBQGoI,10713
111
+ backend/tests/test_jobs_api.py,sha256=z3pGTawTpt75a1bQ5x4ynw3pTlNihLA6vqktTc6mBAU,11092
109
112
  backend/tests/test_local_encoding_service.py,sha256=TbBaFOB-8Re09G-qXIQCl8mS9WlQMFNQF01V64IekyI,14638
110
113
  backend/tests/test_local_preview_encoding_service.py,sha256=VwxpcAjDdYaVauSsrJ609U575VWRZbLsXew8BqBXpJY,20228
111
114
  backend/tests/test_main.py,sha256=BvzcadLUV0SEUO1ViKm0djgy_TEKXm8PPvgfcCCEJAY,3348
@@ -116,6 +119,7 @@ backend/tests/test_routes_jobs.py,sha256=iqOZ93ACKpizSzLEJUx5hIo0vkKe2oIT7s7iZqQ
116
119
  backend/tests/test_routes_review.py,sha256=ENo6_wFUDcyXZhRy_NBqajan9dvqV_7f1rIQ7LLLS-U,14334
117
120
  backend/tests/test_services.py,sha256=JSsZZSpa0HjNZBSIrXXzBxZfJewfdR9RC3P8tHV8M8E,24288
118
121
  backend/tests/test_services_extended.py,sha256=cBcaEicz4WdBo5WZB77AoGZTwWAOC6W4OslpT6VmFI8,4124
122
+ backend/tests/test_spacy_preloader.py,sha256=uWpkrOPUOOcW1-fWClKMKVhXHGk30FXq537kQH5jpd0,4437
119
123
  backend/tests/test_storage_service.py,sha256=gorn3aFwKSr8lihxcG7kza7fYvDsTvxJhOxzuTtPK_o,19069
120
124
  backend/tests/test_style_upload.py,sha256=8G2dGgvyMGXsh1jk8PcGpTlSdBQAlHn13kpZcKn6rTY,10491
121
125
  backend/tests/test_template_service.py,sha256=BKURQ1M-hXXgdBl9LqhPQKfpItWOYDRBSt1y5FXQzaM,10431
@@ -130,6 +134,7 @@ backend/tests/test_workers.py,sha256=BW-MDQx5HrgICKOcd1KkvzyBO4sOfJXeLVlMiJDfmBw
130
134
  backend/tests/test_workers_extended.py,sha256=uFznO4Q2v_H7xjfje5eYXXjS72EnhwXNRcT9YshuB8M,6763
131
135
  backend/tests/test_youtube_service.py,sha256=HPm3HUJcu9rKcyRpBfufU6tzMP1lwZ9ZSe0vOoKNypI,9134
132
136
  backend/tests/test_youtube_upload_service.py,sha256=o58DXYWLOtp8nmaowO_UHdOErVCDl8YxHJUgI-xvyMs,21428
137
+ backend/utils/test_data.py,sha256=0H0GmV0UnLXe-l0NKAug7COORyQI3PQtbC_qQHllIpI,713
133
138
  backend/validate.py,sha256=u8lXKnC1ocXKAaYxNoCE9dRrhvoYI11XrTtaWIp-HLw,5040
134
139
  backend/version.py,sha256=Ai-O0n-W-iBIYY_c71-QZikxAsmTpcel4X3T3p8AqVI,677
135
140
  backend/workers/README.md,sha256=JYfLyngGkzxl1mRpkppIZB3ou9GR3sRVmZAxIAdURPY,15935
@@ -137,7 +142,7 @@ backend/workers/__init__.py,sha256=Eswp1Jvb6-97r7zUe7FUXSexsCZvghR08qwx1L_4298,3
137
142
  backend/workers/audio_worker.py,sha256=9RWIzuhU24fpRMj9JW8cpWODeYsZrbaXhtxddrMFKIo,29088
138
143
  backend/workers/lyrics_worker.py,sha256=nsy90pb0BPAl_lbZ0E7d7jIclcXK7W8d9RxxEdJaHVg,35149
139
144
  backend/workers/render_video_worker.py,sha256=L6-VCxSBY6MbXBBm0-wu_DXxcIWKfW6ynTv9K03WCjY,25163
140
- backend/workers/screens_worker.py,sha256=HvSrgpY4X0imu81HygAG-qS024puBWUONx-0fJ4sOe8,20998
145
+ backend/workers/screens_worker.py,sha256=vCS7j4dQRQ_tsWNhz-XH5nau7kGyUSzSp-dzyZpeJEo,21402
141
146
  backend/workers/style_helper.py,sha256=7-PM79dAUAZHTj-bnATy43ImqNdl_00E9pm94CYg1BM,6906
142
147
  backend/workers/video_worker.py,sha256=49Q7D3bNg38iJk26q2k8y3v-oUscUtE50qaUymlFQbk,55292
143
148
  backend/workers/video_worker_orchestrator.py,sha256=NInZjFwztqcYneuxrkDqEFWV7z5BzGlaaoJxj812KA4,27510
@@ -193,7 +198,7 @@ lyrics_transcriber/core/controller.py,sha256=zRjdxOrJEaa2depvzZvwVQiEFmf8Ew3Aek8
193
198
  lyrics_transcriber/correction/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
194
199
  lyrics_transcriber/correction/agentic/__init__.py,sha256=p7PHiebuvRs8RDlPDs-9gLZKzXG5KfWg3fFCdDhY6pE,222
195
200
  lyrics_transcriber/correction/agentic/adapter.py,sha256=Z0JBTAA7xlSdctCHqO9nBMl78C4XmqsLKKtS6BvNZNI,2912
196
- lyrics_transcriber/correction/agentic/agent.py,sha256=GV6TkrIQBhibJllXDnp9zBBmaf_vnoSVuJZmm6WVhS0,12722
201
+ lyrics_transcriber/correction/agentic/agent.py,sha256=73E2ouAiE0RUiIvxIb_lVaiIo_DJr_YaH3CAaqdjh08,13274
197
202
  lyrics_transcriber/correction/agentic/feedback/aggregator.py,sha256=323t8LDbE26ni83woyN7uVMSuSQhnqTgwJc-d-KuDbs,273
198
203
  lyrics_transcriber/correction/agentic/feedback/collector.py,sha256=HT-2cAP_bx7Iv-0-tpZv534do111g0FlTUt2XaKoUtA,415
199
204
  lyrics_transcriber/correction/agentic/feedback/retention.py,sha256=dUCUsKPCzHVQxiLLBXcdfAZ5NqiG25go0Z6GFXeK0vY,881
@@ -231,8 +236,8 @@ lyrics_transcriber/correction/agentic/providers/circuit_breaker.py,sha256=D3Jg4Y
231
236
  lyrics_transcriber/correction/agentic/providers/config.py,sha256=7m4I3imOZyv-6eR8mxuK9vVdqPEDcGuVAkEgdGWu10w,4299
232
237
  lyrics_transcriber/correction/agentic/providers/constants.py,sha256=cXLzKTyFVt9q6wQd_gWcv3EZ5Sm27AOAz6NyPapcess,695
233
238
  lyrics_transcriber/correction/agentic/providers/health.py,sha256=F8pHY5BQYvylGRDGXUHplcAJooAyiqVLRhBl4kHC1H8,710
234
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py,sha256=KrvjM0jY-7xrP65mW5TfsXnqN2BHcUgaDzcmWv1Rh_U,10423
235
- lyrics_transcriber/correction/agentic/providers/model_factory.py,sha256=90EjVwoKTWo8jXTrroI7GXM9AU-_ACx9g_fHB4vnR2w,9919
239
+ lyrics_transcriber/correction/agentic/providers/langchain_bridge.py,sha256=fgWZjzWiClBZ1NA6E8pf0kp23j6p56Htry_h7vtJ3ZM,12607
240
+ lyrics_transcriber/correction/agentic/providers/model_factory.py,sha256=ZsPdjSwnN1hFZxPwJkH-2rtgv59f9BmNRJmsLYtK67Q,10805
236
241
  lyrics_transcriber/correction/agentic/providers/response_cache.py,sha256=Byr7fQJsgUMFlsvHeVCxTiFjjnbsg3KIlEmEEtAo-Gw,7047
237
242
  lyrics_transcriber/correction/agentic/providers/response_parser.py,sha256=c2KypM-yHbIXXakHV5s-qh8fl8FhssLPVo3pJbyAiG4,4301
238
243
  lyrics_transcriber/correction/agentic/providers/retry_executor.py,sha256=hX21Zwy2cSECAw7k13ndEinWRqwjo4xYoSCQ2B2CUf0,3912
@@ -241,7 +246,7 @@ lyrics_transcriber/correction/agentic/workflows/__init__.py,sha256=OsBExAbIIKxJg
241
246
  lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py,sha256=gMuLTUxkgYaciMsI4yrZSC3wi--7V_PgaDNE-Vd6FE8,575
242
247
  lyrics_transcriber/correction/agentic/workflows/correction_graph.py,sha256=kgZKnz0h9cG1EfhW7BSSl-kSpQtJrRM_S86kAniXfE4,1815
243
248
  lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py,sha256=KsKLD3AP66YYmXfUn-mVZjERYLtU1Zs4a-7CB2zDfas,596
244
- lyrics_transcriber/correction/anchor_sequence.py,sha256=5tl4Cjiw5UlLbEb1Oy-g3ebKCinXSwohdaCB9-rTMtI,43798
249
+ lyrics_transcriber/correction/anchor_sequence.py,sha256=-FT4NoNjwGnoxgm4m117Z3DPlJAJAlFYcTe4BeUj9pQ,48974
245
250
  lyrics_transcriber/correction/corrector.py,sha256=zqmpwt_LMG1VdijXUIMGr42ny2qIiBqwEIDCslqi2dE,42622
246
251
  lyrics_transcriber/correction/feedback/__init__.py,sha256=i1gd0Vb4qvlzZQ3lqA3fJjt288YP7f-MBPwOzZ7Rjh4,68
247
252
  lyrics_transcriber/correction/feedback/schemas.py,sha256=OiF_WUqcqiEKIoburYM8kWAIundy82PQE7ImsdP8UCk,4416
@@ -254,11 +259,11 @@ lyrics_transcriber/correction/handlers/no_space_punct_match.py,sha256=jY2fa547Qc
254
259
  lyrics_transcriber/correction/handlers/relaxed_word_count_match.py,sha256=x4k__6gav4-STk_TycLcg5Sw4x2vUFAj5fWmOv7Yd_w,3911
255
260
  lyrics_transcriber/correction/handlers/repeat.py,sha256=1PJADW44egYh7N9D2fN-gDIusWVglFjGHrCZuTQYNpA,4313
256
261
  lyrics_transcriber/correction/handlers/sound_alike.py,sha256=75IvDSfoGUG2xVbYp-xsYuQXf7Jo-0ymsTzdBSOrwwQ,11935
257
- lyrics_transcriber/correction/handlers/syllables_match.py,sha256=c9_hrJb_xkkqd2SuDjrsSmUF7OMYV65LRzBfAhCHxEY,11217
262
+ lyrics_transcriber/correction/handlers/syllables_match.py,sha256=CV_sYISqPhA2VFbaU2XuX4SbjQcpeuMH7LTw91pgH4I,13103
258
263
  lyrics_transcriber/correction/handlers/word_count_match.py,sha256=OltTEs6eYnslxdvak97M5gXDiqXJxMHKk__Q9F_akXc,3595
259
264
  lyrics_transcriber/correction/handlers/word_operations.py,sha256=410xhyO9tiqezV5yd5JKwKbxSGwXK9LWHJ7-zNIuOWA,7423
260
265
  lyrics_transcriber/correction/operations.py,sha256=rmSxDdlu5H2drbVvR1A9KuaZT60vbHeZKKaB7olD4ns,14659
261
- lyrics_transcriber/correction/phrase_analyzer.py,sha256=dtO_2LjxnPdHJM7De40mYIdHCkozwhizVVQp5XGO7x0,16962
266
+ lyrics_transcriber/correction/phrase_analyzer.py,sha256=bEGz3KlH8iFAudMK7L0eC-CyVSwZkIZtXr8qh_k4_u8,17576
262
267
  lyrics_transcriber/correction/text_utils.py,sha256=7QHK6-PY7Rx1G1E31sWiLBw00mHorRDo-M44KMHFaZs,833
263
268
  lyrics_transcriber/frontend/.gitignore,sha256=cR2ofyyWArkna_jByfaWi8gTeMhsKTSoK128PmIw218,262
264
269
  lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs,sha256=KTYy2KCV2OpHhussV5jIPDdUSr7RftMRhqPsRUmgfAY,2765465
@@ -431,8 +436,8 @@ lyrics_transcriber/transcribers/whisper.py,sha256=YcCB1ic9H6zL1GS0jD0emu8-qlcH0Q
431
436
  lyrics_transcriber/types.py,sha256=UJjaxhVd2o14AG4G8ToU598p0JeYdiTFjpG38jGCoYQ,27917
432
437
  lyrics_transcriber/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
433
438
  lyrics_transcriber/utils/word_utils.py,sha256=-cMGpj9UV4F6IsoDKAV2i1aiqSO8eI91HMAm_igtVMk,958
434
- karaoke_gen-0.96.0.dist-info/METADATA,sha256=7yJr3KPMRKSUFYK-U05SfBDGHk9ZpYMGMDmeTPsAf1Q,23115
435
- karaoke_gen-0.96.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
436
- karaoke_gen-0.96.0.dist-info/entry_points.txt,sha256=xIyLe7K84ZyjO8L0_AmNectz93QjGSs5AkApMtlAd4g,160
437
- karaoke_gen-0.96.0.dist-info/licenses/LICENSE,sha256=81R_4XwMZDODHD7JcZeUR8IiCU8AD7Ajl6bmwR9tYDk,1074
438
- karaoke_gen-0.96.0.dist-info/RECORD,,
439
+ karaoke_gen-0.99.3.dist-info/METADATA,sha256=unsN1sCERd0RAJ-G2KFKw1UI_1sUpM-ZG6Sl3FODVJ8,23115
440
+ karaoke_gen-0.99.3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
441
+ karaoke_gen-0.99.3.dist-info/entry_points.txt,sha256=xIyLe7K84ZyjO8L0_AmNectz93QjGSs5AkApMtlAd4g,160
442
+ karaoke_gen-0.99.3.dist-info/licenses/LICENSE,sha256=81R_4XwMZDODHD7JcZeUR8IiCU8AD7Ajl6bmwR9tYDk,1074
443
+ karaoke_gen-0.99.3.dist-info/RECORD,,
@@ -86,28 +86,39 @@ class AgenticCorrector:
86
86
 
87
87
  @classmethod
88
88
  def from_model(
89
- cls,
90
- model: str,
89
+ cls,
90
+ model: str,
91
91
  config: ProviderConfig | None = None,
92
92
  session_id: Optional[str] = None,
93
- cache_dir: Optional[str] = None
93
+ cache_dir: Optional[str] = None,
94
+ warmup: bool = True
94
95
  ) -> "AgenticCorrector":
95
96
  """Factory method to create corrector from model specification.
96
-
97
+
97
98
  This is a convenience method for the common case where you want
98
99
  to use LangChainBridge with a model spec string.
99
-
100
+
100
101
  Args:
101
102
  model: Model identifier in format "provider/model"
102
103
  config: Optional provider configuration
103
104
  session_id: Optional Langfuse session ID to group related traces
104
105
  cache_dir: Optional cache directory (uses default if not provided)
105
-
106
+ warmup: If True, eagerly initialize the model to avoid delays when
107
+ multiple threads call classify_gap() simultaneously (default: True)
108
+
106
109
  Returns:
107
110
  AgenticCorrector instance with LangChainBridge provider
108
111
  """
109
112
  config = config or ProviderConfig.from_env(cache_dir=cache_dir)
110
113
  provider = LangChainBridge(model=model, config=config)
114
+
115
+ # Eagerly initialize the model to avoid lazy initialization delays
116
+ # when multiple threads call classify_gap() simultaneously
117
+ if warmup:
118
+ logger.info(f"🤖 Warming up model {model} before parallel processing...")
119
+ if not provider.warmup():
120
+ logger.warning(f"🤖 Model warmup failed for {model}, will retry on first use")
121
+
111
122
  return cls(provider=provider, session_id=session_id)
112
123
 
113
124
  def classify_gap(