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,102 @@
1
+ import logging
2
+ import os
3
+ from typing import List, Optional
4
+
5
+ from lyrics_transcriber.types import LyricsSegment, Word
6
+
7
+
8
+ class LyricsFileGenerator:
9
+ """Handles generation of lyrics files in various formats (LRC, etc)."""
10
+
11
+ def __init__(self, output_dir: str, logger: Optional[logging.Logger] = None):
12
+ """Initialize LyricsFileGenerator.
13
+
14
+ Args:
15
+ output_dir: Directory where output files will be written
16
+ logger: Optional logger instance
17
+ """
18
+ self.output_dir = output_dir
19
+ self.logger = logger or logging.getLogger(__name__)
20
+
21
+ def _get_output_path(self, output_prefix: str, extension: str) -> str:
22
+ """Generate full output path for a file."""
23
+ return os.path.join(self.output_dir, f"{output_prefix}.{extension}")
24
+
25
+ def generate_lrc(self, segments: List[LyricsSegment], output_prefix: str) -> str:
26
+ """Generate LRC format lyrics file.
27
+
28
+ Args:
29
+ segments: List of LyricsSegment objects containing word timing data
30
+ output_prefix: Prefix for output filename
31
+
32
+ Returns:
33
+ Path to generated LRC file
34
+ """
35
+ self.logger.info("Generating LRC format lyrics")
36
+ output_path = self._get_output_path(f"{output_prefix} (Karaoke)", "lrc")
37
+
38
+ try:
39
+ self._write_lrc_file(output_path, segments)
40
+ self.logger.info(f"LRC file generated: {output_path}")
41
+ return output_path
42
+
43
+ except Exception as e:
44
+ self.logger.error(f"Failed to generate LRC file: {str(e)}")
45
+ raise
46
+
47
+ def _write_lrc_file(self, output_path: str, segments: List[LyricsSegment]) -> None:
48
+ """Write LRC file content with MidiCo-compatible word-level timestamps.
49
+
50
+ Args:
51
+ output_path: Path to write the LRC file
52
+ segments: List of LyricsSegment objects containing word timing data
53
+ """
54
+ with open(output_path, "w", encoding="utf-8") as f:
55
+ # Write MidiCo header
56
+ f.write("[re:MidiCo]\n")
57
+
58
+ for segment in segments:
59
+ for i, word in enumerate(segment.words):
60
+ start_time = self._format_lrc_timestamp(word.start_time)
61
+
62
+ # Add space after all words except last in segment
63
+ text = word.text
64
+ if i != len(segment.words) - 1:
65
+ text += " "
66
+
67
+ # Add "/" prefix for first word in segment
68
+ prefix = "/" if i == 0 else ""
69
+
70
+ # Write MidiCo formatted line
71
+ f.write(f"[{start_time}]1:{prefix}{text}\n")
72
+
73
+ def _format_lrc_timestamp(self, seconds: float) -> str:
74
+ """Format timestamp for MidiCo LRC format (MM:SS.mmm).
75
+
76
+ Args:
77
+ seconds: Time in seconds
78
+
79
+ Returns:
80
+ Formatted timestamp string in MM:SS.mmm format
81
+ """
82
+ minutes = int(seconds // 60)
83
+ remaining_seconds = seconds % 60
84
+
85
+ # Convert to milliseconds and round to nearest integer
86
+ total_milliseconds = round(remaining_seconds * 1000)
87
+
88
+ # Extract seconds and milliseconds
89
+ seconds_part = total_milliseconds // 1000
90
+ milliseconds = total_milliseconds % 1000
91
+
92
+ # Handle rollover
93
+ if seconds_part == 60:
94
+ seconds_part = 0
95
+ minutes += 1
96
+
97
+ return f"{minutes:02d}:{seconds_part:02d}.{milliseconds:03d}"
98
+
99
+ # Future methods for other lyrics file formats can be added here
100
+ # def generate_txt(self, segments: List[LyricsSegment], output_prefix: str) -> str:
101
+ # """Generate Power Karaoke TXT format lyrics file."""
102
+ # pass
@@ -0,0 +1,96 @@
1
+ import logging
2
+ import os
3
+ from typing import List, Optional
4
+
5
+ from lyrics_transcriber.types import LyricsData, LyricsSegment
6
+ from lyrics_transcriber.correction.corrector import CorrectionResult
7
+
8
+
9
+ class PlainTextGenerator:
10
+ """Handles generation of plain text output files for lyrics and transcriptions."""
11
+
12
+ def __init__(self, output_dir: str, logger: Optional[logging.Logger] = None):
13
+ """Initialize PlainTextGenerator.
14
+
15
+ Args:
16
+ output_dir: Directory where output files will be written
17
+ logger: Optional logger instance
18
+ """
19
+ self.output_dir = output_dir
20
+ self.logger = logger or logging.getLogger(__name__)
21
+
22
+ def _get_output_path(self, output_prefix: str, extension: str) -> str:
23
+ """Generate full output path for a file."""
24
+ return os.path.join(self.output_dir, f"{output_prefix}.{extension}")
25
+
26
+ def write_lyrics(self, lyrics_data: LyricsData, output_prefix: str) -> str:
27
+ """Write plain text lyrics file from provider data.
28
+
29
+ Args:
30
+ lyrics_data: LyricsData from a lyrics provider
31
+ output_prefix: Prefix for output filename
32
+
33
+ Returns:
34
+ Path to generated file
35
+ """
36
+ self.logger.info("Writing plain lyrics file")
37
+ provider_name = lyrics_data.metadata.source.title()
38
+ output_path = self._get_output_path(f"{output_prefix} (Lyrics {provider_name})", "txt")
39
+
40
+ try:
41
+ with open(output_path, "w", encoding="utf-8") as f:
42
+ # Join segment texts with newlines
43
+ lyrics_text = "\n".join(segment.text for segment in lyrics_data.segments)
44
+ f.write(lyrics_text)
45
+ self.logger.info(f"Plain lyrics file generated: {output_path}")
46
+ return output_path
47
+ except Exception as e:
48
+ self.logger.error(f"Failed to write plain lyrics file: {str(e)}")
49
+ raise
50
+
51
+ def write_corrected_lyrics(self, segments: List[LyricsSegment], output_prefix: str) -> str:
52
+ """Write corrected lyrics as plain text file.
53
+
54
+ Args:
55
+ segments: List of corrected LyricsSegment objects
56
+ output_prefix: Prefix for output filename
57
+
58
+ Returns:
59
+ Path to generated file
60
+ """
61
+ self.logger.info("Writing corrected lyrics file")
62
+ output_path = self._get_output_path(f"{output_prefix} (Lyrics Corrected)", "txt")
63
+
64
+ try:
65
+ with open(output_path, "w", encoding="utf-8") as f:
66
+ for segment in segments:
67
+ f.write(f"{segment.text}\n")
68
+ self.logger.info(f"Corrected lyrics file generated: {output_path}")
69
+ return output_path
70
+ except Exception as e:
71
+ self.logger.error(f"Failed to write corrected lyrics file: {str(e)}")
72
+ raise
73
+
74
+ def write_original_transcription(self, correction_result: CorrectionResult, output_prefix: str) -> str:
75
+ """Write original (uncorrected) transcription as plain text.
76
+
77
+ Args:
78
+ correction_result: CorrectionResult containing original transcription
79
+ output_prefix: Prefix for output filename
80
+
81
+ Returns:
82
+ Path to generated file
83
+ """
84
+ self.logger.info("Writing original transcription file")
85
+ output_path = self._get_output_path(f"{output_prefix} (Lyrics Uncorrected)", "txt")
86
+
87
+ transcribed_text = " ".join(" ".join(w.text for w in segment.words) for segment in correction_result.original_segments)
88
+
89
+ try:
90
+ with open(output_path, "w", encoding="utf-8") as f:
91
+ f.write(transcribed_text)
92
+ self.logger.info(f"Original transcription file generated: {output_path}")
93
+ return output_path
94
+ except Exception as e:
95
+ self.logger.error(f"Failed to write original transcription file: {str(e)}")
96
+ raise
@@ -0,0 +1,431 @@
1
+ import logging
2
+ import re
3
+ from typing import List, Optional
4
+
5
+ from lyrics_transcriber.types import LyricsSegment, Word
6
+ from lyrics_transcriber.utils.word_utils import WordUtils
7
+
8
+
9
+ class SegmentResizer:
10
+ """Handles resizing of lyrics segments to ensure proper line lengths and natural breaks.
11
+
12
+ This class processes lyrics segments and splits them into smaller segments when they exceed
13
+ a maximum line length. It attempts to split at natural break points like sentence endings,
14
+ commas, or conjunctions to maintain readability.
15
+
16
+ Example:
17
+ resizer = SegmentResizer(max_line_length=36)
18
+ segments = [
19
+ LyricsSegment(
20
+ text="This is a very long sentence that needs to be split into multiple lines for better readability",
21
+ words=[...], # List of Word objects with timing information
22
+ start_time=0.0,
23
+ end_time=5.0
24
+ )
25
+ ]
26
+ resized = resizer.resize_segments(segments)
27
+ # Results in:
28
+ # [
29
+ # LyricsSegment(text="This is a very long sentence", ...),
30
+ # LyricsSegment(text="that needs to be split", ...),
31
+ # LyricsSegment(text="into multiple lines", ...),
32
+ # LyricsSegment(text="for better readability", ...)
33
+ # ]
34
+ """
35
+
36
+ def __init__(self, max_line_length: int = 36, logger: Optional[logging.Logger] = None):
37
+ """Initialize the SegmentResizer.
38
+
39
+ Args:
40
+ max_line_length: Maximum allowed length for a single line of text
41
+ logger: Optional logger for debugging information
42
+ """
43
+ self.max_line_length = max_line_length
44
+ self.logger = logger or logging.getLogger(__name__)
45
+
46
+ def resize_segments(self, segments: List[LyricsSegment]) -> List[LyricsSegment]:
47
+ """Main entry point for resizing segments.
48
+
49
+ Takes a list of potentially long segments and splits them into smaller ones
50
+ while preserving word timing information.
51
+
52
+ Example:
53
+ Input segment: "Hello world, this is a test. And here's another sentence."
54
+ Output segments: [
55
+ "Hello world, this is a test.",
56
+ "And here's another sentence."
57
+ ]
58
+
59
+ Args:
60
+ segments: List of LyricsSegment objects to process
61
+
62
+ Returns:
63
+ List of resized LyricsSegment objects
64
+ """
65
+ self._log_input_segments(segments)
66
+ resized_segments: List[LyricsSegment] = []
67
+
68
+ for segment_idx, segment in enumerate(segments):
69
+ cleaned_segment = self._create_cleaned_segment(segment)
70
+
71
+ # Only split if the segment is longer than max_line_length
72
+ if len(cleaned_segment.text) <= self.max_line_length:
73
+ resized_segments.append(cleaned_segment)
74
+ continue
75
+
76
+ # Process oversized segments
77
+ resized_segments.extend(self._split_oversized_segment(segment_idx, segment))
78
+
79
+ self._log_output_segments(resized_segments)
80
+ return resized_segments
81
+
82
+ def _clean_text(self, text: str) -> str:
83
+ """Clean text by removing newlines and extra whitespace.
84
+
85
+ Example:
86
+ Input: "Hello\n World \n!"
87
+ Output: "Hello World !"
88
+
89
+ Args:
90
+ text: String to clean
91
+
92
+ Returns:
93
+ Cleaned string with normalized whitespace
94
+ """
95
+ return " ".join(text.replace("\n", " ").split())
96
+
97
+ def _create_cleaned_segment(self, segment: LyricsSegment) -> LyricsSegment:
98
+ """Create a new segment with cleaned text while preserving timing info.
99
+
100
+ Example:
101
+ Input: LyricsSegment(text="Hello\n World\n", words=[...])
102
+ Output: LyricsSegment(text="Hello World", words=[...])
103
+ """
104
+ cleaned_text = self._clean_text(segment.text)
105
+ return LyricsSegment(
106
+ id=segment.id, # Preserve the original segment ID
107
+ text=cleaned_text,
108
+ words=segment.words,
109
+ start_time=segment.start_time,
110
+ end_time=segment.end_time,
111
+ )
112
+
113
+ def _create_cleaned_word(self, word: Word) -> Word:
114
+ """Create a new word with cleaned text."""
115
+ cleaned_text = self._clean_text(word.text)
116
+ return Word(
117
+ id=word.id, # Preserve the original word ID
118
+ text=cleaned_text,
119
+ start_time=word.start_time,
120
+ end_time=word.end_time,
121
+ confidence=word.confidence if hasattr(word, "confidence") else None,
122
+ )
123
+
124
+ def _split_oversized_segment(self, segment_idx: int, segment: LyricsSegment) -> List[LyricsSegment]:
125
+ """Split an oversized segment into multiple segments at natural break points.
126
+
127
+ Example:
128
+ Input: "This is a long sentence. Here's another one."
129
+ Output: [
130
+ LyricsSegment(text="This is a long sentence.", ...),
131
+ LyricsSegment(text="Here's another one.", ...)
132
+ ]
133
+ """
134
+ segment_text = self._clean_text(segment.text)
135
+
136
+ self.logger.info(f"Processing oversized segment {segment_idx}: '{segment_text}'")
137
+ split_lines = self._process_segment_text(segment_text)
138
+ self.logger.debug(f"Split into {len(split_lines)} lines: {split_lines}")
139
+
140
+ return self._create_segments_from_lines(segment_text, split_lines, segment.words)
141
+
142
+ def _create_segments_from_lines(self, segment_text: str, split_lines: List[str], words: List[Word]) -> List[LyricsSegment]:
143
+ """Create segments from split lines while preserving word timing.
144
+
145
+ Matches words to their corresponding lines based on text position and
146
+ creates new segments with the correct timing information.
147
+
148
+ Example:
149
+ segment_text: "Hello world, how are you"
150
+ split_lines: ["Hello world,", "how are you"]
151
+ words: [Word("Hello", 0.0, 1.0), Word("world", 1.0, 2.0), ...]
152
+
153
+ Returns segments with words properly assigned to each line.
154
+ """
155
+ segments: List[LyricsSegment] = []
156
+ words_to_process = words.copy()
157
+ current_pos = 0
158
+
159
+ for line in split_lines:
160
+ line_words = []
161
+ line_text = line.strip()
162
+ remaining_line = line_text
163
+
164
+ # Keep processing words until we've found all words for this line
165
+ while words_to_process and remaining_line:
166
+ word = words_to_process[0]
167
+ word_clean = self._clean_text(word.text)
168
+
169
+ # Check if the cleaned word appears in the remaining line text
170
+ if word_clean in remaining_line:
171
+ word_pos = remaining_line.find(word_clean)
172
+ if word_pos != -1:
173
+ line_words.append(words_to_process.pop(0))
174
+ # Remove the word and any following spaces from remaining line
175
+ remaining_line = remaining_line[word_pos + len(word_clean) :].strip()
176
+ continue
177
+
178
+ # If we can't find the word in the remaining line, we're done with this line
179
+ break
180
+
181
+ if line_words:
182
+ segments.append(self._create_segment_from_words(line, line_words))
183
+ current_pos += len(line) + 1 # +1 for the space between lines
184
+
185
+ # If we have any remaining words, create a final segment with them
186
+ if words_to_process:
187
+ remaining_text = " ".join(self._clean_text(w.text) for w in words_to_process)
188
+ segments.append(self._create_segment_from_words(remaining_text, words_to_process))
189
+
190
+ return segments
191
+
192
+ def _create_line_segment(
193
+ self, line_idx: int, line: str, segment_text: str, available_words: List[Word], current_pos: int
194
+ ) -> Optional[LyricsSegment]:
195
+ """Create a single segment from a line of text."""
196
+ line_pos = segment_text.find(line, current_pos)
197
+ if line_pos == -1:
198
+ self.logger.error(f"Failed to find line '{line}' in segment text '{segment_text}' " f"starting from position {current_pos}")
199
+ return None
200
+
201
+ line_words = self._find_words_for_line(line, line_pos, len(line), segment_text, available_words, current_pos)
202
+
203
+ if line_words:
204
+ return self._create_segment_from_words(line, line_words)
205
+ else:
206
+ self.logger.warning(f"No words found for line '{line}'")
207
+ return None
208
+
209
+ def _find_words_for_line(
210
+ self, line: str, line_pos: int, line_length: int, segment_text: str, available_words: List[Word], current_pos: int
211
+ ) -> List[Word]:
212
+ """Find words that belong to a specific line."""
213
+ line_words = []
214
+ line_text = line.strip()
215
+ remaining_text = line_text
216
+
217
+ for word in available_words:
218
+ # Skip if word isn't in remaining text
219
+ if word.text not in remaining_text:
220
+ continue
221
+
222
+ # Find position of word in line
223
+ word_pos = remaining_text.find(word.text)
224
+ if word_pos != -1:
225
+ line_words.append(word)
226
+ # Remove processed text up to and including this word
227
+ remaining_text = remaining_text[word_pos + len(word.text) :].strip()
228
+
229
+ if not remaining_text: # All words found
230
+ break
231
+
232
+ return line_words
233
+
234
+ def _create_segment_from_words(self, line: str, words: List[Word]) -> LyricsSegment:
235
+ """Create a new segment from a list of words."""
236
+ cleaned_text = self._clean_text(line)
237
+ return LyricsSegment(
238
+ id=WordUtils.generate_id(), # Generate new ID for split segments
239
+ text=cleaned_text,
240
+ words=words,
241
+ start_time=words[0].start_time,
242
+ end_time=words[-1].end_time,
243
+ )
244
+
245
+ def _process_segment_text(self, text: str) -> List[str]:
246
+ """Process segment text to determine optimal split points."""
247
+ self.logger.debug(f"Processing segment text: '{text}'")
248
+ processed_lines: List[str] = []
249
+ remaining_text = text.strip()
250
+
251
+ while remaining_text:
252
+ self.logger.debug(f"Remaining text to process: '{remaining_text}'")
253
+
254
+ # If remaining text is within limit, add it and we're done
255
+ if len(remaining_text) <= self.max_line_length:
256
+ processed_lines.append(remaining_text)
257
+ break
258
+
259
+ # Find best split point
260
+ split_point = self._find_best_split_point(remaining_text)
261
+ first_part = remaining_text[:split_point].strip()
262
+ second_part = remaining_text[split_point:].strip()
263
+
264
+ # Only split if:
265
+ # 1. We found a valid split point
266
+ # 2. First part isn't too long
267
+ # 3. Both parts are non-empty
268
+ if split_point < len(remaining_text) and len(first_part) <= self.max_line_length and first_part and second_part:
269
+
270
+ processed_lines.append(first_part)
271
+ remaining_text = second_part
272
+ else:
273
+ # If we can't find a good split, keep the whole text
274
+ processed_lines.append(remaining_text)
275
+ break
276
+
277
+ return processed_lines
278
+
279
+ def _find_best_split_point(self, line: str) -> int:
280
+ """Find the best split point that creates natural, well-balanced segments."""
281
+ self.logger.debug(f"Finding best split point for line: '{line}' (length: {len(line)})")
282
+
283
+ # If line is within max length, don't split
284
+ if len(line) <= self.max_line_length:
285
+ return len(line)
286
+
287
+ break_points = self._find_break_points(line)
288
+ best_point = None
289
+ best_score = float("-inf")
290
+
291
+ # Try each break point and score it
292
+ for priority, points in enumerate(break_points):
293
+ for point in sorted(points): # Sort points to prefer earlier ones in same priority
294
+ if point <= 0 or point >= len(line):
295
+ continue
296
+
297
+ first_part = line[:point].strip()
298
+
299
+ # Skip if first part is too long
300
+ if len(first_part) > self.max_line_length:
301
+ continue
302
+
303
+ # Score this break point
304
+ score = self._score_break_point(line, point, priority)
305
+ if score > best_score:
306
+ best_score = score
307
+ best_point = point
308
+
309
+ # If no good break points found, fall back to last space before max_length
310
+ if best_point is None:
311
+ last_space = line.rfind(" ", 0, self.max_line_length)
312
+ if last_space != -1:
313
+ return last_space
314
+
315
+ return best_point if best_point is not None else self.max_line_length
316
+
317
+ def _score_break_point(self, line: str, point: int, priority: int) -> float:
318
+ """Score a potential break point based on multiple factors.
319
+
320
+ Factors considered:
321
+ 1. Priority of the break point type (sentence > clause > comma, etc.)
322
+ 2. Balance of segment lengths
323
+ 3. Proximity to target length
324
+
325
+ Example:
326
+ line: "This is a sentence. And more text."
327
+ point: 18 (after "sentence.")
328
+ priority: 0 (sentence break)
329
+
330
+ Returns a score where higher is better. Score components:
331
+ - Base score (100-20*priority): 100 for priority 0
332
+ - Length ratio bonus (0-10): Based on segment balance
333
+ - Target length bonus (0-5): Based on proximity to ideal length
334
+ """
335
+ first_segment = line[:point].strip()
336
+ second_segment = line[point:].strip()
337
+
338
+ # Base score starts with priority
339
+ score = 100 - (priority * 20) # Priorities 0-4 give scores 100,80,60,40,20
340
+
341
+ # Length ratio bonus
342
+ length_ratio = min(len(first_segment), len(second_segment)) / max(len(first_segment), len(second_segment))
343
+ score += length_ratio * 10
344
+
345
+ # Target length bonus
346
+ target_length = self.max_line_length * 0.7
347
+ first_length_score = 1 - abs(len(first_segment) - target_length) / self.max_line_length
348
+ score += first_length_score * 5
349
+
350
+ return score
351
+
352
+ def _find_break_points(self, line: str) -> List[List[int]]:
353
+ """Find potential break points in order of preference.
354
+
355
+ Returns a list of lists, where each inner list contains break points
356
+ of the same priority. Break points are indices where text should be split.
357
+
358
+ Priority order:
359
+ 1. Sentence endings (., !, ?)
360
+ 2. Major clause breaks (;, -)
361
+ 3. Comma breaks
362
+ 4. Coordinating conjunctions (and, but, or)
363
+ 5. Prepositions/articles (in, at, the, a)
364
+
365
+ Example:
366
+ Input: "Hello, world. This is a test"
367
+ Output: [
368
+ [12], # sentence break after "world."
369
+ [], # no semicolons or dashes
370
+ [5], # comma after "Hello,"
371
+ [], # no conjunctions
372
+ [15] # preposition "is"
373
+ ]
374
+ """
375
+ break_points = []
376
+
377
+ # Priority 1: Sentence endings
378
+ sentence_breaks = []
379
+ for punct in [".", "!", "?"]:
380
+ for match in re.finditer(rf"\{punct}\s+", line):
381
+ sentence_breaks.append(match.start() + 1)
382
+ break_points.append(sentence_breaks)
383
+
384
+ # Priority 2: Major clause breaks (semicolons, dashes)
385
+ major_breaks = []
386
+ for punct in [";", " - "]:
387
+ for match in re.finditer(re.escape(punct), line):
388
+ major_breaks.append(match.start()) # Position before the punctuation
389
+ break_points.append(major_breaks)
390
+
391
+ # Priority 3: Comma breaks, typically marking natural pauses
392
+ comma_breaks = []
393
+ for match in re.finditer(r",\s+", line):
394
+ comma_breaks.append(match.start() + 1) # Position after the comma
395
+ break_points.append(comma_breaks)
396
+
397
+ # Priority 4: Coordinating conjunctions with surrounding spaces
398
+ conjunction_breaks = []
399
+ for conj in [" and ", " but ", " or "]:
400
+ for match in re.finditer(re.escape(conj), line):
401
+ conjunction_breaks.append(match.start()) # Position before the conjunction
402
+ break_points.append(conjunction_breaks)
403
+
404
+ # Priority 5: Prepositions or articles with surrounding spaces (last resort)
405
+ minor_breaks = []
406
+ for prep in [" in ", " at ", " the ", " a "]:
407
+ for match in re.finditer(re.escape(prep), line):
408
+ minor_breaks.append(match.start()) # Position before the preposition
409
+ break_points.append(minor_breaks)
410
+
411
+ return break_points
412
+
413
+ def _log_input_segments(self, segments: List[LyricsSegment]) -> None:
414
+ """Log input segment information."""
415
+ self.logger.info(f"Starting segment resize. Input: {len(segments)} segments")
416
+ for idx, segment in enumerate(segments):
417
+ self.logger.debug(
418
+ f"Input segment {idx}: text='{segment.text}', "
419
+ f"words={len(segment.words)} words, "
420
+ f"time={segment.start_time:.2f}-{segment.end_time:.2f}"
421
+ )
422
+
423
+ def _log_output_segments(self, segments: List[LyricsSegment]) -> None:
424
+ """Log output segment information."""
425
+ self.logger.info(f"Finished resizing. Output: {len(segments)} segments")
426
+ for idx, segment in enumerate(segments):
427
+ self.logger.debug(
428
+ f"Output segment {idx}: text='{segment.text}', "
429
+ f"words={len(segment.words)} words, "
430
+ f"time={segment.start_time:.2f}-{segment.end_time:.2f}"
431
+ )