karaoke-gen 0.75.54__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.

Potentially problematic release.


This version of karaoke-gen might be problematic. Click here for more details.

Files changed (287) hide show
  1. karaoke_gen/__init__.py +38 -0
  2. karaoke_gen/audio_fetcher.py +1614 -0
  3. karaoke_gen/audio_processor.py +790 -0
  4. karaoke_gen/config.py +83 -0
  5. karaoke_gen/file_handler.py +387 -0
  6. karaoke_gen/instrumental_review/__init__.py +45 -0
  7. karaoke_gen/instrumental_review/analyzer.py +408 -0
  8. karaoke_gen/instrumental_review/editor.py +322 -0
  9. karaoke_gen/instrumental_review/models.py +171 -0
  10. karaoke_gen/instrumental_review/server.py +475 -0
  11. karaoke_gen/instrumental_review/static/index.html +1529 -0
  12. karaoke_gen/instrumental_review/waveform.py +409 -0
  13. karaoke_gen/karaoke_finalise/__init__.py +1 -0
  14. karaoke_gen/karaoke_finalise/karaoke_finalise.py +1833 -0
  15. karaoke_gen/karaoke_gen.py +1026 -0
  16. karaoke_gen/lyrics_processor.py +474 -0
  17. karaoke_gen/metadata.py +160 -0
  18. karaoke_gen/pipeline/__init__.py +87 -0
  19. karaoke_gen/pipeline/base.py +215 -0
  20. karaoke_gen/pipeline/context.py +230 -0
  21. karaoke_gen/pipeline/executors/__init__.py +21 -0
  22. karaoke_gen/pipeline/executors/local.py +159 -0
  23. karaoke_gen/pipeline/executors/remote.py +257 -0
  24. karaoke_gen/pipeline/stages/__init__.py +27 -0
  25. karaoke_gen/pipeline/stages/finalize.py +202 -0
  26. karaoke_gen/pipeline/stages/render.py +165 -0
  27. karaoke_gen/pipeline/stages/screens.py +139 -0
  28. karaoke_gen/pipeline/stages/separation.py +191 -0
  29. karaoke_gen/pipeline/stages/transcription.py +191 -0
  30. karaoke_gen/resources/AvenirNext-Bold.ttf +0 -0
  31. karaoke_gen/resources/Montserrat-Bold.ttf +0 -0
  32. karaoke_gen/resources/Oswald-Bold.ttf +0 -0
  33. karaoke_gen/resources/Oswald-SemiBold.ttf +0 -0
  34. karaoke_gen/resources/Zurich_Cn_BT_Bold.ttf +0 -0
  35. karaoke_gen/style_loader.py +531 -0
  36. karaoke_gen/utils/__init__.py +18 -0
  37. karaoke_gen/utils/bulk_cli.py +492 -0
  38. karaoke_gen/utils/cli_args.py +432 -0
  39. karaoke_gen/utils/gen_cli.py +978 -0
  40. karaoke_gen/utils/remote_cli.py +3268 -0
  41. karaoke_gen/video_background_processor.py +351 -0
  42. karaoke_gen/video_generator.py +424 -0
  43. karaoke_gen-0.75.54.dist-info/METADATA +718 -0
  44. karaoke_gen-0.75.54.dist-info/RECORD +287 -0
  45. karaoke_gen-0.75.54.dist-info/WHEEL +4 -0
  46. karaoke_gen-0.75.54.dist-info/entry_points.txt +5 -0
  47. karaoke_gen-0.75.54.dist-info/licenses/LICENSE +21 -0
  48. lyrics_transcriber/__init__.py +10 -0
  49. lyrics_transcriber/cli/__init__.py +0 -0
  50. lyrics_transcriber/cli/cli_main.py +285 -0
  51. lyrics_transcriber/core/__init__.py +0 -0
  52. lyrics_transcriber/core/config.py +50 -0
  53. lyrics_transcriber/core/controller.py +594 -0
  54. lyrics_transcriber/correction/__init__.py +0 -0
  55. lyrics_transcriber/correction/agentic/__init__.py +9 -0
  56. lyrics_transcriber/correction/agentic/adapter.py +71 -0
  57. lyrics_transcriber/correction/agentic/agent.py +313 -0
  58. lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
  59. lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
  60. lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
  61. lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
  62. lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
  63. lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
  64. lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
  65. lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
  66. lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
  67. lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
  68. lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
  69. lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
  70. lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
  71. lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
  72. lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
  73. lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
  74. lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
  75. lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
  76. lyrics_transcriber/correction/agentic/models/enums.py +38 -0
  77. lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
  78. lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
  79. lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
  80. lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
  81. lyrics_transcriber/correction/agentic/models/utils.py +19 -0
  82. lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
  83. lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
  84. lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
  85. lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
  86. lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
  87. lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
  88. lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
  89. lyrics_transcriber/correction/agentic/providers/base.py +36 -0
  90. lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
  91. lyrics_transcriber/correction/agentic/providers/config.py +73 -0
  92. lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
  93. lyrics_transcriber/correction/agentic/providers/health.py +28 -0
  94. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
  95. lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
  96. lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
  97. lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
  98. lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
  99. lyrics_transcriber/correction/agentic/router.py +35 -0
  100. lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
  101. lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
  102. lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
  103. lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
  104. lyrics_transcriber/correction/anchor_sequence.py +919 -0
  105. lyrics_transcriber/correction/corrector.py +760 -0
  106. lyrics_transcriber/correction/feedback/__init__.py +2 -0
  107. lyrics_transcriber/correction/feedback/schemas.py +107 -0
  108. lyrics_transcriber/correction/feedback/store.py +236 -0
  109. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  110. lyrics_transcriber/correction/handlers/base.py +52 -0
  111. lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
  112. lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
  113. lyrics_transcriber/correction/handlers/llm.py +293 -0
  114. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  115. lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
  116. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
  117. lyrics_transcriber/correction/handlers/repeat.py +88 -0
  118. lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
  119. lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
  120. lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
  121. lyrics_transcriber/correction/handlers/word_operations.py +187 -0
  122. lyrics_transcriber/correction/operations.py +352 -0
  123. lyrics_transcriber/correction/phrase_analyzer.py +435 -0
  124. lyrics_transcriber/correction/text_utils.py +30 -0
  125. lyrics_transcriber/frontend/.gitignore +23 -0
  126. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
  127. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  128. lyrics_transcriber/frontend/README.md +50 -0
  129. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
  130. lyrics_transcriber/frontend/__init__.py +25 -0
  131. lyrics_transcriber/frontend/eslint.config.js +28 -0
  132. lyrics_transcriber/frontend/index.html +18 -0
  133. lyrics_transcriber/frontend/package.json +42 -0
  134. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  135. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  136. lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
  137. lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
  138. lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
  139. lyrics_transcriber/frontend/public/favicon.ico +0 -0
  140. lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
  141. lyrics_transcriber/frontend/src/App.tsx +214 -0
  142. lyrics_transcriber/frontend/src/api.ts +254 -0
  143. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
  144. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  145. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
  146. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
  147. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
  148. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
  149. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
  150. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
  151. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
  152. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  153. lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
  154. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
  155. lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
  156. lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
  157. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  158. lyrics_transcriber/frontend/src/components/Header.tsx +413 -0
  159. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1387 -0
  160. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +185 -0
  161. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +704 -0
  162. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +80 -0
  163. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +905 -0
  164. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
  165. lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +127 -0
  166. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
  167. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
  168. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
  169. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
  170. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +336 -0
  171. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
  172. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
  173. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
  174. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
  175. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
  176. lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
  177. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
  178. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
  179. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
  180. lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
  181. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
  182. lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
  183. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  184. lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
  185. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
  186. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  187. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
  188. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
  189. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
  190. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  191. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
  192. lyrics_transcriber/frontend/src/main.tsx +17 -0
  193. lyrics_transcriber/frontend/src/theme.ts +177 -0
  194. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  195. lyrics_transcriber/frontend/src/types.js +2 -0
  196. lyrics_transcriber/frontend/src/types.ts +199 -0
  197. lyrics_transcriber/frontend/src/validation.ts +132 -0
  198. lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
  199. lyrics_transcriber/frontend/tsconfig.app.json +26 -0
  200. lyrics_transcriber/frontend/tsconfig.json +25 -0
  201. lyrics_transcriber/frontend/tsconfig.node.json +23 -0
  202. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
  203. lyrics_transcriber/frontend/update_version.js +11 -0
  204. lyrics_transcriber/frontend/vite.config.d.ts +2 -0
  205. lyrics_transcriber/frontend/vite.config.js +10 -0
  206. lyrics_transcriber/frontend/vite.config.ts +11 -0
  207. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  208. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  209. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  210. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js +43288 -0
  211. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +1 -0
  212. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  213. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  214. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  215. lyrics_transcriber/frontend/web_assets/index.html +18 -0
  216. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  217. lyrics_transcriber/frontend/yarn.lock +3752 -0
  218. lyrics_transcriber/lyrics/__init__.py +0 -0
  219. lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
  220. lyrics_transcriber/lyrics/file_provider.py +95 -0
  221. lyrics_transcriber/lyrics/genius.py +384 -0
  222. lyrics_transcriber/lyrics/lrclib.py +231 -0
  223. lyrics_transcriber/lyrics/musixmatch.py +156 -0
  224. lyrics_transcriber/lyrics/spotify.py +290 -0
  225. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  226. lyrics_transcriber/output/__init__.py +0 -0
  227. lyrics_transcriber/output/ass/__init__.py +21 -0
  228. lyrics_transcriber/output/ass/ass.py +2088 -0
  229. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  230. lyrics_transcriber/output/ass/config.py +180 -0
  231. lyrics_transcriber/output/ass/constants.py +23 -0
  232. lyrics_transcriber/output/ass/event.py +94 -0
  233. lyrics_transcriber/output/ass/formatters.py +132 -0
  234. lyrics_transcriber/output/ass/lyrics_line.py +265 -0
  235. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  236. lyrics_transcriber/output/ass/section_detector.py +89 -0
  237. lyrics_transcriber/output/ass/section_screen.py +106 -0
  238. lyrics_transcriber/output/ass/style.py +187 -0
  239. lyrics_transcriber/output/cdg.py +619 -0
  240. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  241. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  242. lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
  243. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  244. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  245. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  246. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  247. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  248. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  249. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  250. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  251. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  252. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  253. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  254. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  255. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  256. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  257. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  258. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  259. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  260. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  261. lyrics_transcriber/output/countdown_processor.py +306 -0
  262. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  263. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  264. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  265. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  266. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  267. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  268. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  269. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  270. lyrics_transcriber/output/generator.py +257 -0
  271. lyrics_transcriber/output/lrc_to_cdg.py +61 -0
  272. lyrics_transcriber/output/lyrics_file.py +102 -0
  273. lyrics_transcriber/output/plain_text.py +96 -0
  274. lyrics_transcriber/output/segment_resizer.py +431 -0
  275. lyrics_transcriber/output/subtitles.py +397 -0
  276. lyrics_transcriber/output/video.py +544 -0
  277. lyrics_transcriber/review/__init__.py +0 -0
  278. lyrics_transcriber/review/server.py +676 -0
  279. lyrics_transcriber/storage/__init__.py +0 -0
  280. lyrics_transcriber/storage/dropbox.py +225 -0
  281. lyrics_transcriber/transcribers/__init__.py +0 -0
  282. lyrics_transcriber/transcribers/audioshake.py +379 -0
  283. lyrics_transcriber/transcribers/base_transcriber.py +157 -0
  284. lyrics_transcriber/transcribers/whisper.py +330 -0
  285. lyrics_transcriber/types.py +650 -0
  286. lyrics_transcriber/utils/__init__.py +0 -0
  287. lyrics_transcriber/utils/word_utils.py +27 -0
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, Any, List
4
+
5
+ from .models.schemas import CorrectionProposal
6
+ from lyrics_transcriber.types import WordCorrection, Word
7
+ from lyrics_transcriber.utils.word_utils import WordUtils
8
+
9
+
10
+ def adapt_proposals_to_word_corrections(
11
+ proposals: List[CorrectionProposal],
12
+ word_map: Dict[str, Word],
13
+ linear_position_map: Dict[str, int],
14
+ ) -> List[WordCorrection]:
15
+ """Convert CorrectionProposal items into WordCorrection objects.
16
+
17
+ Minimal mapping: supports ReplaceWord and DeleteWord actions with single word_id.
18
+ Unknown or unsupported actions are ignored.
19
+
20
+ The reason field includes gap category and confidence for better UI feedback.
21
+ """
22
+ results: List[WordCorrection] = []
23
+ for p in proposals:
24
+ action = (p.action or "").lower()
25
+ target_id = p.word_id or (p.word_ids[0] if p.word_ids else None)
26
+ if not target_id or target_id not in word_map:
27
+ continue
28
+ original = word_map[target_id]
29
+ original_position = linear_position_map.get(target_id, 0)
30
+
31
+ # Build a detailed reason including gap category
32
+ category_str = f" [{p.gap_category.value}]" if p.gap_category else ""
33
+ confidence_str = f" (confidence: {p.confidence:.0%})" if p.confidence else ""
34
+ detailed_reason = f"{p.reason or 'AI correction'}{category_str}{confidence_str}"
35
+
36
+ if action == "replaceword" and p.replacement_text:
37
+ results.append(
38
+ WordCorrection(
39
+ original_word=original.text,
40
+ corrected_word=p.replacement_text,
41
+ original_position=original_position,
42
+ source="agentic",
43
+ reason=detailed_reason,
44
+ confidence=float(p.confidence or 0.0),
45
+ is_deletion=False,
46
+ word_id=target_id,
47
+ corrected_word_id=WordUtils.generate_id(), # Generate unique ID for corrected word
48
+ handler="AgenticCorrector", # Required by frontend
49
+ reference_positions={}, # Required by frontend
50
+ )
51
+ )
52
+ elif action == "deleteword":
53
+ results.append(
54
+ WordCorrection(
55
+ original_word=original.text,
56
+ corrected_word="",
57
+ original_position=original_position,
58
+ source="agentic",
59
+ reason=detailed_reason,
60
+ confidence=float(p.confidence or 0.0),
61
+ is_deletion=True,
62
+ word_id=target_id,
63
+ corrected_word_id=None, # Deleted words don't need a corrected ID
64
+ handler="AgenticCorrector", # Required by frontend
65
+ reference_positions={}, # Required by frontend
66
+ )
67
+ )
68
+
69
+ return results
70
+
71
+
@@ -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
+