karaoke-gen 0.99.3__py3-none-any.whl → 0.101.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.
- backend/api/routes/admin.py +512 -1
- backend/api/routes/audio_search.py +13 -2
- backend/api/routes/file_upload.py +42 -1
- backend/api/routes/internal.py +6 -0
- backend/api/routes/jobs.py +9 -1
- backend/api/routes/review.py +13 -6
- backend/api/routes/tenant.py +120 -0
- backend/api/routes/users.py +167 -245
- backend/main.py +6 -1
- backend/middleware/__init__.py +7 -1
- backend/middleware/tenant.py +192 -0
- backend/models/job.py +19 -3
- backend/models/tenant.py +208 -0
- backend/models/user.py +18 -0
- backend/services/email_service.py +253 -6
- backend/services/firestore_service.py +6 -0
- backend/services/job_manager.py +32 -1
- backend/services/stripe_service.py +61 -35
- backend/services/tenant_service.py +285 -0
- backend/services/user_service.py +85 -7
- backend/tests/emulator/test_made_for_you_integration.py +167 -0
- backend/tests/test_admin_job_files.py +337 -0
- backend/tests/test_admin_job_reset.py +384 -0
- backend/tests/test_admin_job_update.py +326 -0
- backend/tests/test_email_service.py +233 -0
- backend/tests/test_impersonation.py +223 -0
- backend/tests/test_job_creation_regression.py +4 -0
- backend/tests/test_job_manager.py +146 -1
- backend/tests/test_made_for_you.py +2086 -0
- backend/tests/test_models.py +139 -0
- backend/tests/test_tenant_api.py +350 -0
- backend/tests/test_tenant_middleware.py +345 -0
- backend/tests/test_tenant_models.py +406 -0
- backend/tests/test_tenant_service.py +418 -0
- backend/workers/video_worker.py +8 -3
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/METADATA +1 -1
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/RECORD +42 -28
- lyrics_transcriber/frontend/src/api.ts +13 -5
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +90 -57
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for TenantService.
|
|
3
|
+
|
|
4
|
+
Tests config loading from GCS, caching behavior, subdomain resolution,
|
|
5
|
+
and email domain validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from unittest.mock import MagicMock, patch
|
|
11
|
+
|
|
12
|
+
from backend.services.tenant_service import TenantService, get_tenant_service
|
|
13
|
+
from backend.models.tenant import TenantConfig, TenantPublicConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Sample tenant config data that would come from GCS
|
|
17
|
+
SAMPLE_VOCALSTAR_CONFIG = {
|
|
18
|
+
"id": "vocalstar",
|
|
19
|
+
"name": "Vocal Star",
|
|
20
|
+
"subdomain": "vocalstar.nomadkaraoke.com",
|
|
21
|
+
"is_active": True,
|
|
22
|
+
"branding": {
|
|
23
|
+
"logo_url": "https://example.com/logo.png",
|
|
24
|
+
"logo_height": 50,
|
|
25
|
+
"primary_color": "#ffff00",
|
|
26
|
+
"secondary_color": "#006CF9",
|
|
27
|
+
"site_title": "Vocal Star Karaoke Generator",
|
|
28
|
+
},
|
|
29
|
+
"features": {
|
|
30
|
+
"audio_search": False,
|
|
31
|
+
"file_upload": True,
|
|
32
|
+
"youtube_url": False,
|
|
33
|
+
"youtube_upload": False,
|
|
34
|
+
"dropbox_upload": False,
|
|
35
|
+
"gdrive_upload": False,
|
|
36
|
+
"theme_selection": False,
|
|
37
|
+
},
|
|
38
|
+
"defaults": {
|
|
39
|
+
"theme_id": "vocalstar",
|
|
40
|
+
"locked_theme": "vocalstar",
|
|
41
|
+
"distribution_mode": "download_only",
|
|
42
|
+
},
|
|
43
|
+
"auth": {
|
|
44
|
+
"allowed_email_domains": ["vocal-star.com", "vocalstarmusic.com"],
|
|
45
|
+
"require_email_domain": True,
|
|
46
|
+
"sender_email": "vocalstar@nomadkaraoke.com",
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
SAMPLE_INACTIVE_CONFIG = {
|
|
51
|
+
"id": "inactive",
|
|
52
|
+
"name": "Inactive Tenant",
|
|
53
|
+
"subdomain": "inactive.nomadkaraoke.com",
|
|
54
|
+
"is_active": False,
|
|
55
|
+
"branding": {},
|
|
56
|
+
"features": {},
|
|
57
|
+
"defaults": {},
|
|
58
|
+
"auth": {},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestTenantService:
|
|
63
|
+
"""Tests for TenantService class."""
|
|
64
|
+
|
|
65
|
+
@pytest.fixture
|
|
66
|
+
def mock_storage(self):
|
|
67
|
+
"""Create a mock StorageService."""
|
|
68
|
+
storage = MagicMock()
|
|
69
|
+
storage.file_exists.return_value = False
|
|
70
|
+
storage.download_json.return_value = {}
|
|
71
|
+
storage.generate_signed_url.return_value = "https://signed-url.example.com"
|
|
72
|
+
return storage
|
|
73
|
+
|
|
74
|
+
@pytest.fixture
|
|
75
|
+
def tenant_service(self, mock_storage):
|
|
76
|
+
"""Create a TenantService with mock storage."""
|
|
77
|
+
return TenantService(storage=mock_storage)
|
|
78
|
+
|
|
79
|
+
# Tests for get_tenant_config()
|
|
80
|
+
def test_get_tenant_config_success(self, tenant_service, mock_storage):
|
|
81
|
+
"""Test successful config loading from GCS."""
|
|
82
|
+
mock_storage.file_exists.return_value = True
|
|
83
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
84
|
+
|
|
85
|
+
config = tenant_service.get_tenant_config("vocalstar")
|
|
86
|
+
|
|
87
|
+
assert config is not None
|
|
88
|
+
assert config.id == "vocalstar"
|
|
89
|
+
assert config.name == "Vocal Star"
|
|
90
|
+
assert config.features.audio_search is False
|
|
91
|
+
mock_storage.download_json.assert_called_once_with("tenants/vocalstar/config.json")
|
|
92
|
+
|
|
93
|
+
def test_get_tenant_config_not_found(self, tenant_service, mock_storage):
|
|
94
|
+
"""Test returns None when tenant config doesn't exist."""
|
|
95
|
+
mock_storage.file_exists.return_value = False
|
|
96
|
+
|
|
97
|
+
config = tenant_service.get_tenant_config("nonexistent")
|
|
98
|
+
|
|
99
|
+
assert config is None
|
|
100
|
+
mock_storage.download_json.assert_not_called()
|
|
101
|
+
|
|
102
|
+
def test_get_tenant_config_gcs_error(self, tenant_service, mock_storage):
|
|
103
|
+
"""Test handles GCS errors gracefully."""
|
|
104
|
+
mock_storage.file_exists.return_value = True
|
|
105
|
+
mock_storage.download_json.side_effect = Exception("GCS error")
|
|
106
|
+
|
|
107
|
+
config = tenant_service.get_tenant_config("vocalstar")
|
|
108
|
+
|
|
109
|
+
assert config is None
|
|
110
|
+
|
|
111
|
+
# Tests for caching behavior
|
|
112
|
+
def test_config_is_cached(self, tenant_service, mock_storage):
|
|
113
|
+
"""Test that configs are cached after first load."""
|
|
114
|
+
mock_storage.file_exists.return_value = True
|
|
115
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
116
|
+
|
|
117
|
+
# First call loads from GCS
|
|
118
|
+
config1 = tenant_service.get_tenant_config("vocalstar")
|
|
119
|
+
# Second call should use cache
|
|
120
|
+
config2 = tenant_service.get_tenant_config("vocalstar")
|
|
121
|
+
|
|
122
|
+
assert config1 is config2
|
|
123
|
+
# download_json should only be called once
|
|
124
|
+
assert mock_storage.download_json.call_count == 1
|
|
125
|
+
|
|
126
|
+
def test_force_refresh_bypasses_cache(self, tenant_service, mock_storage):
|
|
127
|
+
"""Test that force_refresh reloads from GCS."""
|
|
128
|
+
mock_storage.file_exists.return_value = True
|
|
129
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
130
|
+
|
|
131
|
+
# First call
|
|
132
|
+
tenant_service.get_tenant_config("vocalstar")
|
|
133
|
+
# Force refresh
|
|
134
|
+
tenant_service.get_tenant_config("vocalstar", force_refresh=True)
|
|
135
|
+
|
|
136
|
+
# download_json should be called twice
|
|
137
|
+
assert mock_storage.download_json.call_count == 2
|
|
138
|
+
|
|
139
|
+
def test_cache_expires_after_ttl(self, tenant_service, mock_storage):
|
|
140
|
+
"""Test that cache expires after TTL."""
|
|
141
|
+
mock_storage.file_exists.return_value = True
|
|
142
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
143
|
+
|
|
144
|
+
# Load config
|
|
145
|
+
tenant_service.get_tenant_config("vocalstar")
|
|
146
|
+
|
|
147
|
+
# Simulate cache expiration by manipulating cache time
|
|
148
|
+
tenant_service._cache_times["vocalstar"] = datetime.now() - timedelta(seconds=400)
|
|
149
|
+
|
|
150
|
+
# Next call should reload
|
|
151
|
+
tenant_service.get_tenant_config("vocalstar")
|
|
152
|
+
|
|
153
|
+
assert mock_storage.download_json.call_count == 2
|
|
154
|
+
|
|
155
|
+
# Tests for get_tenant_by_subdomain()
|
|
156
|
+
def test_get_tenant_by_subdomain_cached(self, tenant_service, mock_storage):
|
|
157
|
+
"""Test subdomain lookup uses cache."""
|
|
158
|
+
mock_storage.file_exists.return_value = True
|
|
159
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
160
|
+
|
|
161
|
+
# First load by ID to populate cache
|
|
162
|
+
tenant_service.get_tenant_config("vocalstar")
|
|
163
|
+
|
|
164
|
+
# Now lookup by subdomain should use cached mapping
|
|
165
|
+
config = tenant_service.get_tenant_by_subdomain("vocalstar.nomadkaraoke.com")
|
|
166
|
+
|
|
167
|
+
assert config is not None
|
|
168
|
+
assert config.id == "vocalstar"
|
|
169
|
+
|
|
170
|
+
def test_get_tenant_by_subdomain_fallback_resolution(self, tenant_service, mock_storage):
|
|
171
|
+
"""Test subdomain resolution when not in cache."""
|
|
172
|
+
mock_storage.file_exists.return_value = True
|
|
173
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
174
|
+
|
|
175
|
+
# Lookup by subdomain without prior cache
|
|
176
|
+
config = tenant_service.get_tenant_by_subdomain("vocalstar.nomadkaraoke.com")
|
|
177
|
+
|
|
178
|
+
assert config is not None
|
|
179
|
+
assert config.id == "vocalstar"
|
|
180
|
+
|
|
181
|
+
def test_get_tenant_by_subdomain_not_found(self, tenant_service, mock_storage):
|
|
182
|
+
"""Test returns None for unknown subdomain."""
|
|
183
|
+
mock_storage.file_exists.return_value = False
|
|
184
|
+
|
|
185
|
+
config = tenant_service.get_tenant_by_subdomain("unknown.nomadkaraoke.com")
|
|
186
|
+
|
|
187
|
+
assert config is None
|
|
188
|
+
|
|
189
|
+
def test_get_tenant_by_subdomain_case_insensitive(self, tenant_service, mock_storage):
|
|
190
|
+
"""Test subdomain lookup is case insensitive."""
|
|
191
|
+
mock_storage.file_exists.return_value = True
|
|
192
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
193
|
+
|
|
194
|
+
config = tenant_service.get_tenant_by_subdomain("VOCALSTAR.NomadKaraoke.COM")
|
|
195
|
+
|
|
196
|
+
assert config is not None
|
|
197
|
+
assert config.id == "vocalstar"
|
|
198
|
+
|
|
199
|
+
# Tests for _resolve_subdomain_to_tenant_id()
|
|
200
|
+
def test_resolve_subdomain_3_parts(self, tenant_service, mock_storage):
|
|
201
|
+
"""Test resolving tenant.nomadkaraoke.com pattern."""
|
|
202
|
+
mock_storage.file_exists.return_value = True
|
|
203
|
+
|
|
204
|
+
tenant_id = tenant_service._resolve_subdomain_to_tenant_id("vocalstar.nomadkaraoke.com")
|
|
205
|
+
|
|
206
|
+
assert tenant_id == "vocalstar"
|
|
207
|
+
|
|
208
|
+
def test_resolve_subdomain_4_parts(self, tenant_service, mock_storage):
|
|
209
|
+
"""Test resolving tenant.gen.nomadkaraoke.com pattern."""
|
|
210
|
+
mock_storage.file_exists.return_value = True
|
|
211
|
+
|
|
212
|
+
tenant_id = tenant_service._resolve_subdomain_to_tenant_id("vocalstar.gen.nomadkaraoke.com")
|
|
213
|
+
|
|
214
|
+
assert tenant_id == "vocalstar"
|
|
215
|
+
|
|
216
|
+
def test_resolve_subdomain_too_short(self, tenant_service, mock_storage):
|
|
217
|
+
"""Test returns None for subdomains with fewer than 3 parts."""
|
|
218
|
+
tenant_id = tenant_service._resolve_subdomain_to_tenant_id("nomadkaraoke.com")
|
|
219
|
+
|
|
220
|
+
assert tenant_id is None
|
|
221
|
+
|
|
222
|
+
def test_resolve_subdomain_tenant_not_exists(self, tenant_service, mock_storage):
|
|
223
|
+
"""Test returns None when tenant config doesn't exist in GCS."""
|
|
224
|
+
mock_storage.file_exists.return_value = False
|
|
225
|
+
|
|
226
|
+
tenant_id = tenant_service._resolve_subdomain_to_tenant_id("nonexistent.nomadkaraoke.com")
|
|
227
|
+
|
|
228
|
+
assert tenant_id is None
|
|
229
|
+
|
|
230
|
+
# Tests for get_public_config()
|
|
231
|
+
def test_get_public_config_success(self, tenant_service, mock_storage):
|
|
232
|
+
"""Test getting public config."""
|
|
233
|
+
mock_storage.file_exists.return_value = True
|
|
234
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
235
|
+
|
|
236
|
+
public_config = tenant_service.get_public_config("vocalstar")
|
|
237
|
+
|
|
238
|
+
assert public_config is not None
|
|
239
|
+
assert isinstance(public_config, TenantPublicConfig)
|
|
240
|
+
assert public_config.id == "vocalstar"
|
|
241
|
+
|
|
242
|
+
def test_get_public_config_not_found(self, tenant_service, mock_storage):
|
|
243
|
+
"""Test returns None when tenant not found."""
|
|
244
|
+
mock_storage.file_exists.return_value = False
|
|
245
|
+
|
|
246
|
+
public_config = tenant_service.get_public_config("nonexistent")
|
|
247
|
+
|
|
248
|
+
assert public_config is None
|
|
249
|
+
|
|
250
|
+
def test_get_public_config_by_subdomain(self, tenant_service, mock_storage):
|
|
251
|
+
"""Test getting public config by subdomain."""
|
|
252
|
+
mock_storage.file_exists.return_value = True
|
|
253
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
254
|
+
|
|
255
|
+
public_config = tenant_service.get_public_config_by_subdomain("vocalstar.nomadkaraoke.com")
|
|
256
|
+
|
|
257
|
+
assert public_config is not None
|
|
258
|
+
assert public_config.id == "vocalstar"
|
|
259
|
+
|
|
260
|
+
# Tests for tenant_exists()
|
|
261
|
+
def test_tenant_exists_true(self, tenant_service, mock_storage):
|
|
262
|
+
"""Test returns True when config exists."""
|
|
263
|
+
mock_storage.file_exists.return_value = True
|
|
264
|
+
|
|
265
|
+
assert tenant_service.tenant_exists("vocalstar") is True
|
|
266
|
+
mock_storage.file_exists.assert_called_with("tenants/vocalstar/config.json")
|
|
267
|
+
|
|
268
|
+
def test_tenant_exists_false(self, tenant_service, mock_storage):
|
|
269
|
+
"""Test returns False when config doesn't exist."""
|
|
270
|
+
mock_storage.file_exists.return_value = False
|
|
271
|
+
|
|
272
|
+
assert tenant_service.tenant_exists("nonexistent") is False
|
|
273
|
+
|
|
274
|
+
# Tests for is_email_allowed_for_tenant()
|
|
275
|
+
def test_is_email_allowed_for_tenant_valid(self, tenant_service, mock_storage):
|
|
276
|
+
"""Test email allowed for valid domain."""
|
|
277
|
+
mock_storage.file_exists.return_value = True
|
|
278
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
279
|
+
|
|
280
|
+
assert tenant_service.is_email_allowed_for_tenant("vocalstar", "user@vocal-star.com") is True
|
|
281
|
+
|
|
282
|
+
def test_is_email_allowed_for_tenant_invalid(self, tenant_service, mock_storage):
|
|
283
|
+
"""Test email not allowed for invalid domain."""
|
|
284
|
+
mock_storage.file_exists.return_value = True
|
|
285
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
286
|
+
|
|
287
|
+
assert tenant_service.is_email_allowed_for_tenant("vocalstar", "user@gmail.com") is False
|
|
288
|
+
|
|
289
|
+
def test_is_email_allowed_for_tenant_not_found(self, tenant_service, mock_storage):
|
|
290
|
+
"""Test returns False when tenant not found."""
|
|
291
|
+
mock_storage.file_exists.return_value = False
|
|
292
|
+
|
|
293
|
+
assert tenant_service.is_email_allowed_for_tenant("nonexistent", "user@any.com") is False
|
|
294
|
+
|
|
295
|
+
# Tests for get_tenant_sender_email()
|
|
296
|
+
def test_get_tenant_sender_email_custom(self, tenant_service, mock_storage):
|
|
297
|
+
"""Test returns custom sender email when configured."""
|
|
298
|
+
mock_storage.file_exists.return_value = True
|
|
299
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
300
|
+
|
|
301
|
+
sender = tenant_service.get_tenant_sender_email("vocalstar")
|
|
302
|
+
|
|
303
|
+
assert sender == "vocalstar@nomadkaraoke.com"
|
|
304
|
+
|
|
305
|
+
def test_get_tenant_sender_email_default(self, tenant_service, mock_storage):
|
|
306
|
+
"""Test returns default sender when tenant not found."""
|
|
307
|
+
mock_storage.file_exists.return_value = False
|
|
308
|
+
|
|
309
|
+
sender = tenant_service.get_tenant_sender_email("nonexistent")
|
|
310
|
+
|
|
311
|
+
# Should match DEFAULT_SENDER_EMAIL constant (consistent with EmailService)
|
|
312
|
+
assert sender == "gen@nomadkaraoke.com"
|
|
313
|
+
|
|
314
|
+
# Tests for invalidate_cache()
|
|
315
|
+
def test_invalidate_cache_specific_tenant(self, tenant_service, mock_storage):
|
|
316
|
+
"""Test invalidating cache for specific tenant."""
|
|
317
|
+
mock_storage.file_exists.return_value = True
|
|
318
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
319
|
+
|
|
320
|
+
# Load into cache
|
|
321
|
+
tenant_service.get_tenant_config("vocalstar")
|
|
322
|
+
assert "vocalstar" in tenant_service._config_cache
|
|
323
|
+
|
|
324
|
+
# Invalidate
|
|
325
|
+
tenant_service.invalidate_cache("vocalstar")
|
|
326
|
+
|
|
327
|
+
assert "vocalstar" not in tenant_service._config_cache
|
|
328
|
+
assert "vocalstar" not in tenant_service._cache_times
|
|
329
|
+
|
|
330
|
+
def test_invalidate_cache_specific_tenant_clears_subdomain_map(self, tenant_service, mock_storage):
|
|
331
|
+
"""Test invalidating specific tenant also clears its subdomain map entry."""
|
|
332
|
+
mock_storage.file_exists.return_value = True
|
|
333
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
334
|
+
|
|
335
|
+
# Load into cache (this also populates subdomain map)
|
|
336
|
+
tenant_service.get_tenant_config("vocalstar")
|
|
337
|
+
assert "vocalstar.nomadkaraoke.com" in tenant_service._subdomain_map
|
|
338
|
+
assert tenant_service._subdomain_map["vocalstar.nomadkaraoke.com"] == "vocalstar"
|
|
339
|
+
|
|
340
|
+
# Invalidate specific tenant
|
|
341
|
+
tenant_service.invalidate_cache("vocalstar")
|
|
342
|
+
|
|
343
|
+
# Subdomain map entry should also be removed
|
|
344
|
+
assert "vocalstar.nomadkaraoke.com" not in tenant_service._subdomain_map
|
|
345
|
+
|
|
346
|
+
def test_invalidate_cache_all(self, tenant_service, mock_storage):
|
|
347
|
+
"""Test invalidating all caches."""
|
|
348
|
+
mock_storage.file_exists.return_value = True
|
|
349
|
+
mock_storage.download_json.return_value = SAMPLE_VOCALSTAR_CONFIG
|
|
350
|
+
|
|
351
|
+
# Load into cache
|
|
352
|
+
tenant_service.get_tenant_config("vocalstar")
|
|
353
|
+
|
|
354
|
+
# Invalidate all
|
|
355
|
+
tenant_service.invalidate_cache()
|
|
356
|
+
|
|
357
|
+
assert len(tenant_service._config_cache) == 0
|
|
358
|
+
assert len(tenant_service._cache_times) == 0
|
|
359
|
+
assert len(tenant_service._subdomain_map) == 0
|
|
360
|
+
|
|
361
|
+
# Tests for get_asset_url()
|
|
362
|
+
def test_get_asset_url_success(self, tenant_service, mock_storage):
|
|
363
|
+
"""Test getting signed URL for existing asset."""
|
|
364
|
+
mock_storage.file_exists.return_value = True
|
|
365
|
+
|
|
366
|
+
url = tenant_service.get_asset_url("vocalstar", "logo.png")
|
|
367
|
+
|
|
368
|
+
assert url == "https://signed-url.example.com"
|
|
369
|
+
mock_storage.file_exists.assert_called_with("tenants/vocalstar/logo.png")
|
|
370
|
+
mock_storage.generate_signed_url.assert_called_with("tenants/vocalstar/logo.png", expiration_minutes=60)
|
|
371
|
+
|
|
372
|
+
def test_get_asset_url_not_found(self, tenant_service, mock_storage):
|
|
373
|
+
"""Test returns None when asset doesn't exist."""
|
|
374
|
+
mock_storage.file_exists.return_value = False
|
|
375
|
+
|
|
376
|
+
url = tenant_service.get_asset_url("vocalstar", "missing.png")
|
|
377
|
+
|
|
378
|
+
assert url is None
|
|
379
|
+
mock_storage.generate_signed_url.assert_not_called()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class TestGetTenantServiceSingleton:
|
|
383
|
+
"""Tests for the get_tenant_service() singleton function."""
|
|
384
|
+
|
|
385
|
+
def test_returns_same_instance(self):
|
|
386
|
+
"""Test that get_tenant_service returns singleton."""
|
|
387
|
+
# Reset the singleton
|
|
388
|
+
import backend.services.tenant_service as module
|
|
389
|
+
module._tenant_service = None
|
|
390
|
+
|
|
391
|
+
with patch.object(TenantService, '__init__', return_value=None):
|
|
392
|
+
service1 = get_tenant_service()
|
|
393
|
+
service2 = get_tenant_service()
|
|
394
|
+
|
|
395
|
+
assert service1 is service2
|
|
396
|
+
|
|
397
|
+
def test_thread_safe_initialization(self):
|
|
398
|
+
"""Test that singleton initialization is thread-safe."""
|
|
399
|
+
import threading
|
|
400
|
+
import backend.services.tenant_service as module
|
|
401
|
+
|
|
402
|
+
# Reset the singleton
|
|
403
|
+
module._tenant_service = None
|
|
404
|
+
|
|
405
|
+
instances = []
|
|
406
|
+
|
|
407
|
+
def get_instance():
|
|
408
|
+
with patch.object(TenantService, '__init__', return_value=None):
|
|
409
|
+
instances.append(get_tenant_service())
|
|
410
|
+
|
|
411
|
+
threads = [threading.Thread(target=get_instance) for _ in range(10)]
|
|
412
|
+
for t in threads:
|
|
413
|
+
t.start()
|
|
414
|
+
for t in threads:
|
|
415
|
+
t.join()
|
|
416
|
+
|
|
417
|
+
# All instances should be the same
|
|
418
|
+
assert all(i is instances[0] for i in instances)
|
backend/workers/video_worker.py
CHANGED
|
@@ -831,9 +831,12 @@ async def _handle_native_distribution(
|
|
|
831
831
|
# Don't fail the job - distribution is optional
|
|
832
832
|
|
|
833
833
|
# Upload to Google Drive using native API
|
|
834
|
+
# Skip if orchestrator already uploaded (gdrive_files already populated)
|
|
835
|
+
# This prevents duplicate uploads when using the orchestrator path
|
|
834
836
|
gdrive_folder_id = getattr(job, 'gdrive_folder_id', None)
|
|
835
|
-
|
|
836
|
-
|
|
837
|
+
existing_gdrive_files = result.get('gdrive_files')
|
|
838
|
+
|
|
839
|
+
if gdrive_folder_id and not existing_gdrive_files:
|
|
837
840
|
try:
|
|
838
841
|
from backend.services.gdrive_service import get_gdrive_service
|
|
839
842
|
|
|
@@ -869,7 +872,9 @@ async def _handle_native_distribution(
|
|
|
869
872
|
except Exception as e:
|
|
870
873
|
job_log.error(f"Native Google Drive upload failed: {e}", exc_info=True)
|
|
871
874
|
# Don't fail the job - distribution is optional
|
|
872
|
-
|
|
875
|
+
elif existing_gdrive_files:
|
|
876
|
+
job_log.info(f"Skipping Google Drive upload - orchestrator already uploaded {len(existing_gdrive_files)} files")
|
|
877
|
+
|
|
873
878
|
# Update job state_data with brand code and links
|
|
874
879
|
if brand_code or result.get('dropbox_link') or result.get('gdrive_files'):
|
|
875
880
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: karaoke-gen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.101.0
|
|
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,25 +7,28 @@ 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=
|
|
11
|
-
backend/api/routes/audio_search.py,sha256=
|
|
10
|
+
backend/api/routes/admin.py,sha256=GI4ANzshBUfAzMdRH3M8cUBnzsCbrL1owRXmIY5HK8A,46506
|
|
11
|
+
backend/api/routes/audio_search.py,sha256=CK2lVaM8RDP7i61XEtLeS0AJDfVJSy0ctnduK-ew-hQ,40129
|
|
12
12
|
backend/api/routes/auth.py,sha256=G1U2KwS3uqBJpgvg2PIe_mZOWCJKPFhhewrmKbW3Z_s,11531
|
|
13
|
-
backend/api/routes/file_upload.py,sha256=
|
|
13
|
+
backend/api/routes/file_upload.py,sha256=nEl06-ZqkKZNzyNZh0RRtuOBox54XvdEOFkYtnSqpv8,95828
|
|
14
14
|
backend/api/routes/health.py,sha256=iZlhJpmz4vminUy_qrzkyk_1tgG1gmITz4jEWaCWJnM,14010
|
|
15
|
-
backend/api/routes/internal.py,sha256=
|
|
16
|
-
backend/api/routes/jobs.py,sha256=
|
|
17
|
-
backend/api/routes/review.py,sha256=
|
|
15
|
+
backend/api/routes/internal.py,sha256=yqzd5V8xGJQ_vTZL85vKIMwmBXf1MIp1nhder-kQEuA,15683
|
|
16
|
+
backend/api/routes/jobs.py,sha256=7i3uAOL9p4cNk1vJHJIIYCBkcJFaJFl6o3iq92Z9KFo,61218
|
|
17
|
+
backend/api/routes/review.py,sha256=BiaXZs-NXl7AVzGiwBzIhzm60fUslcQagSw4XuUByhU,28842
|
|
18
|
+
backend/api/routes/tenant.py,sha256=sM0WVXWGKJeyBNMbPDnAKOD9-SkJBmH4RkMXVWz4dlM,4038
|
|
18
19
|
backend/api/routes/themes.py,sha256=_fPZg9N2KN-yyr1YR2vAy8FIm8PqVowboQ4WEpFTYiQ,4721
|
|
19
|
-
backend/api/routes/users.py,sha256=
|
|
20
|
+
backend/api/routes/users.py,sha256=nKJKuGedzSMuJVLMU4-CXNc7GsM5c82JzjFYeZtzgsg,52771
|
|
20
21
|
backend/config.py,sha256=-HsMaacaKuRftYXUXZJr8GBHPdmacFbrNnFDMk7DHNI,8187
|
|
21
|
-
backend/main.py,sha256=
|
|
22
|
-
backend/middleware/__init__.py,sha256=
|
|
22
|
+
backend/main.py,sha256=yRIdnkv_iIyOiLc2btqomxlflym13A-b3ei3X8gI8Ac,5944
|
|
23
|
+
backend/middleware/__init__.py,sha256=usnVRHqfGW56bWJFqaIXz49pSW91uyciBfwO7ANCpIk,369
|
|
23
24
|
backend/middleware/audit_logging.py,sha256=oGdgbfH_M_3hIMAGrS5HpRvri2jjLxGFnFuA6IiU9BU,4170
|
|
25
|
+
backend/middleware/tenant.py,sha256=a-HEAhyvMUC4uFF6VptaNOO9O6YP64-ZoUitppl79PQ,6373
|
|
24
26
|
backend/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
-
backend/models/job.py,sha256=
|
|
27
|
+
backend/models/job.py,sha256=B1Q12F6448okCOkmLmpBnlwGIKi16TqBjVkRttTmqi0,25663
|
|
26
28
|
backend/models/requests.py,sha256=tCX7ss4to3rKAR_Wu92CZ8uPqNb2nnmCp1XepD4VR5s,4152
|
|
29
|
+
backend/models/tenant.py,sha256=rhj6n7bsX8ClstoHQKCVcLWALI31l5Tepcf_N8IXDdM,7403
|
|
27
30
|
backend/models/theme.py,sha256=cfNSHIigoAV4P41Jku6Sa1okHSRhUHjFpl1XBVF3kCY,4628
|
|
28
|
-
backend/models/user.py,sha256=
|
|
31
|
+
backend/models/user.py,sha256=2Dl81Sa8_c0cuPJ3uddsLSOAN40mtv-5aUETtFZZK8c,8022
|
|
29
32
|
backend/models/worker_log.py,sha256=QDgpdRarm00N1TdFoGhKJxZjP9GmiMjrTJb6msw7pHA,5547
|
|
30
33
|
backend/pyproject.toml,sha256=LAvEXxQSRI94XQcwpBmuSoEu36341HOu53iigsN6dVs,577
|
|
31
34
|
backend/quick-check.sh,sha256=PUvH0wXaGzWrgWwwJG5M4Bxxt3vVA4mmdEdL6VepVHY,2382
|
|
@@ -39,10 +42,10 @@ backend/services/auth_service.py,sha256=rEpb4O9YSTOQaEoP2XXKXRpppWI5sG10AsueQSVc
|
|
|
39
42
|
backend/services/credential_manager.py,sha256=VA--PwvL3gboJ21orQb7MIUiovX8n66FohD6w84exX8,31682
|
|
40
43
|
backend/services/discord_service.py,sha256=LnEasOgaSRlmebDKsVT2yKCdWNDmskUToP3o2wPmdus,5082
|
|
41
44
|
backend/services/dropbox_service.py,sha256=R47MUKCqK2BEXXmQxMLG6zkPWw02htFoES9q9xiaYAw,10717
|
|
42
|
-
backend/services/email_service.py,sha256=
|
|
45
|
+
backend/services/email_service.py,sha256=gpufwXhNWmdElpWCmKrTLtm2LB8S-0Y5wcyJ3n1i6Jg,52868
|
|
43
46
|
backend/services/encoding_interface.py,sha256=Y8hPskDyX9DOyAD557F3-SdHjymaBkGXcoC9E_OW7SE,15825
|
|
44
47
|
backend/services/encoding_service.py,sha256=XAX4hyL2wIHdpBCTc8yglcjVJboZxRVAjc07lUTxgFg,18025
|
|
45
|
-
backend/services/firestore_service.py,sha256
|
|
48
|
+
backend/services/firestore_service.py,sha256=--Va-2lawugskepxo2p9w3ON2KsMGqacSOutCmU_CDQ,18801
|
|
46
49
|
backend/services/flacfetch_client.py,sha256=zj4VToKamR8bShXZifLvjDmUVa1HHvDhZuaw_b7g4lU,19901
|
|
47
50
|
backend/services/gce_encoding/README.md,sha256=frTFrp7-ugFAvUd_K9EgE4ud8Xu4Qf1V7BNirQn6qJk,2515
|
|
48
51
|
backend/services/gce_encoding/__init__.py,sha256=d2H6zM207NSc5afnfgkd3kwOXUu2gFBYjxBSS7tvX_s,571
|
|
@@ -50,7 +53,7 @@ backend/services/gce_encoding/main.py,sha256=2A0Bd3zYzbV66ikShB3avpqfNoDFBxr8Ja6
|
|
|
50
53
|
backend/services/gce_encoding/requirements.txt,sha256=-hW5ot-PnVoKkxrpY3yV6l68wx7AvzgnLYAWEgDGWNc,408
|
|
51
54
|
backend/services/gdrive_service.py,sha256=3JTXmPBuxpKopAbWDrh6YhqR5L9BLop1R7caXBnIk2A,13266
|
|
52
55
|
backend/services/job_logging.py,sha256=theCkAlwMbqyqAbu7vCCRpVRscaIpkam3UrCs_4Yi1g,8052
|
|
53
|
-
backend/services/job_manager.py,sha256=
|
|
56
|
+
backend/services/job_manager.py,sha256=2vBp-a1r3kwOg-CvXRjjlgkBh9U5AlI6yLeCBAjKLMY,33214
|
|
54
57
|
backend/services/job_notification_service.py,sha256=ywGJUMUDqV23OSDsugOUufpXO6N_24bM9YVaPp8bOk4,9557
|
|
55
58
|
backend/services/langfuse_preloader.py,sha256=t-Rt_ThjZm4K8W3jlyLn2H3cEy7KNUlhrvOPmdg-gYM,3100
|
|
56
59
|
backend/services/local_encoding_service.py,sha256=Q6yK2-QL9tHM_pUBoxdyfT7ln2L5xMx82JB2nC5OfNQ,20680
|
|
@@ -62,12 +65,13 @@ backend/services/packaging_service.py,sha256=MUuBk0jtG3vGQhuOWbDv9u1uIMwbBC4NknL
|
|
|
62
65
|
backend/services/rclone_service.py,sha256=Q6wRAjlBiMMh9FBbbFRp4SHK1ljOQBed9zUJaJgAuhU,3646
|
|
63
66
|
backend/services/spacy_preloader.py,sha256=rpsMZB0CQWcoXjq8nyZCwJBkyjGheKaCFrfhxCOln5w,1836
|
|
64
67
|
backend/services/storage_service.py,sha256=gA7Snz1pihzYpRKZjEZx3eDwyMUBezl3ccLuBdpTwE4,8573
|
|
65
|
-
backend/services/stripe_service.py,sha256=
|
|
68
|
+
backend/services/stripe_service.py,sha256=YPBVE2_ZeBYWklcFCTGoRWOuO1UTI16_Un2NKAE-sqE,14825
|
|
66
69
|
backend/services/structured_logging.py,sha256=daGgZUdc1xIiOdxL9-YtWHDy51hwZqd8ELpomkh2QH4,9063
|
|
67
70
|
backend/services/template_service.py,sha256=5CdSxBGhdBtxIUz24AhvltUfSWcFqZzBIxb9MjlN4YU,11212
|
|
71
|
+
backend/services/tenant_service.py,sha256=AlG5acJww-rpxGEQbw7eOWe1KaHLOcCn3fM95qDZMGw,9709
|
|
68
72
|
backend/services/theme_service.py,sha256=6lJqMpFjmRY09CMJcW9IJ6DVPffLmwE0DnpXDUKaJ0U,17374
|
|
69
73
|
backend/services/tracing.py,sha256=MNpJNMmSOw-9TRlb3dJk7pA8JTmSjAFHj9bSuyM5cOY,17210
|
|
70
|
-
backend/services/user_service.py,sha256=
|
|
74
|
+
backend/services/user_service.py,sha256=0_ztVqXKNH1jqKiZwqgStEfW95YowJbvYOL-eaBw2oY,29339
|
|
71
75
|
backend/services/worker_service.py,sha256=WRgh3kconWYmvraEIsx6eUto2frKacauKApFU1wIEiQ,21093
|
|
72
76
|
backend/services/youtube_service.py,sha256=L2jqcWkw6qIaKh4wEfX894mHaTU0m5qtgIX6TPi6uiQ,3828
|
|
73
77
|
backend/services/youtube_upload_service.py,sha256=fvo4WxLQaKY_I75ViBMLk6vm87ym2ABHK0Fn_StYN6g,16694
|
|
@@ -77,12 +81,16 @@ backend/tests/emulator/__init__.py,sha256=yOGPU5e0JPNYyUpy-u13jW4gEfWQs3dCJxfinc
|
|
|
77
81
|
backend/tests/emulator/conftest.py,sha256=VXhoJ3NZg3uiLzSAsushQtozFS7Lt6lmjk7ym-k6V8o,3844
|
|
78
82
|
backend/tests/emulator/test_e2e_cli_backend.py,sha256=BTrWDv4sRTeOlheZ-d4R37tZsvJ2MhdFLbDb9tjbF08,41018
|
|
79
83
|
backend/tests/emulator/test_emulator_integration.py,sha256=pm_JzgA2bmjcnHmGA-mZfJhUAYwGx10jv48A2deujRs,11822
|
|
84
|
+
backend/tests/emulator/test_made_for_you_integration.py,sha256=EYWZrYrQdoYk4Eb8G4M7Cyzq5ncAWqeYLuG8a31EAqc,5470
|
|
80
85
|
backend/tests/emulator/test_style_loading_direct.py,sha256=e-FzlCVW_-ealM9hTGa2qgpwoVRiYKrcXDWB1FjkwXc,16842
|
|
81
86
|
backend/tests/emulator/test_worker_logs_direct.py,sha256=96EeaSxeNQBtDiZ9ky1-CGE6Lv6ZV3dKuoF3inHxO8A,8158
|
|
82
87
|
backend/tests/emulator/test_worker_logs_subcollection.py,sha256=whwUXW_-fF-nQp2Z791PNxi0nnvlRwLo87QacF-yMUI,16834
|
|
83
88
|
backend/tests/requirements-test.txt,sha256=1ixGqdsXFG0hbqUPWUjNGPlKdQgYxo1GajXOiLhs66M,193
|
|
84
89
|
backend/tests/requirements.txt,sha256=1526vDy39B1ZjJ5URZtoiDGnjvwuxiD9dNrjOLd6-gA,95
|
|
85
90
|
backend/tests/test_admin_email_endpoints.py,sha256=C1mOX3K4P-_QqSWa8ZSH4gtMnUJ7hqq5rOi00zSKR8g,16592
|
|
91
|
+
backend/tests/test_admin_job_files.py,sha256=LXeV08a4XuaipM-QFFVJKE02e83Muj-JrjtCPlgExuM,13348
|
|
92
|
+
backend/tests/test_admin_job_reset.py,sha256=PsM6cBPeN5d6HO4pejaOQNyTh7OHTOFNPMTPuulcZCI,14292
|
|
93
|
+
backend/tests/test_admin_job_update.py,sha256=19NFazBk-fU8vrKKOoJV9c7pAvskNcAhihpmdQFZNIk,12157
|
|
86
94
|
backend/tests/test_api_integration.py,sha256=ps4dFoW69NCKJAWvPSr410fEILPxfQNME_YmLPdNqxo,14886
|
|
87
95
|
backend/tests/test_api_routes.py,sha256=j6alBwXgtKspXxbOiDuNCCVMENk6yxdFJkakbife-rM,3193
|
|
88
96
|
backend/tests/test_audio_analysis_service.py,sha256=bKIAgyxfdbUOr7atHH-WCbZcZFkWGmGOOn7IfvRF2ms,11373
|
|
@@ -96,23 +104,25 @@ backend/tests/test_dependencies.py,sha256=kWiOQEMQWFPIbAM8FEMYI921n3dLOQFJ7HXyRH
|
|
|
96
104
|
backend/tests/test_discord_service.py,sha256=OU5mVzjukFtJd_H5e_HxJqSsf78B0TrNfjxhwypbV34,8902
|
|
97
105
|
backend/tests/test_distribution_services.py,sha256=8oB2vMq5T_4rvOs6BURoLLTQiDf0kUJ-jF8dzJzDYRk,33699
|
|
98
106
|
backend/tests/test_dropbox_service.py,sha256=nDN1NjiCMS_f1bICWfe3maqbKT5FbhNuKCb1zjyjKq0,19198
|
|
99
|
-
backend/tests/test_email_service.py,sha256=
|
|
107
|
+
backend/tests/test_email_service.py,sha256=u1VSTBCZl7m3GYR-vKV84tu3q7zhMuIhNA5a4OiCDlE,25290
|
|
100
108
|
backend/tests/test_emulator_integration.py,sha256=m4teGnKLBKQNe9UdFi0-0iR_unnotWs07VWgSd_atuw,10387
|
|
101
109
|
backend/tests/test_encoding_interface.py,sha256=7q9mKX4QG8-bfkxQgZkmeirK_ynfL88UTbgWS0f097U,14609
|
|
102
110
|
backend/tests/test_file_upload.py,sha256=mWEIVj6cK6ZzKf4X0D0JhSk6pmJeCO0dOawV2GED7tc,68038
|
|
103
111
|
backend/tests/test_flacfetch_client.py,sha256=ZquMDLFVgORr9GoNfybOV9Z3eruborRgCvBRY6l-V8A,22614
|
|
104
112
|
backend/tests/test_gdrive_service.py,sha256=fh02KPaFqLEWfPaDQvSehn6r6RLq0bgt6MdXenF1kxA,19762
|
|
113
|
+
backend/tests/test_impersonation.py,sha256=vuCJaYmPOObLvFWuzxRvqadeRtFXgsbQxyOKd4TaxHg,9087
|
|
105
114
|
backend/tests/test_instrumental_api.py,sha256=2eWaoEAYVY02HZgY4esn5echuWGv_sznoO6FYWIgTbQ,17210
|
|
106
115
|
backend/tests/test_internal_api.py,sha256=qMGSDCkYd5OY_Z85vQu10xjqJn1JtaQcaKSnSBpVijw,12980
|
|
107
|
-
backend/tests/test_job_creation_regression.py,sha256=
|
|
108
|
-
backend/tests/test_job_manager.py,sha256=
|
|
116
|
+
backend/tests/test_job_creation_regression.py,sha256=ydd97391bMq40MSTlTlvMn3_zjtFDWzSQLjYczoqR_U,22826
|
|
117
|
+
backend/tests/test_job_manager.py,sha256=9SvwVkgN5VoyaJH-vzVN3VRoXpMJf4YAluLmspZrhzs,18862
|
|
109
118
|
backend/tests/test_job_manager_notifications.py,sha256=WlzXXxTCmHHCrD4XWu2j2fDYODlHGKThOQpZQeyhPXM,12996
|
|
110
119
|
backend/tests/test_job_notification_service.py,sha256=YbXqCWbsK8O5WICItb3VCrEY8qKqoJ3NBEZtwCs7AYM,17892
|
|
111
120
|
backend/tests/test_jobs_api.py,sha256=z3pGTawTpt75a1bQ5x4ynw3pTlNihLA6vqktTc6mBAU,11092
|
|
112
121
|
backend/tests/test_local_encoding_service.py,sha256=TbBaFOB-8Re09G-qXIQCl8mS9WlQMFNQF01V64IekyI,14638
|
|
113
122
|
backend/tests/test_local_preview_encoding_service.py,sha256=VwxpcAjDdYaVauSsrJ609U575VWRZbLsXew8BqBXpJY,20228
|
|
123
|
+
backend/tests/test_made_for_you.py,sha256=WNiJhmv_zse96Yg4scEXnpu66MMc0Hi4pNyiadfdlQ8,87628
|
|
114
124
|
backend/tests/test_main.py,sha256=BvzcadLUV0SEUO1ViKm0djgy_TEKXm8PPvgfcCCEJAY,3348
|
|
115
|
-
backend/tests/test_models.py,sha256=
|
|
125
|
+
backend/tests/test_models.py,sha256=buUjtm3TaiIff58SCpG0Du1Bnm-S9X8LyaW64zEhb4U,39606
|
|
116
126
|
backend/tests/test_packaging_service.py,sha256=v4Bj8R61Fx0Kng6RoTj3SRN9jmxow5qCVtTJstTWNfE,13916
|
|
117
127
|
backend/tests/test_requests.py,sha256=DI2IG67hHk2rhuzUfz_QNJCLRTy1NZPLsnSdZbIfPA8,6817
|
|
118
128
|
backend/tests/test_routes_jobs.py,sha256=iqOZ93ACKpizSzLEJUx5hIo0vkKe2oIT7s7iZqQUJaE,11258
|
|
@@ -123,6 +133,10 @@ backend/tests/test_spacy_preloader.py,sha256=uWpkrOPUOOcW1-fWClKMKVhXHGk30FXq537
|
|
|
123
133
|
backend/tests/test_storage_service.py,sha256=gorn3aFwKSr8lihxcG7kza7fYvDsTvxJhOxzuTtPK_o,19069
|
|
124
134
|
backend/tests/test_style_upload.py,sha256=8G2dGgvyMGXsh1jk8PcGpTlSdBQAlHn13kpZcKn6rTY,10491
|
|
125
135
|
backend/tests/test_template_service.py,sha256=BKURQ1M-hXXgdBl9LqhPQKfpItWOYDRBSt1y5FXQzaM,10431
|
|
136
|
+
backend/tests/test_tenant_api.py,sha256=P4395WYGldJIX7hYcbk96bIDW0Hogcj2McUG6iApNmc,13441
|
|
137
|
+
backend/tests/test_tenant_middleware.py,sha256=0WflieACxT1c_k-C2-yQ5sap_xm6z6w3p3ys09B8iMA,12577
|
|
138
|
+
backend/tests/test_tenant_models.py,sha256=NwNz197f5IPuhNU8f0d2OGBTeE7y7K2oulHfOJRo1EQ,15228
|
|
139
|
+
backend/tests/test_tenant_service.py,sha256=39bL1B0poa1LyxHDwFdQgSVu-QJ96rpe7Occ_ItC1Zc,16494
|
|
126
140
|
backend/tests/test_theme_service.py,sha256=I_BrXjDAwmqGhxClJA8kE9FDYdQmtdYQINjIwyI3NTs,20454
|
|
127
141
|
backend/tests/test_unicode_sanitization.py,sha256=M4QfJmHGZ8WrhTfg7QPljPj2kUkN5QUNThtofzZhQiE,22930
|
|
128
142
|
backend/tests/test_upload_api.py,sha256=oWXPunON3pSTEnE3oE9vIQk63GC6lKM5RgTmaKKQths,10127
|
|
@@ -144,7 +158,7 @@ backend/workers/lyrics_worker.py,sha256=nsy90pb0BPAl_lbZ0E7d7jIclcXK7W8d9RxxEdJa
|
|
|
144
158
|
backend/workers/render_video_worker.py,sha256=L6-VCxSBY6MbXBBm0-wu_DXxcIWKfW6ynTv9K03WCjY,25163
|
|
145
159
|
backend/workers/screens_worker.py,sha256=vCS7j4dQRQ_tsWNhz-XH5nau7kGyUSzSp-dzyZpeJEo,21402
|
|
146
160
|
backend/workers/style_helper.py,sha256=7-PM79dAUAZHTj-bnATy43ImqNdl_00E9pm94CYg1BM,6906
|
|
147
|
-
backend/workers/video_worker.py,sha256=
|
|
161
|
+
backend/workers/video_worker.py,sha256=5r3PcYdoca16fjZtFzCwfKm-G7QjL98ZLMhw6qGVDNE,55670
|
|
148
162
|
backend/workers/video_worker_orchestrator.py,sha256=NInZjFwztqcYneuxrkDqEFWV7z5BzGlaaoJxj812KA4,27510
|
|
149
163
|
backend/workers/worker_logging.py,sha256=nlbGsjDjdkv28Fm-95vKBumy6P47dGiNN5Zf3r0A480,9928
|
|
150
164
|
karaoke_gen/__init__.py,sha256=wHpDbURJxmJAMNZ0uQjISv5MIT7KD9RWYi15xlYgEhU,1351
|
|
@@ -287,7 +301,7 @@ lyrics_transcriber/frontend/public/favicon.ico,sha256=ZK7QvdBuZp0QxPkluCW4IKxfle
|
|
|
287
301
|
lyrics_transcriber/frontend/public/nomad-karaoke-logo.png,sha256=jTTBFXV6hGJGolZYQ-dIjgQQbMsehk5XGtsllhLrdzg,212641
|
|
288
302
|
lyrics_transcriber/frontend/public/nomad-karaoke-logo.svg,sha256=0LOH346_a-1JeYquMWd1v2A3XMN8zLDBeujfWAOeNYk,9185
|
|
289
303
|
lyrics_transcriber/frontend/src/App.tsx,sha256=STVmqN3xtXambV_5X4M0MNuwYjARHBQfn1cuCqxNGkw,7979
|
|
290
|
-
lyrics_transcriber/frontend/src/api.ts,sha256=
|
|
304
|
+
lyrics_transcriber/frontend/src/api.ts,sha256=MDTKq1rZUXKdRHb1YFDKaGuFOK-LO2wC3yvladlWjeY,9085
|
|
291
305
|
lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx,sha256=YvJlBP-3udqrOmvwKuMR7FxfIozZq5pVfTYvmhzbnHo,5602
|
|
292
306
|
lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx,sha256=ubJwQewryjUrXwpBkITQNu4POhoUtDbNA93cqa-yJKY,3416
|
|
293
307
|
lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx,sha256=Yg6FG0LtrneRfAYeBu3crt_RdN-_o7FojtYhDMDKi0o,8595
|
|
@@ -314,7 +328,7 @@ lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx,sha256=33XpyHj0s
|
|
|
314
328
|
lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx,sha256=eihGI49r9tKq-AaEtnmVrbiBOoJApWvabaZW4ydmg-4,5302
|
|
315
329
|
lyrics_transcriber/frontend/src/components/ModeSelector.tsx,sha256=HnBAK_gFgNBJLtMC_ESMVdUapDjmqmoLX8pQeyHfpOw,2651
|
|
316
330
|
lyrics_transcriber/frontend/src/components/ModelSelector.tsx,sha256=lfG_B5VAzSfrU0FqJl8XptN6DVt2kSljU96HMXo8mf4,559
|
|
317
|
-
lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx,sha256=
|
|
331
|
+
lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx,sha256=zKR_S1AyYAEmr1hnr2IN14mw1igpYbJB12HjaUrXbH4,6803
|
|
318
332
|
lyrics_transcriber/frontend/src/components/ReferenceView.tsx,sha256=a3CFpbJ8M-kFV3K79xyuo-sfOoC9He_IuXwhIcAOImo,10510
|
|
319
333
|
lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx,sha256=pVlqHrSloxXZV_Ib8cbk1invF7WA3uge5b7pnFPe9Pc,12290
|
|
320
334
|
lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx,sha256=VQg_gBFViAxQu9Z75o6rOsvmH5DZBjKq9FkU8aB_7mI,13790
|
|
@@ -436,8 +450,8 @@ lyrics_transcriber/transcribers/whisper.py,sha256=YcCB1ic9H6zL1GS0jD0emu8-qlcH0Q
|
|
|
436
450
|
lyrics_transcriber/types.py,sha256=UJjaxhVd2o14AG4G8ToU598p0JeYdiTFjpG38jGCoYQ,27917
|
|
437
451
|
lyrics_transcriber/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
438
452
|
lyrics_transcriber/utils/word_utils.py,sha256=-cMGpj9UV4F6IsoDKAV2i1aiqSO8eI91HMAm_igtVMk,958
|
|
439
|
-
karaoke_gen-0.
|
|
440
|
-
karaoke_gen-0.
|
|
441
|
-
karaoke_gen-0.
|
|
442
|
-
karaoke_gen-0.
|
|
443
|
-
karaoke_gen-0.
|
|
453
|
+
karaoke_gen-0.101.0.dist-info/METADATA,sha256=zkFeEtygnskVvZLwCarJ9blhCd4pA60LV75aSZm8RaI,23116
|
|
454
|
+
karaoke_gen-0.101.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
455
|
+
karaoke_gen-0.101.0.dist-info/entry_points.txt,sha256=xIyLe7K84ZyjO8L0_AmNectz93QjGSs5AkApMtlAd4g,160
|
|
456
|
+
karaoke_gen-0.101.0.dist-info/licenses/LICENSE,sha256=81R_4XwMZDODHD7JcZeUR8IiCU8AD7Ajl6bmwR9tYDk,1074
|
|
457
|
+
karaoke_gen-0.101.0.dist-info/RECORD,,
|