karaoke-gen 0.86.7__py3-none-any.whl → 0.96.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- backend/.coveragerc +20 -0
- backend/.gitignore +37 -0
- backend/Dockerfile +43 -0
- backend/Dockerfile.base +74 -0
- backend/README.md +242 -0
- backend/__init__.py +0 -0
- backend/api/__init__.py +0 -0
- backend/api/dependencies.py +457 -0
- backend/api/routes/__init__.py +0 -0
- backend/api/routes/admin.py +742 -0
- backend/api/routes/audio_search.py +903 -0
- backend/api/routes/auth.py +348 -0
- backend/api/routes/file_upload.py +2076 -0
- backend/api/routes/health.py +344 -0
- backend/api/routes/internal.py +435 -0
- backend/api/routes/jobs.py +1610 -0
- backend/api/routes/review.py +652 -0
- backend/api/routes/themes.py +162 -0
- backend/api/routes/users.py +1014 -0
- backend/config.py +172 -0
- backend/main.py +133 -0
- backend/middleware/__init__.py +5 -0
- backend/middleware/audit_logging.py +124 -0
- backend/models/__init__.py +0 -0
- backend/models/job.py +519 -0
- backend/models/requests.py +123 -0
- backend/models/theme.py +153 -0
- backend/models/user.py +254 -0
- backend/models/worker_log.py +164 -0
- backend/pyproject.toml +29 -0
- backend/quick-check.sh +93 -0
- backend/requirements.txt +29 -0
- backend/run_tests.sh +60 -0
- backend/services/__init__.py +0 -0
- backend/services/audio_analysis_service.py +243 -0
- backend/services/audio_editing_service.py +278 -0
- backend/services/audio_search_service.py +702 -0
- backend/services/auth_service.py +630 -0
- backend/services/credential_manager.py +792 -0
- backend/services/discord_service.py +172 -0
- backend/services/dropbox_service.py +301 -0
- backend/services/email_service.py +1093 -0
- backend/services/encoding_interface.py +454 -0
- backend/services/encoding_service.py +405 -0
- backend/services/firestore_service.py +512 -0
- backend/services/flacfetch_client.py +573 -0
- backend/services/gce_encoding/README.md +72 -0
- backend/services/gce_encoding/__init__.py +22 -0
- backend/services/gce_encoding/main.py +589 -0
- backend/services/gce_encoding/requirements.txt +16 -0
- backend/services/gdrive_service.py +356 -0
- backend/services/job_logging.py +258 -0
- backend/services/job_manager.py +842 -0
- backend/services/job_notification_service.py +271 -0
- backend/services/local_encoding_service.py +590 -0
- backend/services/local_preview_encoding_service.py +407 -0
- backend/services/lyrics_cache_service.py +216 -0
- backend/services/metrics.py +413 -0
- backend/services/packaging_service.py +287 -0
- backend/services/rclone_service.py +106 -0
- backend/services/storage_service.py +209 -0
- backend/services/stripe_service.py +275 -0
- backend/services/structured_logging.py +254 -0
- backend/services/template_service.py +330 -0
- backend/services/theme_service.py +469 -0
- backend/services/tracing.py +543 -0
- backend/services/user_service.py +721 -0
- backend/services/worker_service.py +558 -0
- backend/services/youtube_service.py +112 -0
- backend/services/youtube_upload_service.py +445 -0
- backend/tests/__init__.py +4 -0
- backend/tests/conftest.py +224 -0
- backend/tests/emulator/__init__.py +7 -0
- backend/tests/emulator/conftest.py +88 -0
- backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
- backend/tests/emulator/test_emulator_integration.py +356 -0
- backend/tests/emulator/test_style_loading_direct.py +436 -0
- backend/tests/emulator/test_worker_logs_direct.py +229 -0
- backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
- backend/tests/requirements-test.txt +10 -0
- backend/tests/requirements.txt +6 -0
- backend/tests/test_admin_email_endpoints.py +411 -0
- backend/tests/test_api_integration.py +460 -0
- backend/tests/test_api_routes.py +93 -0
- backend/tests/test_audio_analysis_service.py +294 -0
- backend/tests/test_audio_editing_service.py +386 -0
- backend/tests/test_audio_search.py +1398 -0
- backend/tests/test_audio_services.py +378 -0
- backend/tests/test_auth_firestore.py +231 -0
- backend/tests/test_config_extended.py +68 -0
- backend/tests/test_credential_manager.py +377 -0
- backend/tests/test_dependencies.py +54 -0
- backend/tests/test_discord_service.py +244 -0
- backend/tests/test_distribution_services.py +820 -0
- backend/tests/test_dropbox_service.py +472 -0
- backend/tests/test_email_service.py +492 -0
- backend/tests/test_emulator_integration.py +322 -0
- backend/tests/test_encoding_interface.py +412 -0
- backend/tests/test_file_upload.py +1739 -0
- backend/tests/test_flacfetch_client.py +632 -0
- backend/tests/test_gdrive_service.py +524 -0
- backend/tests/test_instrumental_api.py +431 -0
- backend/tests/test_internal_api.py +343 -0
- backend/tests/test_job_creation_regression.py +583 -0
- backend/tests/test_job_manager.py +339 -0
- backend/tests/test_job_manager_notifications.py +329 -0
- backend/tests/test_job_notification_service.py +443 -0
- backend/tests/test_jobs_api.py +273 -0
- backend/tests/test_local_encoding_service.py +423 -0
- backend/tests/test_local_preview_encoding_service.py +567 -0
- backend/tests/test_main.py +87 -0
- backend/tests/test_models.py +918 -0
- backend/tests/test_packaging_service.py +382 -0
- backend/tests/test_requests.py +201 -0
- backend/tests/test_routes_jobs.py +282 -0
- backend/tests/test_routes_review.py +337 -0
- backend/tests/test_services.py +556 -0
- backend/tests/test_services_extended.py +112 -0
- backend/tests/test_storage_service.py +448 -0
- backend/tests/test_style_upload.py +261 -0
- backend/tests/test_template_service.py +295 -0
- backend/tests/test_theme_service.py +516 -0
- backend/tests/test_unicode_sanitization.py +522 -0
- backend/tests/test_upload_api.py +256 -0
- backend/tests/test_validate.py +156 -0
- backend/tests/test_video_worker_orchestrator.py +847 -0
- backend/tests/test_worker_log_subcollection.py +509 -0
- backend/tests/test_worker_logging.py +365 -0
- backend/tests/test_workers.py +1116 -0
- backend/tests/test_workers_extended.py +178 -0
- backend/tests/test_youtube_service.py +247 -0
- backend/tests/test_youtube_upload_service.py +568 -0
- backend/validate.py +173 -0
- backend/version.py +27 -0
- backend/workers/README.md +597 -0
- backend/workers/__init__.py +11 -0
- backend/workers/audio_worker.py +618 -0
- backend/workers/lyrics_worker.py +683 -0
- backend/workers/render_video_worker.py +483 -0
- backend/workers/screens_worker.py +525 -0
- backend/workers/style_helper.py +198 -0
- backend/workers/video_worker.py +1277 -0
- backend/workers/video_worker_orchestrator.py +701 -0
- backend/workers/worker_logging.py +278 -0
- karaoke_gen/instrumental_review/static/index.html +7 -4
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
- karaoke_gen/style_loader.py +3 -1
- karaoke_gen/utils/__init__.py +163 -8
- karaoke_gen/video_background_processor.py +9 -4
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/METADATA +2 -1
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/RECORD +187 -42
- lyrics_transcriber/correction/agentic/providers/config.py +9 -5
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +1 -51
- lyrics_transcriber/correction/corrector.py +192 -130
- lyrics_transcriber/correction/operations.py +24 -9
- lyrics_transcriber/frontend/package-lock.json +2 -2
- lyrics_transcriber/frontend/package.json +1 -1
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
- lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
- lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
- lyrics_transcriber/frontend/src/theme.ts +42 -15
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/vite.config.js +5 -0
- lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
- lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/index.html +6 -2
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
- lyrics_transcriber/output/generator.py +17 -3
- lyrics_transcriber/output/video.py +60 -95
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -268,12 +268,26 @@ class LyricsCorrector:
|
|
|
268
268
|
_adapt = None
|
|
269
269
|
_ModelRouter = None
|
|
270
270
|
|
|
271
|
+
# Pre-initialized agentic corrector (created once, reused for all gaps)
|
|
272
|
+
_agentic_agent = None
|
|
273
|
+
|
|
271
274
|
if use_agentic_env:
|
|
272
275
|
try:
|
|
273
276
|
from lyrics_transcriber.correction.agentic.agent import AgenticCorrector as _AgenticCorrector
|
|
274
277
|
from lyrics_transcriber.correction.agentic.adapter import adapt_proposals_to_word_corrections as _adapt
|
|
275
278
|
from lyrics_transcriber.correction.agentic.router import ModelRouter as _ModelRouter
|
|
276
279
|
self.logger.info("🤖 Agentic modules imported successfully - running in AGENTIC-ONLY mode")
|
|
280
|
+
|
|
281
|
+
# Create agent ONCE and reuse for all gaps (avoids repeated model initialization)
|
|
282
|
+
_router = _ModelRouter()
|
|
283
|
+
model_id = _router.choose_model("gap", uncertainty=0.5) # Use default uncertainty
|
|
284
|
+
self.logger.info(f"🤖 Creating single AgenticCorrector with model: {model_id}")
|
|
285
|
+
_agentic_agent = _AgenticCorrector.from_model(
|
|
286
|
+
model=model_id,
|
|
287
|
+
session_id=session_id,
|
|
288
|
+
cache_dir=str(self._cache_dir)
|
|
289
|
+
)
|
|
290
|
+
self.logger.info("🤖 AgenticCorrector initialized and ready for all gaps")
|
|
277
291
|
except Exception as e:
|
|
278
292
|
self.logger.error(f"🤖 Failed to import agentic modules but USE_AGENTIC_AI=1: {e}")
|
|
279
293
|
raise RuntimeError(f"Agentic AI correction is enabled but required modules could not be imported: {e}") from e
|
|
@@ -443,145 +457,193 @@ class LyricsCorrector:
|
|
|
443
457
|
sys.exit(0)
|
|
444
458
|
# === END TEMPORARY CODE ===
|
|
445
459
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
self.logger.warning(
|
|
451
|
-
f"⏰ AGENTIC TIMEOUT: Deadline exceeded after processing {i-1}/{len(gap_sequences)} gaps. "
|
|
452
|
-
"Skipping remaining gaps - human review will correct any issues."
|
|
453
|
-
)
|
|
454
|
-
# Break out of loop - continue with whatever corrections we have (likely none)
|
|
455
|
-
break
|
|
460
|
+
# AGENTIC-ONLY MODE: Process all gaps in parallel for better performance
|
|
461
|
+
if use_agentic_env:
|
|
462
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
463
|
+
from lyrics_transcriber.correction.agentic.providers.config import ProviderConfig
|
|
456
464
|
|
|
457
|
-
|
|
465
|
+
# Get parallel processing config
|
|
466
|
+
_config = ProviderConfig.from_env()
|
|
467
|
+
max_workers = _config.max_parallel_gaps
|
|
468
|
+
self.logger.info(f"🤖 Processing {len(gap_sequences)} gaps in parallel (max_workers={max_workers})")
|
|
458
469
|
|
|
459
|
-
#
|
|
460
|
-
|
|
461
|
-
|
|
470
|
+
# Pre-compute shared data structures once (not per-gap)
|
|
471
|
+
all_transcribed_words = []
|
|
472
|
+
for seg in segments:
|
|
473
|
+
all_transcribed_words.extend(seg.words)
|
|
474
|
+
word_position = {w.id: idx for idx, w in enumerate(all_transcribed_words)}
|
|
462
475
|
|
|
463
|
-
#
|
|
464
|
-
|
|
465
|
-
|
|
476
|
+
# Build reference contexts once (same for all gaps)
|
|
477
|
+
reference_contexts = {}
|
|
478
|
+
for source, lyrics_data in self.reference_lyrics.items():
|
|
479
|
+
if lyrics_data and lyrics_data.segments:
|
|
480
|
+
ref_words = []
|
|
481
|
+
for seg in lyrics_data.segments:
|
|
482
|
+
ref_words.extend([w.text for w in seg.words])
|
|
483
|
+
reference_contexts[source] = " ".join(ref_words)
|
|
484
|
+
|
|
485
|
+
# Get artist and title once
|
|
486
|
+
artist = metadata.get("artist") if metadata else None
|
|
487
|
+
title = metadata.get("title") if metadata else None
|
|
488
|
+
|
|
489
|
+
# Prepare all gap inputs upfront
|
|
490
|
+
gap_inputs = []
|
|
491
|
+
for i, gap in enumerate(gap_sequences, 1):
|
|
492
|
+
# Prepare gap words data
|
|
493
|
+
gap_words_data = []
|
|
494
|
+
for word_id in gap.transcribed_word_ids:
|
|
495
|
+
if word_id in word_map:
|
|
496
|
+
word = word_map[word_id]
|
|
497
|
+
gap_words_data.append({
|
|
498
|
+
"id": word_id,
|
|
499
|
+
"text": word.text,
|
|
500
|
+
"start_time": getattr(word, 'start_time', 0),
|
|
501
|
+
"end_time": getattr(word, 'end_time', 0)
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
# Compute context words
|
|
505
|
+
gap_positions = [word_position[wid] for wid in gap.transcribed_word_ids if wid in word_position]
|
|
506
|
+
preceding_words = ""
|
|
507
|
+
following_words = ""
|
|
508
|
+
|
|
509
|
+
if gap_positions:
|
|
510
|
+
first_gap_pos = min(gap_positions)
|
|
511
|
+
last_gap_pos = max(gap_positions)
|
|
512
|
+
|
|
513
|
+
# Get 10 words before
|
|
514
|
+
start_pos = max(0, first_gap_pos - 10)
|
|
515
|
+
preceding_list = [all_transcribed_words[idx].text for idx in range(start_pos, first_gap_pos) if idx < len(all_transcribed_words)]
|
|
516
|
+
preceding_words = " ".join(preceding_list)
|
|
517
|
+
|
|
518
|
+
# Get 10 words after
|
|
519
|
+
end_pos = min(len(all_transcribed_words), last_gap_pos + 11)
|
|
520
|
+
following_list = [all_transcribed_words[idx].text for idx in range(last_gap_pos + 1, end_pos) if idx < len(all_transcribed_words)]
|
|
521
|
+
following_words = " ".join(following_list)
|
|
522
|
+
|
|
523
|
+
gap_inputs.append({
|
|
524
|
+
'index': i,
|
|
525
|
+
'gap': gap,
|
|
526
|
+
'gap_id': f"gap_{i}",
|
|
527
|
+
'gap_words': gap_words_data,
|
|
528
|
+
'preceding_words': preceding_words,
|
|
529
|
+
'following_words': following_words,
|
|
530
|
+
'reference_contexts': reference_contexts,
|
|
531
|
+
'artist': artist,
|
|
532
|
+
'title': title
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
# Function to process a single gap (runs in thread pool)
|
|
536
|
+
def process_single_gap(gap_input):
|
|
537
|
+
"""Process a single gap and return proposals. Thread-safe."""
|
|
538
|
+
idx = gap_input['index']
|
|
466
539
|
try:
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
"start_time": getattr(word, 'start_time', 0),
|
|
476
|
-
"end_time": getattr(word, 'end_time', 0)
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
# Get context words
|
|
480
|
-
all_transcribed_words = []
|
|
481
|
-
for seg in segments:
|
|
482
|
-
all_transcribed_words.extend(seg.words)
|
|
483
|
-
word_position = {w.id: idx for idx, w in enumerate(all_transcribed_words)}
|
|
484
|
-
|
|
485
|
-
gap_positions = [word_position[wid] for wid in gap.transcribed_word_ids if wid in word_position]
|
|
486
|
-
preceding_words = ""
|
|
487
|
-
following_words = ""
|
|
488
|
-
|
|
489
|
-
if gap_positions:
|
|
490
|
-
first_gap_pos = min(gap_positions)
|
|
491
|
-
last_gap_pos = max(gap_positions)
|
|
492
|
-
|
|
493
|
-
# Get 10 words before
|
|
494
|
-
start_pos = max(0, first_gap_pos - 10)
|
|
495
|
-
preceding_list = [all_transcribed_words[idx].text for idx in range(start_pos, first_gap_pos) if idx < len(all_transcribed_words)]
|
|
496
|
-
preceding_words = " ".join(preceding_list)
|
|
497
|
-
|
|
498
|
-
# Get 10 words after
|
|
499
|
-
end_pos = min(len(all_transcribed_words), last_gap_pos + 11)
|
|
500
|
-
following_list = [all_transcribed_words[idx].text for idx in range(last_gap_pos + 1, end_pos) if idx < len(all_transcribed_words)]
|
|
501
|
-
following_words = " ".join(following_list)
|
|
502
|
-
|
|
503
|
-
# Get reference contexts from all sources
|
|
504
|
-
reference_contexts = {}
|
|
505
|
-
for source, lyrics_data in self.reference_lyrics.items():
|
|
506
|
-
if lyrics_data and lyrics_data.segments:
|
|
507
|
-
ref_words = []
|
|
508
|
-
for seg in lyrics_data.segments:
|
|
509
|
-
ref_words.extend([w.text for w in seg.words])
|
|
510
|
-
# For now, use full text (handlers will extract relevant portions)
|
|
511
|
-
reference_contexts[source] = " ".join(ref_words)
|
|
512
|
-
|
|
513
|
-
# Get artist and title from metadata
|
|
514
|
-
artist = metadata.get("artist") if metadata else None
|
|
515
|
-
title = metadata.get("title") if metadata else None
|
|
516
|
-
|
|
517
|
-
# Choose model via router
|
|
518
|
-
_router = _ModelRouter()
|
|
519
|
-
uncertainty = 0.3 if len(gap_words_data) <= 2 else 0.7
|
|
520
|
-
model_id = _router.choose_model("gap", uncertainty)
|
|
521
|
-
self.logger.debug(f"🤖 Router selected model: {model_id}")
|
|
522
|
-
|
|
523
|
-
# Create agent and use new classification-first workflow
|
|
524
|
-
self.logger.debug(f"🤖 Creating AgenticCorrector with model: {model_id}")
|
|
525
|
-
_agent = _AgenticCorrector.from_model(
|
|
526
|
-
model=model_id,
|
|
527
|
-
session_id=session_id,
|
|
528
|
-
cache_dir=str(self._cache_dir)
|
|
529
|
-
)
|
|
530
|
-
|
|
531
|
-
# Use new propose_for_gap method
|
|
532
|
-
self.logger.debug(f"🤖 Calling agent.propose_for_gap() for gap {i}")
|
|
533
|
-
_proposals = _agent.propose_for_gap(
|
|
534
|
-
gap_id=f"gap_{i}",
|
|
535
|
-
gap_words=gap_words_data,
|
|
536
|
-
preceding_words=preceding_words,
|
|
537
|
-
following_words=following_words,
|
|
538
|
-
reference_contexts=reference_contexts,
|
|
539
|
-
artist=artist,
|
|
540
|
-
title=title
|
|
540
|
+
proposals = _agentic_agent.propose_for_gap(
|
|
541
|
+
gap_id=gap_input['gap_id'],
|
|
542
|
+
gap_words=gap_input['gap_words'],
|
|
543
|
+
preceding_words=gap_input['preceding_words'],
|
|
544
|
+
following_words=gap_input['following_words'],
|
|
545
|
+
reference_contexts=gap_input['reference_contexts'],
|
|
546
|
+
artist=gap_input['artist'],
|
|
547
|
+
title=gap_input['title']
|
|
541
548
|
)
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
segments_after=updated_segments,
|
|
563
|
-
created_word_ids=[w.id for w in self._get_new_words(updated_segments, affected_word_ids)],
|
|
564
|
-
deleted_word_ids=[id for id in affected_word_ids if not self._word_exists(id, updated_segments)],
|
|
549
|
+
return {'index': idx, 'gap': gap_input['gap'], 'proposals': proposals, 'error': None}
|
|
550
|
+
except Exception as e:
|
|
551
|
+
return {'index': idx, 'gap': gap_input['gap'], 'proposals': None, 'error': str(e)}
|
|
552
|
+
|
|
553
|
+
# Process gaps in parallel
|
|
554
|
+
results = [None] * len(gap_inputs)
|
|
555
|
+
completed_count = 0
|
|
556
|
+
errors = []
|
|
557
|
+
|
|
558
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
559
|
+
# Submit all tasks
|
|
560
|
+
future_to_input = {executor.submit(process_single_gap, g): g for g in gap_inputs}
|
|
561
|
+
|
|
562
|
+
# Collect results as they complete
|
|
563
|
+
for future in as_completed(future_to_input):
|
|
564
|
+
# Check deadline
|
|
565
|
+
if deadline and time.time() > deadline:
|
|
566
|
+
self.logger.warning(
|
|
567
|
+
f"⏰ AGENTIC TIMEOUT: Deadline exceeded after processing {completed_count}/{len(gap_sequences)} gaps. "
|
|
568
|
+
"Cancelling remaining gaps - human review will correct any issues."
|
|
565
569
|
)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
570
|
+
# Cancel remaining futures (use list() to avoid mutating dict during iteration)
|
|
571
|
+
for f in list(future_to_input.keys()):
|
|
572
|
+
f.cancel()
|
|
573
|
+
break
|
|
574
|
+
|
|
575
|
+
result = future.result()
|
|
576
|
+
idx = result['index'] - 1 # Convert 1-based to 0-based
|
|
577
|
+
results[idx] = result
|
|
578
|
+
completed_count += 1
|
|
579
|
+
|
|
580
|
+
if result['error']:
|
|
581
|
+
errors.append(f"Gap {result['index']}: {result['error']}")
|
|
582
|
+
self.logger.error(f"🤖 Gap {result['index']} failed: {result['error']}")
|
|
574
583
|
else:
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
584
|
+
proposal_count = len(result['proposals']) if result['proposals'] else 0
|
|
585
|
+
self.logger.info(f"🤖 Gap {result['index']}/{len(gap_sequences)} completed ({proposal_count} proposals)")
|
|
586
|
+
|
|
587
|
+
self.logger.info(f"🤖 Parallel processing complete: {completed_count}/{len(gap_sequences)} gaps processed")
|
|
588
|
+
|
|
589
|
+
# If any errors occurred, fail fast
|
|
590
|
+
if errors:
|
|
591
|
+
raise RuntimeError(f"Agentic AI correction failed for {len(errors)} gaps: {'; '.join(errors)}")
|
|
592
|
+
|
|
593
|
+
# Apply corrections sequentially (must be in order due to segment modifications)
|
|
594
|
+
for result in results:
|
|
595
|
+
if result is None:
|
|
596
|
+
continue # Skipped due to deadline
|
|
597
|
+
|
|
598
|
+
i = result['index']
|
|
599
|
+
gap = result['gap']
|
|
600
|
+
_proposals = result['proposals']
|
|
601
|
+
|
|
602
|
+
_agentic_corrections = _adapt(_proposals, word_map, linear_position_map) if _proposals else []
|
|
603
|
+
|
|
604
|
+
if _agentic_corrections:
|
|
605
|
+
self.logger.info(f"🤖 Applying {len(_agentic_corrections)} agentic corrections for gap {i}")
|
|
606
|
+
affected_word_ids = [w.id for w in self._get_affected_words(gap, segments)]
|
|
607
|
+
affected_segment_ids = [s.id for s in self._get_affected_segments(gap, segments)]
|
|
608
|
+
updated_segments = self._apply_corrections_to_segments(self._get_affected_segments(gap, segments), _agentic_corrections)
|
|
609
|
+
for correction in _agentic_corrections:
|
|
610
|
+
if correction.word_id and correction.corrected_word_id:
|
|
611
|
+
word_id_map[correction.word_id] = correction.corrected_word_id
|
|
612
|
+
for old_seg, new_seg in zip(self._get_affected_segments(gap, segments), updated_segments):
|
|
613
|
+
segment_id_map[old_seg.id] = new_seg.id
|
|
614
|
+
step = CorrectionStep(
|
|
615
|
+
handler_name="AgenticCorrector",
|
|
616
|
+
affected_word_ids=affected_word_ids,
|
|
617
|
+
affected_segment_ids=affected_segment_ids,
|
|
618
|
+
corrections=_agentic_corrections,
|
|
619
|
+
segments_before=self._get_affected_segments(gap, segments),
|
|
620
|
+
segments_after=updated_segments,
|
|
621
|
+
created_word_ids=[w.id for w in self._get_new_words(updated_segments, affected_word_ids)],
|
|
622
|
+
deleted_word_ids=[id for id in affected_word_ids if not self._word_exists(id, updated_segments)],
|
|
623
|
+
)
|
|
624
|
+
correction_steps.append(step)
|
|
625
|
+
all_corrections.extend(_agentic_corrections)
|
|
626
|
+
# Log corrections made
|
|
627
|
+
for correction in _agentic_corrections:
|
|
628
|
+
self.logger.info(
|
|
629
|
+
f"Made correction: '{correction.original_word}' -> '{correction.corrected_word}' "
|
|
630
|
+
f"(confidence: {correction.confidence:.2f}, reason: {correction.reason})"
|
|
631
|
+
)
|
|
632
|
+
else:
|
|
633
|
+
self.logger.debug(f"🤖 No agentic corrections needed for gap {i}")
|
|
634
|
+
|
|
635
|
+
# RULE-BASED MODE: Process gaps sequentially
|
|
636
|
+
for i, gap in enumerate(gap_sequences, 1):
|
|
637
|
+
# Skip if we already processed in agentic mode
|
|
638
|
+
if use_agentic_env:
|
|
583
639
|
continue
|
|
584
640
|
|
|
641
|
+
self.logger.info(f"Processing gap {i}/{len(gap_sequences)} at position {gap.transcription_position}")
|
|
642
|
+
|
|
643
|
+
# Get the actual words for logging
|
|
644
|
+
gap_words = [word_map[word_id] for word_id in gap.transcribed_word_ids]
|
|
645
|
+
self.logger.debug(f"Gap text: '{' '.join(w.text for w in gap_words)}'")
|
|
646
|
+
|
|
585
647
|
# RULE-BASED MODE: Try each handler in order
|
|
586
648
|
for handler in self.handlers:
|
|
587
649
|
handler_name = handler.__class__.__name__
|
|
@@ -274,11 +274,12 @@ class CorrectionOperations:
|
|
|
274
274
|
audio_filepath: str,
|
|
275
275
|
artist: Optional[str] = None,
|
|
276
276
|
title: Optional[str] = None,
|
|
277
|
-
logger: Optional[logging.Logger] = None
|
|
277
|
+
logger: Optional[logging.Logger] = None,
|
|
278
|
+
ass_only: bool = False,
|
|
278
279
|
) -> Dict[str, Any]:
|
|
279
280
|
"""
|
|
280
281
|
Generate a preview video with current corrections.
|
|
281
|
-
|
|
282
|
+
|
|
282
283
|
Args:
|
|
283
284
|
correction_result: Current correction result
|
|
284
285
|
updated_data: Updated correction data for preview
|
|
@@ -287,10 +288,12 @@ class CorrectionOperations:
|
|
|
287
288
|
artist: Optional artist name
|
|
288
289
|
title: Optional title
|
|
289
290
|
logger: Optional logger instance
|
|
290
|
-
|
|
291
|
+
ass_only: If True, generate only ASS subtitles without video encoding.
|
|
292
|
+
Useful when video encoding is offloaded to external service.
|
|
293
|
+
|
|
291
294
|
Returns:
|
|
292
|
-
Dict with status, preview_hash, and video_path
|
|
293
|
-
|
|
295
|
+
Dict with status, preview_hash, and video_path (or ass_path if ass_only)
|
|
296
|
+
|
|
294
297
|
Raises:
|
|
295
298
|
ValueError: If preview video generation fails
|
|
296
299
|
"""
|
|
@@ -338,15 +341,27 @@ class CorrectionOperations:
|
|
|
338
341
|
audio_filepath=audio_filepath,
|
|
339
342
|
artist=artist,
|
|
340
343
|
title=title,
|
|
344
|
+
ass_only=ass_only,
|
|
341
345
|
)
|
|
342
|
-
|
|
346
|
+
|
|
347
|
+
# When ass_only, we only need the ASS file (video encoding done externally)
|
|
348
|
+
if ass_only:
|
|
349
|
+
if not preview_outputs.ass:
|
|
350
|
+
raise ValueError("Preview ASS generation failed")
|
|
351
|
+
logger.info(f"Generated preview ASS: {preview_outputs.ass}")
|
|
352
|
+
return {
|
|
353
|
+
"status": "success",
|
|
354
|
+
"preview_hash": preview_hash,
|
|
355
|
+
"ass_path": preview_outputs.ass,
|
|
356
|
+
}
|
|
357
|
+
|
|
343
358
|
if not preview_outputs.video:
|
|
344
359
|
raise ValueError("Preview video generation failed")
|
|
345
|
-
|
|
360
|
+
|
|
346
361
|
logger.info(f"Generated preview video: {preview_outputs.video}")
|
|
347
|
-
|
|
362
|
+
|
|
348
363
|
return {
|
|
349
364
|
"status": "success",
|
|
350
365
|
"preview_hash": preview_hash,
|
|
351
|
-
"video_path": preview_outputs.video
|
|
366
|
+
"video_path": preview_outputs.video,
|
|
352
367
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lyrics-transcriber-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.84.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "lyrics-transcriber-frontend",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.84.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@emotion/react": "^11.14.0",
|
|
12
12
|
"@emotion/styled": "^11.14.0",
|
|
@@ -98,7 +98,7 @@ export const AIFeedbackModal: React.FC<Props> = ({ isOpen, onClose, onSubmit, su
|
|
|
98
98
|
onClick={() =>
|
|
99
99
|
onSubmit({ reviewerAction, finalText: finalText || undefined, reasonCategory, reasonDetail: reasonDetail || undefined })
|
|
100
100
|
}
|
|
101
|
-
style={{ background: '#
|
|
101
|
+
style={{ background: '#ff7acc', color: '#fff', border: 'none', borderRadius: 4, padding: '6px 12px', cursor: 'pointer' }}
|
|
102
102
|
>
|
|
103
103
|
Submit
|
|
104
104
|
</button>
|
|
@@ -43,21 +43,21 @@ const WordContainer = styled(Box, {
|
|
|
43
43
|
'50%': { opacity: 0.5 }
|
|
44
44
|
},
|
|
45
45
|
'&:hover': {
|
|
46
|
-
backgroundColor: 'rgba(34, 197, 94, 0.35)' // green tint hover for
|
|
46
|
+
backgroundColor: 'rgba(34, 197, 94, 0.35)' // green tint hover - works for both modes
|
|
47
47
|
}
|
|
48
48
|
}))
|
|
49
49
|
|
|
50
|
-
const OriginalWordLabel = styled(Box)({
|
|
50
|
+
const OriginalWordLabel = styled(Box)(({ theme }) => ({
|
|
51
51
|
position: 'absolute',
|
|
52
52
|
top: '-14px',
|
|
53
53
|
left: '0',
|
|
54
54
|
fontSize: '0.6rem',
|
|
55
|
-
color:
|
|
55
|
+
color: theme.palette.text.secondary, // Theme-aware text color
|
|
56
56
|
textDecoration: 'line-through',
|
|
57
57
|
opacity: 0.7,
|
|
58
58
|
whiteSpace: 'nowrap',
|
|
59
59
|
pointerEvents: 'none'
|
|
60
|
-
})
|
|
60
|
+
}))
|
|
61
61
|
|
|
62
62
|
const ActionsContainer = styled(Box)({
|
|
63
63
|
display: 'inline-flex',
|
|
@@ -72,10 +72,14 @@ const ActionButton = styled(IconButton)(({ theme }) => ({
|
|
|
72
72
|
minHeight: '20px',
|
|
73
73
|
width: '20px',
|
|
74
74
|
height: '20px',
|
|
75
|
-
backgroundColor:
|
|
76
|
-
|
|
75
|
+
backgroundColor: theme.palette.mode === 'dark'
|
|
76
|
+
? 'rgba(30, 41, 59, 0.9)' // slate-800 with opacity for dark mode
|
|
77
|
+
: 'rgba(241, 245, 249, 0.9)', // slate-100 for light mode
|
|
78
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
77
79
|
'&:hover': {
|
|
78
|
-
backgroundColor:
|
|
80
|
+
backgroundColor: theme.palette.mode === 'dark'
|
|
81
|
+
? 'rgba(51, 65, 85, 1)' // slate-700 for dark mode
|
|
82
|
+
: 'rgba(226, 232, 240, 1)', // slate-200 for light mode
|
|
79
83
|
transform: 'scale(1.1)'
|
|
80
84
|
},
|
|
81
85
|
'& .MuiSvgIcon-root': {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, Button } from '@mui/material'
|
|
1
|
+
import { Box, Button, useMediaQuery, useTheme } from '@mui/material'
|
|
2
2
|
import DeleteIcon from '@mui/icons-material/Delete'
|
|
3
3
|
import RestoreIcon from '@mui/icons-material/RestoreFromTrash'
|
|
4
4
|
import HistoryIcon from '@mui/icons-material/History'
|
|
@@ -25,13 +25,29 @@ export default function EditActionBar({
|
|
|
25
25
|
originalTranscribedSegment,
|
|
26
26
|
isGlobal = false
|
|
27
27
|
}: EditActionBarProps) {
|
|
28
|
+
const theme = useTheme()
|
|
29
|
+
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
|
30
|
+
|
|
28
31
|
return (
|
|
29
|
-
<Box sx={{
|
|
30
|
-
|
|
32
|
+
<Box sx={{
|
|
33
|
+
display: 'flex',
|
|
34
|
+
flexDirection: isMobile ? 'column' : 'row',
|
|
35
|
+
alignItems: isMobile ? 'stretch' : 'center',
|
|
36
|
+
gap: 1,
|
|
37
|
+
width: '100%'
|
|
38
|
+
}}>
|
|
39
|
+
<Box sx={{
|
|
40
|
+
display: 'flex',
|
|
41
|
+
alignItems: 'center',
|
|
42
|
+
gap: 1,
|
|
43
|
+
flexWrap: 'wrap',
|
|
44
|
+
justifyContent: isMobile ? 'center' : 'flex-start'
|
|
45
|
+
}}>
|
|
31
46
|
<Button
|
|
32
47
|
startIcon={<RestoreIcon />}
|
|
33
48
|
onClick={onReset}
|
|
34
49
|
color="warning"
|
|
50
|
+
size={isMobile ? 'small' : 'medium'}
|
|
35
51
|
>
|
|
36
52
|
Reset
|
|
37
53
|
</Button>
|
|
@@ -39,6 +55,7 @@ export default function EditActionBar({
|
|
|
39
55
|
<Button
|
|
40
56
|
onClick={onRevertToOriginal}
|
|
41
57
|
startIcon={<HistoryIcon />}
|
|
58
|
+
size={isMobile ? 'small' : 'medium'}
|
|
42
59
|
>
|
|
43
60
|
Un-Correct
|
|
44
61
|
</Button>
|
|
@@ -48,17 +65,26 @@ export default function EditActionBar({
|
|
|
48
65
|
startIcon={<DeleteIcon />}
|
|
49
66
|
onClick={onDelete}
|
|
50
67
|
color="error"
|
|
68
|
+
size={isMobile ? 'small' : 'medium'}
|
|
51
69
|
>
|
|
52
70
|
Delete Segment
|
|
53
71
|
</Button>
|
|
54
72
|
)}
|
|
55
73
|
</Box>
|
|
56
|
-
<Box sx={{
|
|
57
|
-
|
|
74
|
+
<Box sx={{
|
|
75
|
+
ml: isMobile ? 0 : 'auto',
|
|
76
|
+
display: 'flex',
|
|
77
|
+
gap: 1,
|
|
78
|
+
justifyContent: isMobile ? 'center' : 'flex-end'
|
|
79
|
+
}}>
|
|
80
|
+
<Button onClick={onClose} size={isMobile ? 'small' : 'medium'}>
|
|
81
|
+
Cancel
|
|
82
|
+
</Button>
|
|
58
83
|
<Button
|
|
59
84
|
onClick={onSave}
|
|
60
85
|
variant="contained"
|
|
61
86
|
disabled={!editedSegment || editedSegment.words.length === 0}
|
|
87
|
+
size={isMobile ? 'small' : 'medium'}
|
|
62
88
|
>
|
|
63
89
|
Save
|
|
64
90
|
</Button>
|