karaoke-gen 0.57.0__py3-none-any.whl → 0.71.23__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.
- karaoke_gen/audio_fetcher.py +461 -0
- karaoke_gen/audio_processor.py +407 -30
- karaoke_gen/config.py +62 -113
- karaoke_gen/file_handler.py +32 -59
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +148 -67
- karaoke_gen/karaoke_gen.py +270 -61
- karaoke_gen/lyrics_processor.py +13 -1
- karaoke_gen/metadata.py +78 -73
- karaoke_gen/pipeline/__init__.py +87 -0
- karaoke_gen/pipeline/base.py +215 -0
- karaoke_gen/pipeline/context.py +230 -0
- karaoke_gen/pipeline/executors/__init__.py +21 -0
- karaoke_gen/pipeline/executors/local.py +159 -0
- karaoke_gen/pipeline/executors/remote.py +257 -0
- karaoke_gen/pipeline/stages/__init__.py +27 -0
- karaoke_gen/pipeline/stages/finalize.py +202 -0
- karaoke_gen/pipeline/stages/render.py +165 -0
- karaoke_gen/pipeline/stages/screens.py +139 -0
- karaoke_gen/pipeline/stages/separation.py +191 -0
- karaoke_gen/pipeline/stages/transcription.py +191 -0
- karaoke_gen/style_loader.py +531 -0
- karaoke_gen/utils/bulk_cli.py +6 -0
- karaoke_gen/utils/cli_args.py +424 -0
- karaoke_gen/utils/gen_cli.py +26 -261
- karaoke_gen/utils/remote_cli.py +1815 -0
- karaoke_gen/video_background_processor.py +351 -0
- karaoke_gen-0.71.23.dist-info/METADATA +610 -0
- karaoke_gen-0.71.23.dist-info/RECORD +275 -0
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/WHEEL +1 -1
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/entry_points.txt +1 -0
- lyrics_transcriber/__init__.py +10 -0
- lyrics_transcriber/cli/__init__.py +0 -0
- lyrics_transcriber/cli/cli_main.py +285 -0
- lyrics_transcriber/core/__init__.py +0 -0
- lyrics_transcriber/core/config.py +50 -0
- lyrics_transcriber/core/controller.py +520 -0
- lyrics_transcriber/correction/__init__.py +0 -0
- lyrics_transcriber/correction/agentic/__init__.py +9 -0
- lyrics_transcriber/correction/agentic/adapter.py +71 -0
- lyrics_transcriber/correction/agentic/agent.py +313 -0
- lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
- lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
- lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
- lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
- lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
- lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
- lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
- lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
- lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
- lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
- lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
- lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
- lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
- lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
- lyrics_transcriber/correction/agentic/models/enums.py +38 -0
- lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
- lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
- lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
- lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
- lyrics_transcriber/correction/agentic/models/utils.py +19 -0
- lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
- lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
- lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
- lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
- lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
- lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
- lyrics_transcriber/correction/agentic/providers/base.py +36 -0
- lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
- lyrics_transcriber/correction/agentic/providers/config.py +73 -0
- lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
- lyrics_transcriber/correction/agentic/providers/health.py +28 -0
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
- lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
- lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
- lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
- lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
- lyrics_transcriber/correction/agentic/router.py +35 -0
- lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
- lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
- lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
- lyrics_transcriber/correction/anchor_sequence.py +1043 -0
- lyrics_transcriber/correction/corrector.py +760 -0
- lyrics_transcriber/correction/feedback/__init__.py +2 -0
- lyrics_transcriber/correction/feedback/schemas.py +107 -0
- lyrics_transcriber/correction/feedback/store.py +236 -0
- lyrics_transcriber/correction/handlers/__init__.py +0 -0
- lyrics_transcriber/correction/handlers/base.py +52 -0
- lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
- lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
- lyrics_transcriber/correction/handlers/llm.py +293 -0
- lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
- lyrics_transcriber/correction/handlers/repeat.py +88 -0
- lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
- lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
- lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
- lyrics_transcriber/correction/handlers/word_operations.py +187 -0
- lyrics_transcriber/correction/operations.py +352 -0
- lyrics_transcriber/correction/phrase_analyzer.py +435 -0
- lyrics_transcriber/correction/text_utils.py +30 -0
- lyrics_transcriber/frontend/.gitignore +23 -0
- lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
- lyrics_transcriber/frontend/.yarnrc.yml +3 -0
- lyrics_transcriber/frontend/README.md +50 -0
- lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
- lyrics_transcriber/frontend/__init__.py +25 -0
- lyrics_transcriber/frontend/eslint.config.js +28 -0
- lyrics_transcriber/frontend/index.html +18 -0
- lyrics_transcriber/frontend/package.json +42 -0
- lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
- lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
- lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
- lyrics_transcriber/frontend/public/favicon.ico +0 -0
- lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
- lyrics_transcriber/frontend/src/App.tsx +212 -0
- lyrics_transcriber/frontend/src/api.ts +239 -0
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
- lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
- lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
- lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
- lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
- lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
- lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
- lyrics_transcriber/frontend/src/components/Header.tsx +387 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1373 -0
- lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
- lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
- lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +688 -0
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
- lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
- lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
- lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
- lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
- lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
- lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
- lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
- lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
- lyrics_transcriber/frontend/src/main.tsx +17 -0
- lyrics_transcriber/frontend/src/theme.ts +177 -0
- lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
- lyrics_transcriber/frontend/src/types.js +2 -0
- lyrics_transcriber/frontend/src/types.ts +199 -0
- lyrics_transcriber/frontend/src/validation.ts +132 -0
- lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
- lyrics_transcriber/frontend/tsconfig.app.json +26 -0
- lyrics_transcriber/frontend/tsconfig.json +25 -0
- lyrics_transcriber/frontend/tsconfig.node.json +23 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
- lyrics_transcriber/frontend/update_version.js +11 -0
- lyrics_transcriber/frontend/vite.config.d.ts +2 -0
- lyrics_transcriber/frontend/vite.config.js +10 -0
- lyrics_transcriber/frontend/vite.config.ts +11 -0
- lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
- lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js +42039 -0
- lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
- lyrics_transcriber/frontend/web_assets/index.html +18 -0
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
- lyrics_transcriber/frontend/yarn.lock +3752 -0
- lyrics_transcriber/lyrics/__init__.py +0 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
- lyrics_transcriber/lyrics/file_provider.py +95 -0
- lyrics_transcriber/lyrics/genius.py +384 -0
- lyrics_transcriber/lyrics/lrclib.py +231 -0
- lyrics_transcriber/lyrics/musixmatch.py +156 -0
- lyrics_transcriber/lyrics/spotify.py +290 -0
- lyrics_transcriber/lyrics/user_input_provider.py +44 -0
- lyrics_transcriber/output/__init__.py +0 -0
- lyrics_transcriber/output/ass/__init__.py +21 -0
- lyrics_transcriber/output/ass/ass.py +2088 -0
- lyrics_transcriber/output/ass/ass_specs.txt +732 -0
- lyrics_transcriber/output/ass/config.py +180 -0
- lyrics_transcriber/output/ass/constants.py +23 -0
- lyrics_transcriber/output/ass/event.py +94 -0
- lyrics_transcriber/output/ass/formatters.py +132 -0
- lyrics_transcriber/output/ass/lyrics_line.py +265 -0
- lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
- lyrics_transcriber/output/ass/section_detector.py +89 -0
- lyrics_transcriber/output/ass/section_screen.py +106 -0
- lyrics_transcriber/output/ass/style.py +187 -0
- lyrics_transcriber/output/cdg.py +619 -0
- lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
- lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
- lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
- lyrics_transcriber/output/cdgmaker/config.py +151 -0
- lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
- lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
- lyrics_transcriber/output/cdgmaker/pack.py +507 -0
- lyrics_transcriber/output/cdgmaker/render.py +346 -0
- lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
- lyrics_transcriber/output/cdgmaker/utils.py +132 -0
- lyrics_transcriber/output/countdown_processor.py +267 -0
- lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
- lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
- lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/arial.ttf +0 -0
- lyrics_transcriber/output/fonts/georgia.ttf +0 -0
- lyrics_transcriber/output/fonts/verdana.ttf +0 -0
- lyrics_transcriber/output/generator.py +257 -0
- lyrics_transcriber/output/lrc_to_cdg.py +61 -0
- lyrics_transcriber/output/lyrics_file.py +102 -0
- lyrics_transcriber/output/plain_text.py +96 -0
- lyrics_transcriber/output/segment_resizer.py +431 -0
- lyrics_transcriber/output/subtitles.py +397 -0
- lyrics_transcriber/output/video.py +544 -0
- lyrics_transcriber/review/__init__.py +0 -0
- lyrics_transcriber/review/server.py +676 -0
- lyrics_transcriber/storage/__init__.py +0 -0
- lyrics_transcriber/storage/dropbox.py +225 -0
- lyrics_transcriber/transcribers/__init__.py +0 -0
- lyrics_transcriber/transcribers/audioshake.py +290 -0
- lyrics_transcriber/transcribers/base_transcriber.py +157 -0
- lyrics_transcriber/transcribers/whisper.py +330 -0
- lyrics_transcriber/types.py +648 -0
- lyrics_transcriber/utils/__init__.py +0 -0
- lyrics_transcriber/utils/word_utils.py +27 -0
- karaoke_gen-0.57.0.dist-info/METADATA +0 -167
- karaoke_gen-0.57.0.dist-info/RECORD +0 -23
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
from typing import Dict, Any, List, Optional
|
|
7
|
+
|
|
8
|
+
from .providers.base import BaseAIProvider
|
|
9
|
+
from .providers.langchain_bridge import LangChainBridge
|
|
10
|
+
from .providers.config import ProviderConfig
|
|
11
|
+
from .models.schemas import CorrectionProposal, GapClassification, GapCategory
|
|
12
|
+
from .workflows.correction_graph import build_correction_graph
|
|
13
|
+
from .prompts.classifier import build_classification_prompt
|
|
14
|
+
from .handlers.registry import HandlerRegistry
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgenticCorrector:
|
|
20
|
+
"""Main entry for agentic AI correction using LangChain + LangGraph.
|
|
21
|
+
|
|
22
|
+
This orchestrates correction workflows using LangGraph for state management
|
|
23
|
+
and LangChain ChatModels for provider integration. Langfuse tracing is
|
|
24
|
+
automatic via LangChain callbacks.
|
|
25
|
+
|
|
26
|
+
Uses dependency injection for better testability - you can inject a
|
|
27
|
+
mock provider for testing.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
provider: BaseAIProvider,
|
|
33
|
+
graph: Optional[Any] = None,
|
|
34
|
+
langfuse_handler: Optional[Any] = None,
|
|
35
|
+
session_id: Optional[str] = None
|
|
36
|
+
):
|
|
37
|
+
"""Initialize with injected dependencies.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
provider: AI provider implementation (e.g., LangChainBridge)
|
|
41
|
+
graph: Optional LangGraph workflow (builds default if None)
|
|
42
|
+
langfuse_handler: Optional Langfuse callback handler (if None, will try to get from provider)
|
|
43
|
+
session_id: Optional Langfuse session ID to group related traces
|
|
44
|
+
"""
|
|
45
|
+
self._provider = provider
|
|
46
|
+
self._session_id = session_id
|
|
47
|
+
|
|
48
|
+
# Get Langfuse handler from provider if available (avoids duplication)
|
|
49
|
+
self._langfuse_handler = langfuse_handler or self._get_provider_handler()
|
|
50
|
+
|
|
51
|
+
# Build graph with Langfuse callback if available
|
|
52
|
+
self._graph = graph if graph is not None else build_correction_graph(
|
|
53
|
+
callbacks=[self._langfuse_handler] if self._langfuse_handler else None
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _get_provider_handler(self) -> Optional[Any]:
|
|
57
|
+
"""Get Langfuse handler from provider if it has one.
|
|
58
|
+
|
|
59
|
+
This avoids duplicating Langfuse initialization - if the provider
|
|
60
|
+
(e.g., LangChainBridge) already has a handler, we reuse it.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
CallbackHandler instance from provider, or None
|
|
64
|
+
"""
|
|
65
|
+
# Check if provider is LangChainBridge and has a factory
|
|
66
|
+
if hasattr(self._provider, '_factory'):
|
|
67
|
+
factory = self._provider._factory
|
|
68
|
+
|
|
69
|
+
# Force initialization of Langfuse if keys are present
|
|
70
|
+
# This ensures the handler is available when we need it
|
|
71
|
+
if hasattr(factory, '_langfuse_initialized'):
|
|
72
|
+
if not factory._langfuse_initialized:
|
|
73
|
+
# Initialize by calling _create_callbacks (which triggers _initialize_langfuse)
|
|
74
|
+
factory._create_callbacks(self._provider._model)
|
|
75
|
+
|
|
76
|
+
# Now check if handler is available
|
|
77
|
+
if hasattr(factory, '_langfuse_handler'):
|
|
78
|
+
handler = factory._langfuse_handler
|
|
79
|
+
if handler:
|
|
80
|
+
logger.debug("🤖 Reusing Langfuse handler from ModelFactory")
|
|
81
|
+
return handler
|
|
82
|
+
|
|
83
|
+
logger.debug("🤖 No Langfuse handler from provider")
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_model(
|
|
88
|
+
cls,
|
|
89
|
+
model: str,
|
|
90
|
+
config: ProviderConfig | None = None,
|
|
91
|
+
session_id: Optional[str] = None,
|
|
92
|
+
cache_dir: Optional[str] = None
|
|
93
|
+
) -> "AgenticCorrector":
|
|
94
|
+
"""Factory method to create corrector from model specification.
|
|
95
|
+
|
|
96
|
+
This is a convenience method for the common case where you want
|
|
97
|
+
to use LangChainBridge with a model spec string.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
model: Model identifier in format "provider/model"
|
|
101
|
+
config: Optional provider configuration
|
|
102
|
+
session_id: Optional Langfuse session ID to group related traces
|
|
103
|
+
cache_dir: Optional cache directory (uses default if not provided)
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
AgenticCorrector instance with LangChainBridge provider
|
|
107
|
+
"""
|
|
108
|
+
config = config or ProviderConfig.from_env(cache_dir=cache_dir)
|
|
109
|
+
provider = LangChainBridge(model=model, config=config)
|
|
110
|
+
return cls(provider=provider, session_id=session_id)
|
|
111
|
+
|
|
112
|
+
def classify_gap(
|
|
113
|
+
self,
|
|
114
|
+
gap_id: str,
|
|
115
|
+
gap_text: str,
|
|
116
|
+
preceding_words: str,
|
|
117
|
+
following_words: str,
|
|
118
|
+
reference_contexts: Dict[str, str],
|
|
119
|
+
artist: Optional[str] = None,
|
|
120
|
+
title: Optional[str] = None
|
|
121
|
+
) -> Optional[GapClassification]:
|
|
122
|
+
"""Classify a gap using the AI provider.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
gap_id: Unique identifier for the gap
|
|
126
|
+
gap_text: The text of the gap
|
|
127
|
+
preceding_words: Text immediately before the gap
|
|
128
|
+
following_words: Text immediately after the gap
|
|
129
|
+
reference_contexts: Dictionary of reference lyrics from each source
|
|
130
|
+
artist: Song artist name
|
|
131
|
+
title: Song title
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
GapClassification object or None if classification fails
|
|
135
|
+
"""
|
|
136
|
+
# Build classification prompt
|
|
137
|
+
prompt = build_classification_prompt(
|
|
138
|
+
gap_text=gap_text,
|
|
139
|
+
preceding_words=preceding_words,
|
|
140
|
+
following_words=following_words,
|
|
141
|
+
reference_contexts=reference_contexts,
|
|
142
|
+
artist=artist,
|
|
143
|
+
title=title,
|
|
144
|
+
gap_id=gap_id
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Call AI provider to get classification
|
|
148
|
+
try:
|
|
149
|
+
data = self._provider.generate_correction_proposals(
|
|
150
|
+
prompt,
|
|
151
|
+
schema=GapClassification.model_json_schema(),
|
|
152
|
+
session_id=self._session_id
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Extract first result
|
|
156
|
+
if data and len(data) > 0:
|
|
157
|
+
item = data[0]
|
|
158
|
+
if isinstance(item, dict) and "error" not in item:
|
|
159
|
+
classification = GapClassification.model_validate(item)
|
|
160
|
+
logger.debug(f"🤖 Classified gap {gap_id} as {classification.category} (confidence: {classification.confidence})")
|
|
161
|
+
return classification
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.warning(f"🤖 Failed to classify gap {gap_id}: {e}")
|
|
164
|
+
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def propose_for_gap(
|
|
168
|
+
self,
|
|
169
|
+
gap_id: str,
|
|
170
|
+
gap_words: List[Dict[str, Any]],
|
|
171
|
+
preceding_words: str,
|
|
172
|
+
following_words: str,
|
|
173
|
+
reference_contexts: Dict[str, str],
|
|
174
|
+
artist: Optional[str] = None,
|
|
175
|
+
title: Optional[str] = None
|
|
176
|
+
) -> List[CorrectionProposal]:
|
|
177
|
+
"""Generate correction proposals for a gap using two-step classification workflow.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
gap_id: Unique identifier for the gap
|
|
181
|
+
gap_words: List of word dictionaries with id, text, start_time, end_time
|
|
182
|
+
preceding_words: Text immediately before the gap
|
|
183
|
+
following_words: Text immediately after the gap
|
|
184
|
+
reference_contexts: Dictionary of reference lyrics from each source
|
|
185
|
+
artist: Song artist name
|
|
186
|
+
title: Song title
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
List of CorrectionProposal objects
|
|
190
|
+
"""
|
|
191
|
+
# Step 1: Classify the gap
|
|
192
|
+
gap_text = ' '.join(w.get('text', '') for w in gap_words)
|
|
193
|
+
classification = self.classify_gap(
|
|
194
|
+
gap_id=gap_id,
|
|
195
|
+
gap_text=gap_text,
|
|
196
|
+
preceding_words=preceding_words,
|
|
197
|
+
following_words=following_words,
|
|
198
|
+
reference_contexts=reference_contexts,
|
|
199
|
+
artist=artist,
|
|
200
|
+
title=title
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if not classification:
|
|
204
|
+
# Classification failed, flag for human review
|
|
205
|
+
logger.warning(f"🤖 Classification failed for gap {gap_id}, flagging for review")
|
|
206
|
+
return [CorrectionProposal(
|
|
207
|
+
word_ids=[w['id'] for w in gap_words],
|
|
208
|
+
action="Flag",
|
|
209
|
+
confidence=0.0,
|
|
210
|
+
reason="Classification failed - unable to categorize gap",
|
|
211
|
+
requires_human_review=True,
|
|
212
|
+
artist=artist,
|
|
213
|
+
title=title
|
|
214
|
+
)]
|
|
215
|
+
|
|
216
|
+
# Step 2: Route to appropriate handler based on category
|
|
217
|
+
try:
|
|
218
|
+
handler = HandlerRegistry.get_handler(
|
|
219
|
+
category=classification.category,
|
|
220
|
+
artist=artist,
|
|
221
|
+
title=title
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
proposals = handler.handle(
|
|
225
|
+
gap_id=gap_id,
|
|
226
|
+
gap_words=gap_words,
|
|
227
|
+
preceding_words=preceding_words,
|
|
228
|
+
following_words=following_words,
|
|
229
|
+
reference_contexts=reference_contexts,
|
|
230
|
+
classification_reasoning=classification.reasoning
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Add classification metadata to proposals
|
|
234
|
+
for proposal in proposals:
|
|
235
|
+
if not proposal.gap_category:
|
|
236
|
+
proposal.gap_category = classification.category
|
|
237
|
+
if not proposal.artist:
|
|
238
|
+
proposal.artist = artist
|
|
239
|
+
if not proposal.title:
|
|
240
|
+
proposal.title = title
|
|
241
|
+
|
|
242
|
+
return proposals
|
|
243
|
+
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.error(f"🤖 Handler failed for gap {gap_id} (category: {classification.category}): {e}")
|
|
246
|
+
# Handler failed, flag for human review
|
|
247
|
+
return [CorrectionProposal(
|
|
248
|
+
word_ids=[w['id'] for w in gap_words],
|
|
249
|
+
action="Flag",
|
|
250
|
+
confidence=0.0,
|
|
251
|
+
reason=f"Handler error for category {classification.category}: {str(e)}",
|
|
252
|
+
gap_category=classification.category,
|
|
253
|
+
requires_human_review=True,
|
|
254
|
+
artist=artist,
|
|
255
|
+
title=title
|
|
256
|
+
)]
|
|
257
|
+
|
|
258
|
+
def propose(self, prompt: str) -> List[CorrectionProposal]:
|
|
259
|
+
"""Generate correction proposals using LangGraph + LangChain.
|
|
260
|
+
|
|
261
|
+
DEPRECATED: This method uses the old single-step approach.
|
|
262
|
+
Use propose_for_gap() for the new two-step classification workflow.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
prompt: The correction prompt with gap text and reference context
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
List of validated CorrectionProposal objects
|
|
269
|
+
"""
|
|
270
|
+
# Prepare config with session_id in metadata (Langfuse format)
|
|
271
|
+
config = {}
|
|
272
|
+
if self._langfuse_handler:
|
|
273
|
+
config["callbacks"] = [self._langfuse_handler]
|
|
274
|
+
if self._session_id:
|
|
275
|
+
config["metadata"] = {"langfuse_session_id": self._session_id}
|
|
276
|
+
logger.debug(f"🤖 Set Langfuse session_id in metadata: {self._session_id}")
|
|
277
|
+
|
|
278
|
+
# Run LangGraph workflow (with Langfuse tracing if configured)
|
|
279
|
+
if self._graph:
|
|
280
|
+
try:
|
|
281
|
+
self._graph.invoke(
|
|
282
|
+
{"prompt": prompt, "proposals": []},
|
|
283
|
+
config=config
|
|
284
|
+
)
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.debug(f"🤖 LangGraph workflow invocation failed: {e}")
|
|
287
|
+
|
|
288
|
+
# Get proposals from LangChain ChatModel
|
|
289
|
+
# Pass the session_id via metadata to the provider
|
|
290
|
+
data = self._provider.generate_correction_proposals(
|
|
291
|
+
prompt,
|
|
292
|
+
schema=CorrectionProposal.model_json_schema(),
|
|
293
|
+
session_id=self._session_id
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Validate via Pydantic; invalid entries are dropped
|
|
297
|
+
proposals: List[CorrectionProposal] = []
|
|
298
|
+
for item in data:
|
|
299
|
+
# Check if this is an error response from the provider
|
|
300
|
+
if isinstance(item, dict) and "error" in item:
|
|
301
|
+
logger.warning(f"🤖 Provider returned error: {item}")
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
proposals.append(CorrectionProposal.model_validate(item))
|
|
306
|
+
except Exception as e:
|
|
307
|
+
# Log validation errors for debugging
|
|
308
|
+
logger.debug(f"🤖 Failed to validate proposal: {e}, item: {item}")
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
return proposals
|
|
312
|
+
|
|
313
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FeedbackAggregator:
|
|
7
|
+
"""Placeholder for learning data aggregation logic."""
|
|
8
|
+
|
|
9
|
+
def aggregate(self, session_id: str) -> Dict[str, Any]:
|
|
10
|
+
return {"session_id": session_id, "status": "ok"}
|
|
11
|
+
|
|
12
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
from .store import FeedbackStore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FeedbackCollector:
|
|
9
|
+
def __init__(self, store: FeedbackStore | None):
|
|
10
|
+
self._store = store
|
|
11
|
+
|
|
12
|
+
def collect(self, feedback_id: str, session_id: str | None, data_json: str) -> None:
|
|
13
|
+
if not self._store:
|
|
14
|
+
return
|
|
15
|
+
self._store.put_feedback(feedback_id, session_id, data_json)
|
|
16
|
+
|
|
17
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def cleanup_expired(db_path: str, older_than_days: int = 365 * 3) -> int:
|
|
9
|
+
"""Cleanup routine placeholder; returns number of deleted rows.
|
|
10
|
+
|
|
11
|
+
Note: This placeholder assumes `data` JSON contains an ISO timestamp under
|
|
12
|
+
key `createdAt`. For production, store timestamps as columns.
|
|
13
|
+
"""
|
|
14
|
+
threshold = (datetime.utcnow() - timedelta(days=older_than_days)).isoformat()
|
|
15
|
+
with sqlite3.connect(db_path) as conn:
|
|
16
|
+
cur = conn.cursor()
|
|
17
|
+
# Delete sessions and feedback older than threshold by created_at
|
|
18
|
+
cur.execute("DELETE FROM sessions WHERE created_at < ?", (threshold,))
|
|
19
|
+
cur.execute("DELETE FROM feedback WHERE created_at < ?", (threshold,))
|
|
20
|
+
deleted = cur.rowcount
|
|
21
|
+
conn.commit()
|
|
22
|
+
return deleted
|
|
23
|
+
|
|
24
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Any, Iterable, Optional
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FeedbackStore:
|
|
11
|
+
"""SQLite-backed store for sessions, corrections, and feedback.
|
|
12
|
+
|
|
13
|
+
This is a minimal implementation to satisfy contract needs; schema may
|
|
14
|
+
evolve. All operations are simple and synchronous for local usage.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, db_path: str | Path):
|
|
18
|
+
self._db_path = str(db_path)
|
|
19
|
+
self._init()
|
|
20
|
+
|
|
21
|
+
def _init(self) -> None:
|
|
22
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
23
|
+
cur = conn.cursor()
|
|
24
|
+
cur.execute(
|
|
25
|
+
"""
|
|
26
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
data TEXT NOT NULL,
|
|
29
|
+
created_at TEXT NOT NULL
|
|
30
|
+
)
|
|
31
|
+
"""
|
|
32
|
+
)
|
|
33
|
+
cur.execute(
|
|
34
|
+
"""
|
|
35
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
36
|
+
id TEXT PRIMARY KEY,
|
|
37
|
+
session_id TEXT,
|
|
38
|
+
data TEXT NOT NULL,
|
|
39
|
+
created_at TEXT NOT NULL
|
|
40
|
+
)
|
|
41
|
+
"""
|
|
42
|
+
)
|
|
43
|
+
# Attempt to add created_at if upgrading from older schema
|
|
44
|
+
try:
|
|
45
|
+
cur.execute("ALTER TABLE sessions ADD COLUMN created_at TEXT")
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
try:
|
|
49
|
+
cur.execute("ALTER TABLE feedback ADD COLUMN created_at TEXT")
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
conn.commit()
|
|
53
|
+
|
|
54
|
+
def put_session(self, session_id: str, data_json: str) -> None:
|
|
55
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
56
|
+
conn.execute(
|
|
57
|
+
"REPLACE INTO sessions (id, data, created_at) VALUES (?, ?, ?)",
|
|
58
|
+
(session_id, data_json, datetime.utcnow().isoformat()),
|
|
59
|
+
)
|
|
60
|
+
conn.commit()
|
|
61
|
+
|
|
62
|
+
def get_session(self, session_id: str) -> Optional[str]:
|
|
63
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
64
|
+
cur = conn.execute("SELECT data FROM sessions WHERE id = ?", (session_id,))
|
|
65
|
+
row = cur.fetchone()
|
|
66
|
+
return row[0] if row else None
|
|
67
|
+
|
|
68
|
+
def put_feedback(self, feedback_id: str, session_id: Optional[str], data_json: str) -> None:
|
|
69
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
70
|
+
conn.execute(
|
|
71
|
+
"REPLACE INTO feedback (id, session_id, data, created_at) VALUES (?, ?, ?, ?)",
|
|
72
|
+
(feedback_id, session_id, data_json, datetime.utcnow().isoformat()),
|
|
73
|
+
)
|
|
74
|
+
conn.commit()
|
|
75
|
+
|
|
76
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Category-specific handlers for gap correction."""
|
|
2
|
+
|
|
3
|
+
from .base import BaseHandler
|
|
4
|
+
from .punctuation import PunctuationHandler
|
|
5
|
+
from .sound_alike import SoundAlikeHandler
|
|
6
|
+
from .background_vocals import BackgroundVocalsHandler
|
|
7
|
+
from .extra_words import ExtraWordsHandler
|
|
8
|
+
from .repeated_section import RepeatedSectionHandler
|
|
9
|
+
from .complex_multi_error import ComplexMultiErrorHandler
|
|
10
|
+
from .ambiguous import AmbiguousHandler
|
|
11
|
+
from .no_error import NoErrorHandler
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'BaseHandler',
|
|
15
|
+
'PunctuationHandler',
|
|
16
|
+
'SoundAlikeHandler',
|
|
17
|
+
'BackgroundVocalsHandler',
|
|
18
|
+
'ExtraWordsHandler',
|
|
19
|
+
'RepeatedSectionHandler',
|
|
20
|
+
'ComplexMultiErrorHandler',
|
|
21
|
+
'AmbiguousHandler',
|
|
22
|
+
'NoErrorHandler',
|
|
23
|
+
]
|
|
24
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Handler for ambiguous gaps that need human review."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any
|
|
4
|
+
from .base import BaseHandler
|
|
5
|
+
from ..models.schemas import CorrectionProposal, GapCategory
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AmbiguousHandler(BaseHandler):
|
|
9
|
+
"""Handles ambiguous gaps where correct action is unclear without audio."""
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def category(self) -> GapCategory:
|
|
13
|
+
return GapCategory.AMBIGUOUS
|
|
14
|
+
|
|
15
|
+
def handle(
|
|
16
|
+
self,
|
|
17
|
+
gap_id: str,
|
|
18
|
+
gap_words: List[Dict[str, Any]],
|
|
19
|
+
preceding_words: str,
|
|
20
|
+
following_words: str,
|
|
21
|
+
reference_contexts: Dict[str, str],
|
|
22
|
+
classification_reasoning: str = ""
|
|
23
|
+
) -> List[CorrectionProposal]:
|
|
24
|
+
"""Flag ambiguous gaps for human review."""
|
|
25
|
+
|
|
26
|
+
if not gap_words:
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
# Ambiguous cases always require human review with audio
|
|
30
|
+
gap_text = ' '.join(w.get('text', '') for w in gap_words)
|
|
31
|
+
|
|
32
|
+
proposal = CorrectionProposal(
|
|
33
|
+
word_ids=[w['id'] for w in gap_words],
|
|
34
|
+
action="Flag",
|
|
35
|
+
confidence=0.4,
|
|
36
|
+
reason=f"Ambiguous gap: '{gap_text[:100]}...'. Cannot determine correct action without listening to audio. {classification_reasoning}",
|
|
37
|
+
gap_category=self.category,
|
|
38
|
+
requires_human_review=True,
|
|
39
|
+
artist=self.artist,
|
|
40
|
+
title=self.title
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return [proposal]
|
|
44
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Handler for background vocals that should be removed."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any
|
|
4
|
+
from .base import BaseHandler
|
|
5
|
+
from ..models.schemas import CorrectionProposal, GapCategory
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BackgroundVocalsHandler(BaseHandler):
|
|
9
|
+
"""Handles gaps containing background vocals (usually in parentheses)."""
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def category(self) -> GapCategory:
|
|
13
|
+
return GapCategory.BACKGROUND_VOCALS
|
|
14
|
+
|
|
15
|
+
def handle(
|
|
16
|
+
self,
|
|
17
|
+
gap_id: str,
|
|
18
|
+
gap_words: List[Dict[str, Any]],
|
|
19
|
+
preceding_words: str,
|
|
20
|
+
following_words: str,
|
|
21
|
+
reference_contexts: Dict[str, str],
|
|
22
|
+
classification_reasoning: str = ""
|
|
23
|
+
) -> List[CorrectionProposal]:
|
|
24
|
+
"""Propose deletion of words in parentheses."""
|
|
25
|
+
|
|
26
|
+
if not gap_words:
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
proposals = []
|
|
30
|
+
|
|
31
|
+
# Find words that are in parentheses or are parentheses themselves
|
|
32
|
+
words_to_delete = []
|
|
33
|
+
for word in gap_words:
|
|
34
|
+
text = word.get('text', '')
|
|
35
|
+
# Check if word has parentheses or is just parentheses
|
|
36
|
+
if '(' in text or ')' in text:
|
|
37
|
+
words_to_delete.append(word)
|
|
38
|
+
|
|
39
|
+
if words_to_delete:
|
|
40
|
+
# Create delete proposals for parenthesized content
|
|
41
|
+
proposal = CorrectionProposal(
|
|
42
|
+
word_ids=[w['id'] for w in words_to_delete],
|
|
43
|
+
action="DeleteWord",
|
|
44
|
+
confidence=0.85,
|
|
45
|
+
reason=f"Background vocals in parentheses, not in reference lyrics. {classification_reasoning}",
|
|
46
|
+
gap_category=self.category,
|
|
47
|
+
requires_human_review=False,
|
|
48
|
+
artist=self.artist,
|
|
49
|
+
title=self.title
|
|
50
|
+
)
|
|
51
|
+
proposals.append(proposal)
|
|
52
|
+
else:
|
|
53
|
+
# If no parentheses found but classified as background vocals,
|
|
54
|
+
# flag for review as classifier may have other reasoning
|
|
55
|
+
proposal = CorrectionProposal(
|
|
56
|
+
word_ids=[w['id'] for w in gap_words],
|
|
57
|
+
action="Flag",
|
|
58
|
+
confidence=0.6,
|
|
59
|
+
reason=f"Classified as background vocals but no parentheses found. {classification_reasoning}",
|
|
60
|
+
gap_category=self.category,
|
|
61
|
+
requires_human_review=True,
|
|
62
|
+
artist=self.artist,
|
|
63
|
+
title=self.title
|
|
64
|
+
)
|
|
65
|
+
proposals.append(proposal)
|
|
66
|
+
|
|
67
|
+
return proposals
|
|
68
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Base handler interface for gap correction."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import List, Dict, Any
|
|
5
|
+
from ..models.schemas import CorrectionProposal, GapCategory
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseHandler(ABC):
|
|
9
|
+
"""Base class for category-specific correction handlers."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, artist: str = None, title: str = None):
|
|
12
|
+
"""Initialize handler with song metadata.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
artist: Song artist name
|
|
16
|
+
title: Song title
|
|
17
|
+
"""
|
|
18
|
+
self.artist = artist
|
|
19
|
+
self.title = title
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def handle(
|
|
23
|
+
self,
|
|
24
|
+
gap_id: str,
|
|
25
|
+
gap_words: List[Dict[str, Any]],
|
|
26
|
+
preceding_words: str,
|
|
27
|
+
following_words: str,
|
|
28
|
+
reference_contexts: Dict[str, str],
|
|
29
|
+
classification_reasoning: str = ""
|
|
30
|
+
) -> List[CorrectionProposal]:
|
|
31
|
+
"""Process a gap and return correction proposals.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
gap_id: Unique identifier for the gap
|
|
35
|
+
gap_words: List of word dictionaries with id, text, start_time, end_time
|
|
36
|
+
preceding_words: Context before the gap
|
|
37
|
+
following_words: Context after the gap
|
|
38
|
+
reference_contexts: Dictionary of reference lyrics by source
|
|
39
|
+
classification_reasoning: Reasoning from the classifier
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
List of CorrectionProposal objects
|
|
43
|
+
"""
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def category(self) -> GapCategory:
|
|
49
|
+
"""Return the gap category this handler processes."""
|
|
50
|
+
raise NotImplementedError
|
|
51
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Handler for complex gaps with multiple error types."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any
|
|
4
|
+
from .base import BaseHandler
|
|
5
|
+
from ..models.schemas import CorrectionProposal, GapCategory
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ComplexMultiErrorHandler(BaseHandler):
|
|
9
|
+
"""Handles large, complex gaps with multiple types of errors."""
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def category(self) -> GapCategory:
|
|
13
|
+
return GapCategory.COMPLEX_MULTI_ERROR
|
|
14
|
+
|
|
15
|
+
def handle(
|
|
16
|
+
self,
|
|
17
|
+
gap_id: str,
|
|
18
|
+
gap_words: List[Dict[str, Any]],
|
|
19
|
+
preceding_words: str,
|
|
20
|
+
following_words: str,
|
|
21
|
+
reference_contexts: Dict[str, str],
|
|
22
|
+
classification_reasoning: str = ""
|
|
23
|
+
) -> List[CorrectionProposal]:
|
|
24
|
+
"""Flag complex gaps for human review."""
|
|
25
|
+
|
|
26
|
+
if not gap_words:
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
# Complex multi-error gaps are too difficult for automatic correction
|
|
30
|
+
# Always flag for human review
|
|
31
|
+
gap_text = ' '.join(w.get('text', '') for w in gap_words)
|
|
32
|
+
word_count = len(gap_words)
|
|
33
|
+
|
|
34
|
+
proposal = CorrectionProposal(
|
|
35
|
+
word_ids=[w['id'] for w in gap_words],
|
|
36
|
+
action="Flag",
|
|
37
|
+
confidence=0.3,
|
|
38
|
+
reason=f"Complex gap with {word_count} words and multiple error types: '{gap_text[:100]}...'. Too complex for automatic correction. {classification_reasoning}",
|
|
39
|
+
gap_category=self.category,
|
|
40
|
+
requires_human_review=True,
|
|
41
|
+
artist=self.artist,
|
|
42
|
+
title=self.title
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return [proposal]
|
|
46
|
+
|