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,619 @@
1
+ import logging
2
+ from typing import List, Optional, Tuple
3
+ import logging
4
+ import re
5
+ import toml
6
+ from pathlib import Path
7
+ from PIL import ImageFont
8
+ import os
9
+ import zipfile
10
+ import shutil
11
+
12
+ from lyrics_transcriber.output.cdgmaker.cdg import CDG_VISIBLE_WIDTH
13
+ from lyrics_transcriber.output.cdgmaker.composer import KaraokeComposer
14
+ from lyrics_transcriber.output.cdgmaker.render import get_wrapped_text
15
+ from lyrics_transcriber.types import LyricsSegment
16
+
17
+
18
+ class CDGGenerator:
19
+ """Generates CD+G (CD Graphics) format karaoke files."""
20
+
21
+ def __init__(self, output_dir: str, logger: Optional[logging.Logger] = None):
22
+ """Initialize CDGGenerator.
23
+
24
+ Args:
25
+ output_dir: Directory where output files will be written
26
+ logger: Optional logger instance
27
+ """
28
+ self.output_dir = output_dir
29
+ self.logger = logger or logging.getLogger(__name__)
30
+ self.cdg_visible_width = 280
31
+
32
+ def _sanitize_filename(self, filename: str) -> str:
33
+ """Replace or remove characters that are unsafe for filenames."""
34
+ if not filename:
35
+ return ""
36
+ # Replace problematic characters with underscores
37
+ for char in ["\\", "/", ":", "*", "?", '"', "<", ">", "|"]:
38
+ filename = filename.replace(char, "_")
39
+ # Remove any trailing spaces
40
+ filename = filename.rstrip(" ")
41
+ return filename
42
+
43
+ def _get_safe_filename(self, artist: str, title: str, suffix: str = "", ext: str = "") -> str:
44
+ """Create a safe filename from artist and title."""
45
+ safe_artist = self._sanitize_filename(artist)
46
+ safe_title = self._sanitize_filename(title)
47
+ base = f"{safe_artist} - {safe_title}"
48
+ if suffix:
49
+ base += f" ({suffix})"
50
+ if ext:
51
+ base += f".{ext}"
52
+ return base
53
+
54
+ def generate_cdg(
55
+ self,
56
+ segments: List[LyricsSegment],
57
+ audio_file: str,
58
+ title: str,
59
+ artist: str,
60
+ cdg_styles: dict,
61
+ ) -> Tuple[str, str, str]:
62
+ """Generate a CDG file from lyrics segments and audio file.
63
+
64
+ Args:
65
+ segments: List of LyricsSegment objects containing timing and text
66
+ audio_file: Path to the audio file
67
+ title: Title of the song
68
+ artist: Artist name
69
+ cdg_styles: Dictionary containing CDG style parameters
70
+
71
+ Returns:
72
+ Tuple containing paths to (cdg_file, mp3_file, zip_file)
73
+ """
74
+ self._validate_and_setup_font(cdg_styles)
75
+
76
+ # Convert segments to the format expected by the rest of the code
77
+ lyrics_data = self._convert_segments_to_lyrics_data(segments)
78
+
79
+ toml_file = self._create_toml_file(
80
+ audio_file=audio_file,
81
+ title=title,
82
+ artist=artist,
83
+ lyrics_data=lyrics_data,
84
+ cdg_styles=cdg_styles,
85
+ )
86
+
87
+ try:
88
+ self._compose_cdg(toml_file)
89
+ output_zip = self._find_cdg_zip(artist, title)
90
+ self._extract_cdg_files(output_zip)
91
+
92
+ cdg_file = self._get_cdg_path(artist, title)
93
+ mp3_file = self._get_mp3_path(artist, title)
94
+
95
+ self._verify_output_files(cdg_file, mp3_file)
96
+
97
+ self.logger.info("CDG file generated successfully")
98
+ return cdg_file, mp3_file, output_zip
99
+
100
+ except Exception as e:
101
+ self.logger.error(f"Error composing CDG: {e}")
102
+ raise
103
+
104
+ def _convert_segments_to_lyrics_data(self, segments: List[LyricsSegment]) -> List[dict]:
105
+ """Convert LyricsSegment objects to the format needed for CDG generation."""
106
+ lyrics_data = []
107
+
108
+ for segment in segments:
109
+ # Convert each word to a lyric entry
110
+ for word in segment.words:
111
+ # Convert time from seconds to centiseconds
112
+ timestamp = int(word.start_time * 100)
113
+ lyrics_data.append({"timestamp": timestamp, "text": word.text.upper()}) # CDG format expects uppercase text
114
+ self.logger.debug(f"Added lyric: timestamp {timestamp}, text '{word.text}'")
115
+
116
+ # Sort by timestamp to ensure correct order
117
+ lyrics_data.sort(key=lambda x: x["timestamp"])
118
+ return lyrics_data
119
+
120
+ def _create_toml_file(
121
+ self,
122
+ audio_file: str,
123
+ title: str,
124
+ artist: str,
125
+ lyrics_data: List[dict],
126
+ cdg_styles: dict,
127
+ ) -> str:
128
+ """Create TOML configuration file for CDG generation."""
129
+ safe_filename = self._get_safe_filename(artist, title, "Karaoke", "toml")
130
+ toml_file = os.path.join(self.output_dir, safe_filename)
131
+ self.logger.debug(f"Generating TOML file: {toml_file}")
132
+
133
+ self.generate_toml(
134
+ audio_file=audio_file,
135
+ title=title,
136
+ artist=artist,
137
+ lyrics_data=lyrics_data,
138
+ output_file=toml_file,
139
+ cdg_styles=cdg_styles,
140
+ )
141
+ return toml_file
142
+
143
+ def generate_toml(
144
+ self,
145
+ audio_file: str,
146
+ title: str,
147
+ artist: str,
148
+ lyrics_data: List[dict],
149
+ output_file: str,
150
+ cdg_styles: dict,
151
+ ) -> None:
152
+ """Generate a TOML configuration file for CDG creation."""
153
+ audio_file = os.path.abspath(audio_file)
154
+ self.logger.debug(f"Using absolute audio file path: {audio_file}")
155
+
156
+ self._validate_cdg_styles(cdg_styles)
157
+ instrumentals = self._detect_instrumentals(lyrics_data, cdg_styles)
158
+ sync_times, formatted_lyrics = self._format_lyrics_data(lyrics_data, instrumentals, cdg_styles)
159
+
160
+ toml_data = self._create_toml_data(
161
+ title=title,
162
+ artist=artist,
163
+ audio_file=audio_file,
164
+ output_name=f"{artist} - {title} (Karaoke)",
165
+ sync_times=sync_times,
166
+ instrumentals=instrumentals,
167
+ formatted_lyrics=formatted_lyrics,
168
+ cdg_styles=cdg_styles,
169
+ )
170
+
171
+ self._write_toml_file(toml_data, output_file)
172
+
173
+ def _validate_and_setup_font(self, cdg_styles: dict) -> None:
174
+ """Validate and set up font path in CDG styles."""
175
+ if not cdg_styles.get("font_path"):
176
+ return
177
+
178
+ if not os.path.isabs(cdg_styles["font_path"]) and not os.path.exists(cdg_styles["font_path"]):
179
+ package_font_path = os.path.join(os.path.dirname(__file__), "fonts", cdg_styles["font_path"])
180
+ if os.path.exists(package_font_path):
181
+ cdg_styles["font_path"] = package_font_path
182
+ self.logger.debug(f"Found font in package fonts directory: {cdg_styles['font_path']}")
183
+ else:
184
+ self.logger.warning(
185
+ f"Font file {cdg_styles['font_path']} not found in package fonts directory {package_font_path}, will use default font"
186
+ )
187
+ cdg_styles["font_path"] = None
188
+
189
+ def _compose_cdg(self, toml_file: str) -> None:
190
+ """Compose CDG using KaraokeComposer."""
191
+ kc = KaraokeComposer.from_file(toml_file, logger=self.logger)
192
+ kc.compose()
193
+ # kc.create_mp4(height=1080, fps=30)
194
+
195
+ def _find_cdg_zip(self, artist: str, title: str) -> str:
196
+ """Find the generated CDG ZIP file."""
197
+ safe_filename = self._get_safe_filename(artist, title, "Karaoke", "zip")
198
+ output_zip = os.path.join(self.output_dir, safe_filename)
199
+
200
+ self.logger.info(f"Looking for CDG ZIP file in output directory: {output_zip}")
201
+
202
+ if os.path.isfile(output_zip):
203
+ self.logger.info(f"Found CDG ZIP file: {output_zip}")
204
+ return output_zip
205
+
206
+ self.logger.error("Failed to find CDG ZIP file. Output directory contents:")
207
+ for file in os.listdir(self.output_dir):
208
+ self.logger.error(f" - {file}")
209
+ raise FileNotFoundError(f"CDG ZIP file not found: {output_zip}")
210
+
211
+ def _extract_cdg_files(self, zip_path: str) -> None:
212
+ """Extract files from the CDG ZIP."""
213
+ self.logger.info(f"Extracting CDG ZIP file: {zip_path}")
214
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
215
+ zip_ref.extractall(self.output_dir)
216
+
217
+ def _get_cdg_path(self, artist: str, title: str) -> str:
218
+ """Get the path to the CDG file."""
219
+ safe_filename = self._get_safe_filename(artist, title, "Karaoke", "cdg")
220
+ return os.path.join(self.output_dir, safe_filename)
221
+
222
+ def _get_mp3_path(self, artist: str, title: str) -> str:
223
+ """Get the path to the MP3 file."""
224
+ safe_filename = self._get_safe_filename(artist, title, "Karaoke", "mp3")
225
+ return os.path.join(self.output_dir, safe_filename)
226
+
227
+ def _verify_output_files(self, cdg_file: str, mp3_file: str) -> None:
228
+ """Verify that the required output files exist."""
229
+ if not os.path.isfile(cdg_file):
230
+ raise FileNotFoundError(f"CDG file not found after extraction: {cdg_file}")
231
+ if not os.path.isfile(mp3_file):
232
+ raise FileNotFoundError(f"MP3 file not found after extraction: {mp3_file}")
233
+
234
+ def detect_instrumentals(
235
+ self,
236
+ lyrics_data,
237
+ line_tile_height,
238
+ instrumental_font_color,
239
+ instrumental_background,
240
+ instrumental_transition,
241
+ instrumental_gap_threshold,
242
+ instrumental_text,
243
+ ):
244
+ instrumentals = []
245
+ for i in range(len(lyrics_data) - 1):
246
+ current_end = lyrics_data[i]["timestamp"]
247
+ next_start = lyrics_data[i + 1]["timestamp"]
248
+ gap = next_start - current_end
249
+ if gap >= instrumental_gap_threshold:
250
+ instrumental_start = current_end + 200 # Add 2 seconds (200 centiseconds) delay
251
+ instrumental_duration = (gap - 200) // 100 # Convert to seconds
252
+ instrumentals.append(
253
+ {
254
+ "sync": instrumental_start,
255
+ "wait": True,
256
+ "text": f"{instrumental_text}\n{instrumental_duration} seconds\n",
257
+ "text_align": "center",
258
+ "text_placement": "bottom middle",
259
+ "line_tile_height": line_tile_height,
260
+ "fill": instrumental_font_color,
261
+ "stroke": "",
262
+ "image": instrumental_background,
263
+ "transition": instrumental_transition,
264
+ }
265
+ )
266
+ self.logger.info(
267
+ f"Detected instrumental: Gap of {gap} cs, starting at {instrumental_start} cs, duration {instrumental_duration} seconds"
268
+ )
269
+
270
+ self.logger.info(f"Total instrumentals detected: {len(instrumentals)}")
271
+ return instrumentals
272
+
273
+ def _validate_cdg_styles(self, cdg_styles: dict) -> None:
274
+ """Validate required style parameters are present."""
275
+ required_styles = {
276
+ "title_color",
277
+ "artist_color",
278
+ "background_color",
279
+ "border_color",
280
+ "font_path",
281
+ "font_size",
282
+ "stroke_width",
283
+ "stroke_style",
284
+ "active_fill",
285
+ "active_stroke",
286
+ "inactive_fill",
287
+ "inactive_stroke",
288
+ "title_screen_background",
289
+ "instrumental_background",
290
+ "instrumental_transition",
291
+ "instrumental_font_color",
292
+ "title_screen_transition",
293
+ "row",
294
+ "line_tile_height",
295
+ "lines_per_page",
296
+ "clear_mode",
297
+ "sync_offset",
298
+ "instrumental_gap_threshold",
299
+ "instrumental_text",
300
+ "lead_in_threshold",
301
+ "lead_in_symbols",
302
+ "lead_in_duration",
303
+ "lead_in_total",
304
+ "title_artist_gap",
305
+ "title_top_padding",
306
+ "intro_duration_seconds",
307
+ "first_syllable_buffer_seconds",
308
+ "outro_background",
309
+ "outro_transition",
310
+ "outro_text_line1",
311
+ "outro_text_line2",
312
+ "outro_line1_color",
313
+ "outro_line2_color",
314
+ "outro_line1_line2_gap",
315
+ }
316
+
317
+ optional_styles_with_defaults = {
318
+ "title_top_padding": 0,
319
+ # Any other optional parameters with their default values
320
+ }
321
+
322
+ # Add any missing optional parameters with their default values
323
+ for key, default_value in optional_styles_with_defaults.items():
324
+ if key not in cdg_styles:
325
+ cdg_styles[key] = default_value
326
+
327
+ missing_styles = required_styles - set(cdg_styles.keys())
328
+ if missing_styles:
329
+ raise ValueError(f"Missing required style parameters: {', '.join(missing_styles)}")
330
+
331
+ def _detect_instrumentals(self, lyrics_data: List[dict], cdg_styles: dict) -> List[dict]:
332
+ """Detect instrumental sections in lyrics."""
333
+ return self.detect_instrumentals(
334
+ lyrics_data=lyrics_data,
335
+ line_tile_height=cdg_styles["line_tile_height"],
336
+ instrumental_font_color=cdg_styles["instrumental_font_color"],
337
+ instrumental_background=cdg_styles["instrumental_background"],
338
+ instrumental_transition=cdg_styles["instrumental_transition"],
339
+ instrumental_gap_threshold=cdg_styles["instrumental_gap_threshold"],
340
+ instrumental_text=cdg_styles["instrumental_text"],
341
+ )
342
+
343
+ def _format_lyrics_data(self, lyrics_data: List[dict], instrumentals: List[dict], cdg_styles: dict) -> tuple[List[int], List[str]]:
344
+ """Format lyrics data with lead-in symbols and handle line wrapping.
345
+
346
+ Returns:
347
+ tuple: (sync_times, formatted_lyrics) where sync_times includes lead-in timings
348
+ """
349
+ sync_times = []
350
+ formatted_lyrics = []
351
+
352
+ for i, lyric in enumerate(lyrics_data):
353
+ self.logger.debug(f"Processing lyric {i}: timestamp {lyric['timestamp']}, text '{lyric['text']}'")
354
+
355
+ if i == 0 or lyric["timestamp"] - lyrics_data[i - 1]["timestamp"] >= cdg_styles["lead_in_threshold"]:
356
+ lead_in_start = lyric["timestamp"] - cdg_styles["lead_in_total"]
357
+ self.logger.debug(f"Adding lead-in before lyric {i} at timestamp {lead_in_start}")
358
+ for j, symbol in enumerate(cdg_styles["lead_in_symbols"]):
359
+ sync_time = lead_in_start + j * cdg_styles["lead_in_duration"]
360
+ sync_times.append(sync_time)
361
+ formatted_lyrics.append(symbol)
362
+ self.logger.debug(f" Added lead-in symbol {j+1}: '{symbol}' at {sync_time}")
363
+
364
+ sync_times.append(lyric["timestamp"])
365
+ formatted_lyrics.append(lyric["text"])
366
+ self.logger.debug(f"Added lyric: '{lyric['text']}' at {lyric['timestamp']}")
367
+
368
+ formatted_text = self.format_lyrics(
369
+ formatted_lyrics,
370
+ instrumentals,
371
+ sync_times,
372
+ font_path=cdg_styles["font_path"],
373
+ font_size=cdg_styles["font_size"],
374
+ )
375
+
376
+ return sync_times, formatted_text
377
+
378
+ def _create_toml_data(
379
+ self,
380
+ title: str,
381
+ artist: str,
382
+ audio_file: str,
383
+ output_name: str,
384
+ sync_times: List[int],
385
+ instrumentals: List[dict],
386
+ formatted_lyrics: List[str],
387
+ cdg_styles: dict,
388
+ ) -> dict:
389
+ """Create TOML data structure."""
390
+ safe_output_name = self._get_safe_filename(artist, title, "Karaoke")
391
+ return {
392
+ "title": title,
393
+ "artist": artist,
394
+ "file": audio_file,
395
+ "outname": safe_output_name,
396
+ "clear_mode": cdg_styles["clear_mode"],
397
+ "sync_offset": cdg_styles["sync_offset"],
398
+ "background": cdg_styles["background_color"],
399
+ "border": cdg_styles["border_color"],
400
+ "font": cdg_styles["font_path"],
401
+ "font_size": cdg_styles["font_size"],
402
+ "stroke_width": cdg_styles["stroke_width"],
403
+ "stroke_style": cdg_styles["stroke_style"],
404
+ "singers": [
405
+ {
406
+ "active_fill": cdg_styles["active_fill"],
407
+ "active_stroke": cdg_styles["active_stroke"],
408
+ "inactive_fill": cdg_styles["inactive_fill"],
409
+ "inactive_stroke": cdg_styles["inactive_stroke"],
410
+ }
411
+ ],
412
+ "lyrics": [
413
+ {
414
+ "singer": 1,
415
+ "sync": sync_times,
416
+ "row": cdg_styles["row"],
417
+ "line_tile_height": cdg_styles["line_tile_height"],
418
+ "lines_per_page": cdg_styles["lines_per_page"],
419
+ "text": formatted_lyrics,
420
+ }
421
+ ],
422
+ "title_color": cdg_styles["title_color"],
423
+ "artist_color": cdg_styles["artist_color"],
424
+ "title_screen_background": cdg_styles["title_screen_background"],
425
+ "title_screen_transition": cdg_styles["title_screen_transition"],
426
+ "instrumentals": instrumentals,
427
+ "intro_duration_seconds": cdg_styles["intro_duration_seconds"],
428
+ "title_top_padding": cdg_styles["title_top_padding"],
429
+ "title_artist_gap": cdg_styles["title_artist_gap"],
430
+ "first_syllable_buffer_seconds": cdg_styles["first_syllable_buffer_seconds"],
431
+ "outro_background": cdg_styles["outro_background"],
432
+ "outro_transition": cdg_styles["outro_transition"],
433
+ "outro_text_line1": cdg_styles["outro_text_line1"],
434
+ "outro_text_line2": cdg_styles["outro_text_line2"],
435
+ "outro_line1_color": cdg_styles["outro_line1_color"],
436
+ "outro_line2_color": cdg_styles["outro_line2_color"],
437
+ "outro_line1_line2_gap": cdg_styles["outro_line1_line2_gap"],
438
+ }
439
+
440
+ def _write_toml_file(self, toml_data: dict, output_file: str) -> None:
441
+ """Write TOML data to file."""
442
+ with open(output_file, "w", encoding="utf-8") as f:
443
+ toml.dump(toml_data, f)
444
+ self.logger.info(f"TOML file generated: {output_file}")
445
+
446
+ def get_font(self, font_path=None, font_size=18):
447
+ try:
448
+ return ImageFont.truetype(font_path, font_size) if font_path else ImageFont.load_default()
449
+ except IOError:
450
+ self.logger.warning(f"Font file {font_path} not found. Using default font.")
451
+ return ImageFont.load_default()
452
+
453
+ def get_text_width(self, text, font):
454
+ return font.getmask(text).getbbox()[2]
455
+
456
+ def wrap_text(self, text, max_width, font):
457
+ words = text.split()
458
+ lines = []
459
+ current_line = []
460
+ current_width = 0
461
+
462
+ for word in words:
463
+ word_width = self.get_text_width(word, font)
464
+ if current_width + word_width <= max_width:
465
+ current_line.append(word)
466
+ current_width += word_width + self.get_text_width(" ", font)
467
+ else:
468
+ if current_line:
469
+ lines.append(" ".join(current_line))
470
+ self.logger.debug(f"Wrapped line: {' '.join(current_line)}")
471
+ current_line = [word]
472
+ current_width = word_width
473
+
474
+ if current_line:
475
+ lines.append(" ".join(current_line))
476
+ self.logger.debug(f"Wrapped line: {' '.join(current_line)}")
477
+
478
+ return lines
479
+
480
+ def format_lyrics(self, lyrics_data, instrumentals, sync_times, font_path=None, font_size=18):
481
+ formatted_lyrics = []
482
+ font = self.get_font(font_path, font_size)
483
+ self.logger.debug(f"Using font: {font}")
484
+
485
+ current_line = ""
486
+ lines_on_page = 0
487
+ page_number = 1
488
+
489
+ for i, text in enumerate(lyrics_data):
490
+ self.logger.debug(f"format_lyrics: Processing text {i}: '{text}' (sync time: {sync_times[i]})")
491
+
492
+ if text.startswith("/"):
493
+ if current_line:
494
+ wrapped_lines = get_wrapped_text(current_line.strip(), font, CDG_VISIBLE_WIDTH).split("\n")
495
+ for wrapped_line in wrapped_lines:
496
+ formatted_lyrics.append(wrapped_line)
497
+ lines_on_page += 1
498
+ self.logger.debug(f"format_lyrics: Added wrapped line: '{wrapped_line}'. Lines on page: {lines_on_page}")
499
+ # Add empty line after punctuation immediately
500
+ if wrapped_line.endswith(("!", "?", ".")) and not wrapped_line == "~":
501
+ formatted_lyrics.append("~")
502
+ lines_on_page += 1
503
+ self.logger.debug(f"format_lyrics: Added empty line after punctuation. Lines on page now: {lines_on_page}")
504
+ if lines_on_page == 4:
505
+ lines_on_page = 0
506
+ page_number += 1
507
+ self.logger.debug(f"format_lyrics: Page full. New page number: {page_number}")
508
+ current_line = ""
509
+ text = text[1:]
510
+
511
+ current_line += text + " "
512
+ # self.logger.debug(f"format_lyrics: Current line: '{current_line}'")
513
+
514
+ is_last_before_instrumental = any(
515
+ inst["sync"] > sync_times[i] and (i == len(sync_times) - 1 or sync_times[i + 1] > inst["sync"]) for inst in instrumentals
516
+ )
517
+
518
+ if is_last_before_instrumental or i == len(lyrics_data) - 1:
519
+ if current_line:
520
+ wrapped_lines = get_wrapped_text(current_line.strip(), font, CDG_VISIBLE_WIDTH).split("\n")
521
+ for wrapped_line in wrapped_lines:
522
+ formatted_lyrics.append(wrapped_line)
523
+ lines_on_page += 1
524
+ self.logger.debug(
525
+ f"format_lyrics: Added wrapped line at end of section: '{wrapped_line}'. Lines on page: {lines_on_page}"
526
+ )
527
+ if lines_on_page == 4:
528
+ lines_on_page = 0
529
+ page_number += 1
530
+ self.logger.debug(f"format_lyrics: Page full. New page number: {page_number}")
531
+ current_line = ""
532
+
533
+ if is_last_before_instrumental:
534
+ self.logger.debug(f"format_lyrics: is_last_before_instrumental: True lines_on_page: {lines_on_page}")
535
+ # Calculate remaining lines needed to reach next full page
536
+ remaining_lines = 4 - (lines_on_page % 4) if lines_on_page % 4 != 0 else 0
537
+ if remaining_lines > 0:
538
+ formatted_lyrics.extend(["~"] * remaining_lines)
539
+ self.logger.debug(f"format_lyrics: Added {remaining_lines} empty lines to complete current page")
540
+
541
+ # Reset the counter and increment page
542
+ lines_on_page = 0
543
+ page_number += 1
544
+ self.logger.debug(f"format_lyrics: Reset lines_on_page to 0. New page number: {page_number}")
545
+
546
+ return "\n".join(formatted_lyrics)
547
+
548
+ def generate_cdg_from_lrc(
549
+ self,
550
+ lrc_file: str,
551
+ audio_file: str,
552
+ title: str,
553
+ artist: str,
554
+ cdg_styles: dict,
555
+ ) -> Tuple[str, str, str]:
556
+ """Generate a CDG file from an LRC file and audio file.
557
+
558
+ Args:
559
+ lrc_file: Path to the LRC file
560
+ audio_file: Path to the audio file
561
+ title: Title of the song
562
+ artist: Artist name
563
+ cdg_styles: Dictionary containing CDG style parameters
564
+
565
+ Returns:
566
+ Tuple containing paths to (cdg_file, mp3_file, zip_file)
567
+ """
568
+ self._validate_and_setup_font(cdg_styles)
569
+
570
+ # Parse LRC file and convert to lyrics_data format
571
+ lyrics_data = self._parse_lrc(lrc_file)
572
+
573
+ toml_file = self._create_toml_file(
574
+ audio_file=audio_file,
575
+ title=title,
576
+ artist=artist,
577
+ lyrics_data=lyrics_data,
578
+ cdg_styles=cdg_styles,
579
+ )
580
+
581
+ try:
582
+ self._compose_cdg(toml_file)
583
+ output_zip = self._find_cdg_zip(artist, title)
584
+ self._extract_cdg_files(output_zip)
585
+
586
+ cdg_file = self._get_cdg_path(artist, title)
587
+ mp3_file = self._get_mp3_path(artist, title)
588
+
589
+ self._verify_output_files(cdg_file, mp3_file)
590
+
591
+ self.logger.info("CDG file generated successfully")
592
+ return cdg_file, mp3_file, output_zip
593
+
594
+ except Exception as e:
595
+ self.logger.error(f"Error composing CDG: {e}")
596
+ raise
597
+
598
+ def _parse_lrc(self, lrc_file: str) -> List[dict]:
599
+ """Parse LRC file and extract timestamps and lyrics."""
600
+ with open(lrc_file, "r", encoding="utf-8") as f:
601
+ content = f.read()
602
+
603
+ # Extract timestamps and lyrics
604
+ pattern = r"\[(\d{2}):(\d{2})\.(\d{3})\](\d+:)?(/?.*)"
605
+ matches = re.findall(pattern, content)
606
+
607
+ if not matches:
608
+ raise ValueError(f"No valid lyrics found in the LRC file: {lrc_file}")
609
+
610
+ lyrics = []
611
+ for match in matches:
612
+ minutes, seconds, milliseconds = map(int, match[:3])
613
+ timestamp = (minutes * 60 + seconds) * 100 + int(milliseconds / 10) # Convert to centiseconds
614
+ text = match[4].strip().upper()
615
+ if text: # Only add non-empty lyrics
616
+ lyrics.append({"timestamp": timestamp, "text": text})
617
+
618
+ self.logger.info(f"Found {len(lyrics)} lyric lines")
619
+ return lyrics
File without changes