karaoke-gen 0.57.0__py3-none-any.whl → 0.71.27__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 +1965 -0
  26. karaoke_gen/video_background_processor.py +351 -0
  27. karaoke_gen-0.71.27.dist-info/METADATA +610 -0
  28. karaoke_gen-0.71.27.dist-info/RECORD +275 -0
  29. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.dist-info}/WHEEL +1 -1
  30. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.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.27.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,50 @@
1
+ import os
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, Dict, Optional
4
+
5
+
6
+ @dataclass
7
+ class TranscriberConfig:
8
+ """Configuration for transcription services."""
9
+
10
+ audioshake_api_token: Optional[str] = None
11
+ runpod_api_key: Optional[str] = None
12
+ whisper_runpod_id: Optional[str] = None
13
+
14
+
15
+ @dataclass
16
+ class LyricsConfig:
17
+ """Configuration for lyrics services."""
18
+
19
+ genius_api_token: Optional[str] = None
20
+ rapidapi_key: Optional[str] = None
21
+ spotify_cookie: Optional[str] = None
22
+ lyrics_file: Optional[str] = None
23
+
24
+ @dataclass
25
+ class OutputConfig:
26
+ """Configuration for output generation."""
27
+
28
+ output_styles_json: str
29
+ default_max_line_length: int = 36
30
+ styles: Dict[str, Any] = field(default_factory=dict)
31
+ output_dir: Optional[str] = os.getcwd()
32
+ cache_dir: str = os.getenv(
33
+ "LYRICS_TRANSCRIBER_CACHE_DIR",
34
+ os.path.join(os.path.expanduser("~"), "lyrics-transcriber-cache")
35
+ )
36
+
37
+ fetch_lyrics: bool = True
38
+ run_transcription: bool = True
39
+ run_correction: bool = True
40
+ enable_review: bool = True
41
+
42
+ generate_plain_text: bool = True
43
+ generate_lrc: bool = True
44
+ generate_cdg: bool = True
45
+ render_video: bool = True
46
+ video_resolution: str = "360p"
47
+ subtitle_offset_ms: int = 0
48
+
49
+ # Countdown feature for songs that start too quickly
50
+ add_countdown: bool = True
@@ -0,0 +1,520 @@
1
+ import os
2
+ import logging
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from typing import Dict, Optional, List
6
+ from lyrics_transcriber.types import LyricsData, TranscriptionResult, CorrectionResult
7
+ from lyrics_transcriber.transcribers.base_transcriber import BaseTranscriber
8
+ from lyrics_transcriber.transcribers.audioshake import AudioShakeTranscriber, AudioShakeConfig
9
+ from lyrics_transcriber.transcribers.whisper import WhisperTranscriber, WhisperConfig
10
+ from lyrics_transcriber.lyrics.base_lyrics_provider import BaseLyricsProvider, LyricsProviderConfig
11
+ from lyrics_transcriber.lyrics.genius import GeniusProvider
12
+ from lyrics_transcriber.lyrics.spotify import SpotifyProvider
13
+ from lyrics_transcriber.lyrics.musixmatch import MusixmatchProvider
14
+ from lyrics_transcriber.lyrics.lrclib import LRCLIBProvider
15
+ from lyrics_transcriber.output.generator import OutputGenerator
16
+ from lyrics_transcriber.correction.corrector import LyricsCorrector
17
+ from lyrics_transcriber.core.config import TranscriberConfig, LyricsConfig, OutputConfig
18
+ from lyrics_transcriber.lyrics.file_provider import FileProvider
19
+
20
+
21
+ @dataclass
22
+ class LyricsControllerResult:
23
+ """Holds the results of the transcription and correction process."""
24
+
25
+ # Results from different sources
26
+ lyrics_results: dict[str, LyricsData] = field(default_factory=dict)
27
+ transcription_results: List[TranscriptionResult] = field(default_factory=list)
28
+
29
+ # Corrected results
30
+ transcription_corrected: Optional[CorrectionResult] = None
31
+
32
+ # Output files
33
+ lrc_filepath: Optional[str] = None
34
+ ass_filepath: Optional[str] = None
35
+ video_filepath: Optional[str] = None
36
+ mp3_filepath: Optional[str] = None
37
+ cdg_filepath: Optional[str] = None
38
+ cdg_zip_filepath: Optional[str] = None
39
+ original_txt: Optional[str] = None
40
+ corrected_txt: Optional[str] = None
41
+ corrections_json: Optional[str] = None
42
+
43
+ # Countdown padding info (for applying same padding to other audio files)
44
+ countdown_padding_added: bool = False
45
+ countdown_padding_seconds: float = 0.0
46
+ padded_audio_filepath: Optional[str] = None
47
+
48
+
49
+ class LyricsTranscriber:
50
+ """
51
+ Controller class that orchestrates the lyrics transcription workflow:
52
+ 1. Fetch lyrics from internet sources
53
+ 2. Run multiple transcription methods
54
+ 3. Correct transcribed lyrics using fetched lyrics
55
+ 4. Generate output formats (LRC, ASS, video)
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ audio_filepath: str,
61
+ artist: Optional[str] = None,
62
+ title: Optional[str] = None,
63
+ transcriber_config: Optional[TranscriberConfig] = None,
64
+ lyrics_config: Optional[LyricsConfig] = None,
65
+ output_config: Optional[OutputConfig] = None,
66
+ transcribers: Optional[Dict[str, BaseTranscriber]] = None,
67
+ lyrics_providers: Optional[Dict[str, BaseLyricsProvider]] = None,
68
+ corrector: Optional[LyricsCorrector] = None,
69
+ output_generator: Optional[OutputGenerator] = None,
70
+ logger: Optional[logging.Logger] = None,
71
+ log_level: int = logging.DEBUG,
72
+ log_formatter: Optional[logging.Formatter] = None,
73
+ ):
74
+ # Set up logging
75
+ self.logger = logger or logging.getLogger(__name__)
76
+ if not logger:
77
+ self.logger.setLevel(log_level)
78
+ if not self.logger.handlers:
79
+ handler = logging.StreamHandler()
80
+ formatter = log_formatter or logging.Formatter("%(asctime)s - %(levelname)s - %(module)s - %(message)s")
81
+ handler.setFormatter(formatter)
82
+ self.logger.addHandler(handler)
83
+
84
+ self.logger.debug(f"LyricsTranscriber instantiating with input file: {audio_filepath}")
85
+
86
+ # Store configs (with defaults if not provided)
87
+ self.transcriber_config = transcriber_config or TranscriberConfig()
88
+ self.lyrics_config = lyrics_config or LyricsConfig()
89
+ self.output_config = output_config or OutputConfig()
90
+
91
+ # Check if styles JSON is available for CDG and video features
92
+ if not self.output_config.output_styles_json or not os.path.exists(self.output_config.output_styles_json):
93
+ if self.output_config.generate_cdg or self.output_config.render_video:
94
+ self.logger.warning(
95
+ f"Output styles JSON file not found: {self.output_config.output_styles_json}. "
96
+ "CDG and video generation will be disabled."
97
+ )
98
+ self.output_config.generate_cdg = False
99
+ self.output_config.render_video = False
100
+
101
+ # Basic settings with sanitized filenames
102
+ self.audio_filepath = audio_filepath
103
+ self.artist = artist
104
+ self.title = title
105
+ self.output_prefix = self._create_sanitized_output_prefix(artist, title)
106
+
107
+ # Add after creating necessary folders
108
+ self.logger.debug(f"Using cache directory: {self.output_config.cache_dir}")
109
+ self.logger.debug(f"Using output directory: {self.output_config.output_dir}")
110
+
111
+ # Create necessary folders
112
+ os.makedirs(self.output_config.cache_dir, exist_ok=True)
113
+ os.makedirs(self.output_config.output_dir, exist_ok=True)
114
+
115
+ # Initialize results
116
+ self.results = LyricsControllerResult()
117
+
118
+ # Load styles early so lyrics providers can use them
119
+ self._load_styles()
120
+
121
+ # Initialize components (with dependency injection)
122
+ self.transcribers = transcribers or self._initialize_transcribers()
123
+ self.lyrics_providers = lyrics_providers or self._initialize_lyrics_providers()
124
+ self.corrector = corrector or LyricsCorrector(cache_dir=self.output_config.cache_dir, logger=self.logger)
125
+ self.output_generator = output_generator or self._initialize_output_generator()
126
+
127
+ # Log enabled features
128
+ self.logger.info("Enabled features:")
129
+ self.logger.info(f" Lyrics fetching: {'enabled' if self.output_config.fetch_lyrics else 'disabled'}")
130
+ self.logger.info(f" Transcription: {'enabled' if self.output_config.run_transcription else 'disabled'}")
131
+ self.logger.info(f" Lyrics correction: {'enabled' if self.output_config.run_correction else 'disabled'}")
132
+ self.logger.info(f" Plain text output: {'enabled' if self.output_config.generate_plain_text else 'disabled'}")
133
+ self.logger.info(f" LRC file generation: {'enabled' if self.output_config.generate_lrc else 'disabled'}")
134
+ self.logger.info(f" CDG file generation: {'enabled' if self.output_config.generate_cdg else 'disabled'}")
135
+ self.logger.info(f" Video rendering: {'enabled' if self.output_config.render_video else 'disabled'}")
136
+ if self.output_config.render_video:
137
+ self.logger.info(f" Video resolution: {self.output_config.video_resolution}")
138
+
139
+ def _load_styles(self) -> None:
140
+ """Load styles from JSON file if available."""
141
+ if self.output_config.output_styles_json and os.path.exists(self.output_config.output_styles_json):
142
+ try:
143
+ with open(self.output_config.output_styles_json, "r") as f:
144
+ self.output_config.styles = json.load(f)
145
+ self.logger.debug(f"Loaded output styles from: {self.output_config.output_styles_json}")
146
+ except Exception as e:
147
+ self.logger.warning(f"Failed to load output styles file: {str(e)}")
148
+ self.output_config.styles = {}
149
+ else:
150
+ self.logger.debug("No styles JSON file provided or file does not exist")
151
+ self.output_config.styles = {}
152
+
153
+ def _sanitize_filename(self, filename: str) -> str:
154
+ """Replace or remove characters that are unsafe for filenames."""
155
+ if not filename:
156
+ return ""
157
+ # Replace problematic characters with underscores
158
+ for char in ["\\", "/", ":", "*", "?", '"', "<", ">", "|"]:
159
+ filename = filename.replace(char, "_")
160
+ # Remove any trailing spaces
161
+ filename = filename.rstrip(" ")
162
+ return filename
163
+
164
+ def _create_sanitized_output_prefix(self, artist: Optional[str], title: Optional[str]) -> str:
165
+ """Create a sanitized output prefix from artist and title."""
166
+ if artist and title:
167
+ sanitized_artist = self._sanitize_filename(artist)
168
+ sanitized_title = self._sanitize_filename(title)
169
+ return f"{sanitized_artist} - {sanitized_title}"
170
+ else:
171
+ return self._sanitize_filename(os.path.splitext(os.path.basename(self.audio_filepath))[0])
172
+
173
+ def _initialize_transcribers(self) -> Dict[str, BaseTranscriber]:
174
+ """Initialize available transcription services."""
175
+ transcribers = {}
176
+
177
+ # Add debug logging for config values
178
+ self.logger.debug(f"Initializing transcribers with config: {self.transcriber_config}")
179
+ self.logger.debug(f"Using cache directory for transcribers: {self.output_config.cache_dir}")
180
+
181
+ if self.transcriber_config.audioshake_api_token:
182
+ self.logger.debug("Initializing AudioShake transcriber")
183
+ transcribers["audioshake"] = {
184
+ "instance": AudioShakeTranscriber(
185
+ cache_dir=self.output_config.cache_dir,
186
+ config=AudioShakeConfig(api_token=self.transcriber_config.audioshake_api_token),
187
+ logger=self.logger,
188
+ ),
189
+ "priority": 1, # AudioShake has highest priority
190
+ }
191
+ else:
192
+ self.logger.debug("Skipping AudioShake transcriber - no API token provided")
193
+
194
+ if self.transcriber_config.runpod_api_key and self.transcriber_config.whisper_runpod_id:
195
+ self.logger.debug("Initializing Whisper transcriber")
196
+ transcribers["whisper"] = {
197
+ "instance": WhisperTranscriber(
198
+ cache_dir=self.output_config.cache_dir,
199
+ config=WhisperConfig(
200
+ runpod_api_key=self.transcriber_config.runpod_api_key, endpoint_id=self.transcriber_config.whisper_runpod_id
201
+ ),
202
+ logger=self.logger,
203
+ ),
204
+ "priority": 2, # Whisper has lower priority
205
+ }
206
+ else:
207
+ self.logger.debug("Skipping Whisper transcriber - missing runpod_api_key or whisper_runpod_id")
208
+
209
+ return transcribers
210
+
211
+ def _initialize_lyrics_providers(self) -> Dict[str, BaseLyricsProvider]:
212
+ """Initialize available lyrics providers."""
213
+ providers = {}
214
+
215
+ # Get max_line_length from styles if available, otherwise use config default
216
+ max_line_length = self.output_config.styles.get("karaoke", {}).get("max_line_length", self.output_config.default_max_line_length)
217
+ self.logger.info(f"Using max_line_length for lyrics providers: {max_line_length}")
218
+
219
+ # Create provider config with all necessary parameters
220
+ provider_config = LyricsProviderConfig(
221
+ genius_api_token=self.lyrics_config.genius_api_token,
222
+ rapidapi_key=self.lyrics_config.rapidapi_key,
223
+ spotify_cookie=self.lyrics_config.spotify_cookie,
224
+ lyrics_file=self.lyrics_config.lyrics_file,
225
+ cache_dir=self.output_config.cache_dir,
226
+ audio_filepath=self.audio_filepath,
227
+ max_line_length=max_line_length,
228
+ )
229
+
230
+ if provider_config.lyrics_file and os.path.exists(provider_config.lyrics_file):
231
+ self.logger.debug(f"Initializing File lyrics provider with file: {provider_config.lyrics_file}")
232
+ providers["file"] = FileProvider(config=provider_config, logger=self.logger)
233
+ return providers
234
+
235
+ # LRCLIB - always enabled (no API key required)
236
+ self.logger.debug("Initializing LRCLIB lyrics provider")
237
+ providers["lrclib"] = LRCLIBProvider(config=provider_config, logger=self.logger)
238
+
239
+ if provider_config.genius_api_token:
240
+ self.logger.debug("Initializing Genius lyrics provider")
241
+ providers["genius"] = GeniusProvider(config=provider_config, logger=self.logger)
242
+ else:
243
+ self.logger.debug("Skipping Genius provider - no API token provided")
244
+
245
+ if provider_config.spotify_cookie:
246
+ self.logger.debug("Initializing Spotify lyrics provider")
247
+ providers["spotify"] = SpotifyProvider(config=provider_config, logger=self.logger)
248
+ else:
249
+ self.logger.debug("Skipping Spotify provider - no cookie provided")
250
+
251
+ if provider_config.rapidapi_key:
252
+ self.logger.debug("Initializing Musixmatch lyrics provider")
253
+ providers["musixmatch"] = MusixmatchProvider(config=provider_config, logger=self.logger)
254
+ else:
255
+ self.logger.debug("Skipping Musixmatch provider - no RapidAPI key provided")
256
+
257
+ return providers
258
+
259
+ def _initialize_output_generator(self) -> OutputGenerator:
260
+ """Initialize output generation service."""
261
+ return OutputGenerator(config=self.output_config, logger=self.logger)
262
+
263
+ def process(self) -> LyricsControllerResult:
264
+ """Main processing method that orchestrates the entire workflow."""
265
+
266
+ self.logger.info(f"LyricsTranscriber controller beginning processing for {self.artist} - {self.title}")
267
+
268
+ # Debug: Log package version and environment variables
269
+ try:
270
+ import lyrics_transcriber
271
+ package_version = getattr(lyrics_transcriber, '__version__', 'unknown')
272
+ self.logger.info(f"LyricsTranscriber package version: {package_version}")
273
+ except Exception as e:
274
+ self.logger.warning(f"Could not get package version: {e}")
275
+
276
+ # Debug: Log environment variables (first 3 characters only for security)
277
+ env_vars = {}
278
+ for key, value in os.environ.items():
279
+ if value:
280
+ env_vars[key] = value[:3] + "..." if len(value) > 3 else value
281
+ else:
282
+ env_vars[key] = "(empty)"
283
+
284
+ self.logger.info(f"Environment variables count: {len(env_vars)}")
285
+
286
+ # Log specific API-related variables
287
+ api_vars = {k: v for k, v in env_vars.items() if any(keyword in k.upper() for keyword in ['API', 'TOKEN', 'KEY', 'SECRET'])}
288
+ if api_vars:
289
+ self.logger.info(f"API-related environment variables: {api_vars}")
290
+ else:
291
+ self.logger.warning("No API-related environment variables found")
292
+
293
+ # Log all env vars if in debug mode
294
+ if self.logger.getEffectiveLevel() <= logging.DEBUG:
295
+ self.logger.debug(f"All environment variables: {env_vars}")
296
+
297
+ # Check for existing corrections JSON
298
+ corrections_json_path = os.path.join(self.output_config.output_dir, f"{self.output_prefix} (Lyrics Corrections).json")
299
+
300
+ if os.path.exists(corrections_json_path):
301
+ self.logger.info(f"Found existing corrections JSON: {corrections_json_path}")
302
+ try:
303
+ with open(corrections_json_path, "r", encoding="utf-8") as f:
304
+ corrections_data = json.load(f)
305
+
306
+ # Reconstruct CorrectionResult from JSON
307
+ self.results.transcription_corrected = CorrectionResult.from_dict(corrections_data)
308
+ self.logger.info("Successfully loaded existing corrections data")
309
+
310
+ # Skip to output generation
311
+ self.generate_outputs()
312
+ self.logger.info("Processing completed successfully using existing corrections")
313
+ return self.results
314
+
315
+ except Exception as e:
316
+ self.logger.error(f"Failed to load existing corrections JSON: {str(e)}")
317
+ # Continue with normal processing if loading fails
318
+
319
+ # Normal processing flow continues...
320
+ if self.output_config.fetch_lyrics and self.artist and self.title:
321
+ self.fetch_lyrics()
322
+ else:
323
+ self.logger.info("Skipping lyrics fetching - no artist/title provided or fetching disabled")
324
+
325
+ # Step 2: Run transcription if enabled
326
+ if self.output_config.run_transcription:
327
+ self.transcribe()
328
+ else:
329
+ self.logger.info("Skipping transcription - transcription disabled")
330
+
331
+ # Step 3: Process and correct lyrics if enabled AND we have transcription results
332
+ if self.output_config.run_correction and self.results.transcription_results:
333
+ self.correct_lyrics()
334
+ elif self.output_config.run_correction:
335
+ self.logger.info("Skipping lyrics correction - no transcription results available")
336
+
337
+ # Step 4: Generate outputs based on what we have
338
+ if self.results.transcription_corrected or self.results.lyrics_results:
339
+ self.generate_outputs()
340
+ else:
341
+ self.logger.warning("No corrected transcription or lyrics available. Skipping output generation.")
342
+
343
+ self.logger.info("Processing completed successfully")
344
+ return self.results
345
+
346
+ def fetch_lyrics(self) -> None:
347
+ """Fetch lyrics from available providers."""
348
+ self.logger.info(f"Fetching lyrics for {self.artist} - {self.title}")
349
+
350
+ for name, provider in self.lyrics_providers.items():
351
+ try:
352
+ result = provider.fetch_lyrics(self.artist, self.title)
353
+ if result:
354
+ self.results.lyrics_results[name] = result
355
+ self.logger.info(f"Successfully fetched lyrics from {name}")
356
+
357
+ except Exception as e:
358
+ self.logger.error(f"Failed to fetch lyrics from {name}: {str(e)}")
359
+ continue
360
+
361
+ if not self.results.lyrics_results:
362
+ self.logger.warning("No lyrics found from any source")
363
+
364
+ def transcribe(self) -> None:
365
+ """Run transcription using all available transcribers."""
366
+ self.logger.info(f"Starting transcription with providers: {list(self.transcribers.keys())}")
367
+
368
+ for name, transcriber_info in self.transcribers.items():
369
+ self.logger.info(f"Running transcription with {name}")
370
+ result = transcriber_info["instance"].transcribe(self.audio_filepath)
371
+ if result:
372
+ # Add the transcriber name and priority to the result
373
+ self.results.transcription_results.append(
374
+ TranscriptionResult(name=name, priority=transcriber_info["priority"], result=result)
375
+ )
376
+ self.logger.debug(f"Transcription completed for {name}")
377
+
378
+ if not self.results.transcription_results:
379
+ self.logger.warning("No successful transcriptions from any provider")
380
+
381
+ def correct_lyrics(self) -> None:
382
+ """Run lyrics correction using transcription and internet lyrics."""
383
+ self.logger.info("Starting lyrics correction process")
384
+
385
+ # Check if we have reference lyrics to work with
386
+ if not self.results.lyrics_results:
387
+ self.logger.warning("No reference lyrics available for correction - using raw transcription")
388
+ # Use the highest priority transcription result as the "corrected" version
389
+ if self.results.transcription_results:
390
+ sorted_results = sorted(self.results.transcription_results, key=lambda x: x.priority)
391
+ best_transcription = sorted_results[0]
392
+
393
+ # Count total words in the transcription
394
+ total_words = sum(len(segment.words) for segment in best_transcription.result.segments)
395
+
396
+ # Create a CorrectionResult with no corrections
397
+ self.results.transcription_corrected = CorrectionResult(
398
+ original_segments=best_transcription.result.segments,
399
+ corrected_segments=best_transcription.result.segments,
400
+ corrections=[], # No corrections made
401
+ corrections_made=0, # No corrections made
402
+ confidence=1.0, # Full confidence since we're using original
403
+ reference_lyrics={},
404
+ anchor_sequences=[],
405
+ gap_sequences=[],
406
+ resized_segments=[],
407
+ correction_steps=[],
408
+ word_id_map={},
409
+ segment_id_map={},
410
+ metadata={
411
+ "correction_type": "none",
412
+ "reason": "no_reference_lyrics",
413
+ "audio_filepath": self.audio_filepath,
414
+ "anchor_sequences_count": 0,
415
+ "gap_sequences_count": 0,
416
+ "total_words": total_words,
417
+ "correction_ratio": 0.0,
418
+ "available_handlers": [],
419
+ "enabled_handlers": [],
420
+ },
421
+ )
422
+ else:
423
+ # Create metadata dict with song info
424
+ metadata = {
425
+ "artist": self.artist,
426
+ "title": self.title,
427
+ "full_reference_texts": {source: lyrics.get_full_text() for source, lyrics in self.results.lyrics_results.items()},
428
+ }
429
+
430
+ # Get enabled handlers from metadata if available
431
+ enabled_handlers = metadata.get("enabled_handlers", None)
432
+
433
+ # Create corrector with enabled handlers
434
+ corrector = LyricsCorrector(cache_dir=self.output_config.cache_dir, enabled_handlers=enabled_handlers, logger=self.logger)
435
+
436
+ corrected_data = corrector.run(
437
+ transcription_results=self.results.transcription_results,
438
+ lyrics_results=self.results.lyrics_results,
439
+ metadata=metadata,
440
+ )
441
+
442
+ # Store corrected results
443
+ self.results.transcription_corrected = corrected_data
444
+ self.logger.info("Lyrics correction completed")
445
+
446
+ # Add human review step (moved outside the else block)
447
+ if self.output_config.enable_review:
448
+ from lyrics_transcriber.review.server import ReviewServer
449
+
450
+ self.logger.info("Starting human review process")
451
+
452
+ # Create and start review server
453
+ review_server = ReviewServer(
454
+ correction_result=self.results.transcription_corrected,
455
+ output_config=self.output_config,
456
+ audio_filepath=self.audio_filepath,
457
+ logger=self.logger,
458
+ )
459
+ reviewed_data = review_server.start()
460
+
461
+ self.logger.info("Human review completed, updated transcription_corrected with reviewed_data")
462
+ self.results.transcription_corrected = reviewed_data
463
+
464
+ # Add countdown intro if enabled and needed (after review, before output generation)
465
+ if self.output_config.add_countdown and self.results.transcription_corrected:
466
+ from lyrics_transcriber.output.countdown_processor import CountdownProcessor
467
+
468
+ self.logger.info("Processing countdown intro (if needed)")
469
+ countdown_processor = CountdownProcessor(
470
+ cache_dir=self.output_config.cache_dir,
471
+ logger=self.logger,
472
+ )
473
+
474
+ # Process and potentially modify the correction result and audio filepath
475
+ (
476
+ self.results.transcription_corrected,
477
+ self.audio_filepath,
478
+ padding_added,
479
+ padding_seconds,
480
+ ) = countdown_processor.process(
481
+ correction_result=self.results.transcription_corrected,
482
+ audio_filepath=self.audio_filepath,
483
+ )
484
+
485
+ # Store padding information in results for parent code to use
486
+ self.results.countdown_padding_added = padding_added
487
+ self.results.countdown_padding_seconds = padding_seconds
488
+ if padding_added:
489
+ self.results.padded_audio_filepath = self.audio_filepath
490
+ self.logger.info(
491
+ f"Countdown padding applied: {padding_seconds}s added to audio. "
492
+ f"Padded audio: {self.audio_filepath}"
493
+ )
494
+
495
+ def generate_outputs(self) -> None:
496
+ """Generate output files based on enabled features and available data."""
497
+ self.logger.info("Generating output files")
498
+
499
+ # Only proceed with outputs that make sense based on what we have
500
+ has_correction = bool(self.results.transcription_corrected)
501
+
502
+ output_files = self.output_generator.generate_outputs(
503
+ transcription_corrected=self.results.transcription_corrected if has_correction else None,
504
+ lyrics_results=self.results.lyrics_results,
505
+ output_prefix=self.output_prefix,
506
+ audio_filepath=self.audio_filepath,
507
+ artist=self.artist,
508
+ title=self.title,
509
+ )
510
+
511
+ # Store results
512
+ self.results.lrc_filepath = output_files.lrc
513
+ self.results.ass_filepath = output_files.ass
514
+ self.results.video_filepath = output_files.video
515
+ self.results.original_txt = output_files.original_txt
516
+ self.results.corrected_txt = output_files.corrected_txt
517
+ self.results.corrections_json = output_files.corrections_json
518
+ self.results.cdg_filepath = output_files.cdg
519
+ self.results.mp3_filepath = output_files.mp3
520
+ self.results.cdg_zip_filepath = output_files.cdg_zip
File without changes
@@ -0,0 +1,9 @@
1
+ """Agentic AI correction system scaffold.
2
+
3
+ This package will contain the semi-agentic correction workflows, providers,
4
+ observability, and feedback modules. Implementation follows TDD; tests come first.
5
+ """
6
+
7
+ __all__ = []
8
+
9
+
@@ -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
+