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.
Files changed (268) hide show
  1. karaoke_gen/audio_fetcher.py +461 -0
  2. karaoke_gen/audio_processor.py +407 -30
  3. karaoke_gen/config.py +62 -113
  4. karaoke_gen/file_handler.py +32 -59
  5. karaoke_gen/karaoke_finalise/karaoke_finalise.py +148 -67
  6. karaoke_gen/karaoke_gen.py +270 -61
  7. karaoke_gen/lyrics_processor.py +13 -1
  8. karaoke_gen/metadata.py +78 -73
  9. karaoke_gen/pipeline/__init__.py +87 -0
  10. karaoke_gen/pipeline/base.py +215 -0
  11. karaoke_gen/pipeline/context.py +230 -0
  12. karaoke_gen/pipeline/executors/__init__.py +21 -0
  13. karaoke_gen/pipeline/executors/local.py +159 -0
  14. karaoke_gen/pipeline/executors/remote.py +257 -0
  15. karaoke_gen/pipeline/stages/__init__.py +27 -0
  16. karaoke_gen/pipeline/stages/finalize.py +202 -0
  17. karaoke_gen/pipeline/stages/render.py +165 -0
  18. karaoke_gen/pipeline/stages/screens.py +139 -0
  19. karaoke_gen/pipeline/stages/separation.py +191 -0
  20. karaoke_gen/pipeline/stages/transcription.py +191 -0
  21. karaoke_gen/style_loader.py +531 -0
  22. karaoke_gen/utils/bulk_cli.py +6 -0
  23. karaoke_gen/utils/cli_args.py +424 -0
  24. karaoke_gen/utils/gen_cli.py +26 -261
  25. karaoke_gen/utils/remote_cli.py +1815 -0
  26. karaoke_gen/video_background_processor.py +351 -0
  27. karaoke_gen-0.71.23.dist-info/METADATA +610 -0
  28. karaoke_gen-0.71.23.dist-info/RECORD +275 -0
  29. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/WHEEL +1 -1
  30. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/entry_points.txt +1 -0
  31. lyrics_transcriber/__init__.py +10 -0
  32. lyrics_transcriber/cli/__init__.py +0 -0
  33. lyrics_transcriber/cli/cli_main.py +285 -0
  34. lyrics_transcriber/core/__init__.py +0 -0
  35. lyrics_transcriber/core/config.py +50 -0
  36. lyrics_transcriber/core/controller.py +520 -0
  37. lyrics_transcriber/correction/__init__.py +0 -0
  38. lyrics_transcriber/correction/agentic/__init__.py +9 -0
  39. lyrics_transcriber/correction/agentic/adapter.py +71 -0
  40. lyrics_transcriber/correction/agentic/agent.py +313 -0
  41. lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
  42. lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
  43. lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
  44. lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
  45. lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
  46. lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
  47. lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
  48. lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
  49. lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
  50. lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
  51. lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
  52. lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
  53. lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
  54. lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
  55. lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
  56. lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
  57. lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
  58. lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
  59. lyrics_transcriber/correction/agentic/models/enums.py +38 -0
  60. lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
  61. lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
  62. lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
  63. lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
  64. lyrics_transcriber/correction/agentic/models/utils.py +19 -0
  65. lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
  66. lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
  67. lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
  68. lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
  69. lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
  70. lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
  71. lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
  72. lyrics_transcriber/correction/agentic/providers/base.py +36 -0
  73. lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
  74. lyrics_transcriber/correction/agentic/providers/config.py +73 -0
  75. lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
  76. lyrics_transcriber/correction/agentic/providers/health.py +28 -0
  77. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
  78. lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
  79. lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
  80. lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
  81. lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
  82. lyrics_transcriber/correction/agentic/router.py +35 -0
  83. lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
  84. lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
  85. lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
  86. lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
  87. lyrics_transcriber/correction/anchor_sequence.py +1043 -0
  88. lyrics_transcriber/correction/corrector.py +760 -0
  89. lyrics_transcriber/correction/feedback/__init__.py +2 -0
  90. lyrics_transcriber/correction/feedback/schemas.py +107 -0
  91. lyrics_transcriber/correction/feedback/store.py +236 -0
  92. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  93. lyrics_transcriber/correction/handlers/base.py +52 -0
  94. lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
  95. lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
  96. lyrics_transcriber/correction/handlers/llm.py +293 -0
  97. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  98. lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
  99. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
  100. lyrics_transcriber/correction/handlers/repeat.py +88 -0
  101. lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
  102. lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
  103. lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
  104. lyrics_transcriber/correction/handlers/word_operations.py +187 -0
  105. lyrics_transcriber/correction/operations.py +352 -0
  106. lyrics_transcriber/correction/phrase_analyzer.py +435 -0
  107. lyrics_transcriber/correction/text_utils.py +30 -0
  108. lyrics_transcriber/frontend/.gitignore +23 -0
  109. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
  110. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  111. lyrics_transcriber/frontend/README.md +50 -0
  112. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
  113. lyrics_transcriber/frontend/__init__.py +25 -0
  114. lyrics_transcriber/frontend/eslint.config.js +28 -0
  115. lyrics_transcriber/frontend/index.html +18 -0
  116. lyrics_transcriber/frontend/package.json +42 -0
  117. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  118. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  119. lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
  120. lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
  121. lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
  122. lyrics_transcriber/frontend/public/favicon.ico +0 -0
  123. lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
  124. lyrics_transcriber/frontend/src/App.tsx +212 -0
  125. lyrics_transcriber/frontend/src/api.ts +239 -0
  126. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
  127. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  128. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
  129. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
  130. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
  131. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
  132. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
  133. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
  134. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
  135. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  136. lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
  137. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
  138. lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
  139. lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
  140. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  141. lyrics_transcriber/frontend/src/components/Header.tsx +387 -0
  142. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1373 -0
  143. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
  144. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
  145. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
  146. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
  147. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
  148. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +688 -0
  149. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
  150. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
  151. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
  152. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
  153. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
  154. lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
  155. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
  156. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
  157. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
  158. lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
  159. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
  160. lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
  161. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  162. lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
  163. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
  164. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  165. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
  166. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
  167. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
  168. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  169. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
  170. lyrics_transcriber/frontend/src/main.tsx +17 -0
  171. lyrics_transcriber/frontend/src/theme.ts +177 -0
  172. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  173. lyrics_transcriber/frontend/src/types.js +2 -0
  174. lyrics_transcriber/frontend/src/types.ts +199 -0
  175. lyrics_transcriber/frontend/src/validation.ts +132 -0
  176. lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
  177. lyrics_transcriber/frontend/tsconfig.app.json +26 -0
  178. lyrics_transcriber/frontend/tsconfig.json +25 -0
  179. lyrics_transcriber/frontend/tsconfig.node.json +23 -0
  180. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
  181. lyrics_transcriber/frontend/update_version.js +11 -0
  182. lyrics_transcriber/frontend/vite.config.d.ts +2 -0
  183. lyrics_transcriber/frontend/vite.config.js +10 -0
  184. lyrics_transcriber/frontend/vite.config.ts +11 -0
  185. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  186. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  187. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  188. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js +42039 -0
  189. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js.map +1 -0
  190. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  191. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  192. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  193. lyrics_transcriber/frontend/web_assets/index.html +18 -0
  194. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  195. lyrics_transcriber/frontend/yarn.lock +3752 -0
  196. lyrics_transcriber/lyrics/__init__.py +0 -0
  197. lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
  198. lyrics_transcriber/lyrics/file_provider.py +95 -0
  199. lyrics_transcriber/lyrics/genius.py +384 -0
  200. lyrics_transcriber/lyrics/lrclib.py +231 -0
  201. lyrics_transcriber/lyrics/musixmatch.py +156 -0
  202. lyrics_transcriber/lyrics/spotify.py +290 -0
  203. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  204. lyrics_transcriber/output/__init__.py +0 -0
  205. lyrics_transcriber/output/ass/__init__.py +21 -0
  206. lyrics_transcriber/output/ass/ass.py +2088 -0
  207. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  208. lyrics_transcriber/output/ass/config.py +180 -0
  209. lyrics_transcriber/output/ass/constants.py +23 -0
  210. lyrics_transcriber/output/ass/event.py +94 -0
  211. lyrics_transcriber/output/ass/formatters.py +132 -0
  212. lyrics_transcriber/output/ass/lyrics_line.py +265 -0
  213. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  214. lyrics_transcriber/output/ass/section_detector.py +89 -0
  215. lyrics_transcriber/output/ass/section_screen.py +106 -0
  216. lyrics_transcriber/output/ass/style.py +187 -0
  217. lyrics_transcriber/output/cdg.py +619 -0
  218. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  219. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  220. lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
  221. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  222. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  223. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  224. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  225. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  226. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  227. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  228. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  229. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  230. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  231. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  232. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  233. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  234. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  235. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  236. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  237. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  238. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  239. lyrics_transcriber/output/countdown_processor.py +267 -0
  240. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  241. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  242. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  243. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  244. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  245. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  246. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  247. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  248. lyrics_transcriber/output/generator.py +257 -0
  249. lyrics_transcriber/output/lrc_to_cdg.py +61 -0
  250. lyrics_transcriber/output/lyrics_file.py +102 -0
  251. lyrics_transcriber/output/plain_text.py +96 -0
  252. lyrics_transcriber/output/segment_resizer.py +431 -0
  253. lyrics_transcriber/output/subtitles.py +397 -0
  254. lyrics_transcriber/output/video.py +544 -0
  255. lyrics_transcriber/review/__init__.py +0 -0
  256. lyrics_transcriber/review/server.py +676 -0
  257. lyrics_transcriber/storage/__init__.py +0 -0
  258. lyrics_transcriber/storage/dropbox.py +225 -0
  259. lyrics_transcriber/transcribers/__init__.py +0 -0
  260. lyrics_transcriber/transcribers/audioshake.py +290 -0
  261. lyrics_transcriber/transcribers/base_transcriber.py +157 -0
  262. lyrics_transcriber/transcribers/whisper.py +330 -0
  263. lyrics_transcriber/types.py +648 -0
  264. lyrics_transcriber/utils/__init__.py +0 -0
  265. lyrics_transcriber/utils/word_utils.py +27 -0
  266. karaoke_gen-0.57.0.dist-info/METADATA +0 -167
  267. karaoke_gen-0.57.0.dist-info/RECORD +0 -23
  268. {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
+