karaoke-gen 0.75.54__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (287) hide show
  1. karaoke_gen/__init__.py +38 -0
  2. karaoke_gen/audio_fetcher.py +1614 -0
  3. karaoke_gen/audio_processor.py +790 -0
  4. karaoke_gen/config.py +83 -0
  5. karaoke_gen/file_handler.py +387 -0
  6. karaoke_gen/instrumental_review/__init__.py +45 -0
  7. karaoke_gen/instrumental_review/analyzer.py +408 -0
  8. karaoke_gen/instrumental_review/editor.py +322 -0
  9. karaoke_gen/instrumental_review/models.py +171 -0
  10. karaoke_gen/instrumental_review/server.py +475 -0
  11. karaoke_gen/instrumental_review/static/index.html +1529 -0
  12. karaoke_gen/instrumental_review/waveform.py +409 -0
  13. karaoke_gen/karaoke_finalise/__init__.py +1 -0
  14. karaoke_gen/karaoke_finalise/karaoke_finalise.py +1833 -0
  15. karaoke_gen/karaoke_gen.py +1026 -0
  16. karaoke_gen/lyrics_processor.py +474 -0
  17. karaoke_gen/metadata.py +160 -0
  18. karaoke_gen/pipeline/__init__.py +87 -0
  19. karaoke_gen/pipeline/base.py +215 -0
  20. karaoke_gen/pipeline/context.py +230 -0
  21. karaoke_gen/pipeline/executors/__init__.py +21 -0
  22. karaoke_gen/pipeline/executors/local.py +159 -0
  23. karaoke_gen/pipeline/executors/remote.py +257 -0
  24. karaoke_gen/pipeline/stages/__init__.py +27 -0
  25. karaoke_gen/pipeline/stages/finalize.py +202 -0
  26. karaoke_gen/pipeline/stages/render.py +165 -0
  27. karaoke_gen/pipeline/stages/screens.py +139 -0
  28. karaoke_gen/pipeline/stages/separation.py +191 -0
  29. karaoke_gen/pipeline/stages/transcription.py +191 -0
  30. karaoke_gen/resources/AvenirNext-Bold.ttf +0 -0
  31. karaoke_gen/resources/Montserrat-Bold.ttf +0 -0
  32. karaoke_gen/resources/Oswald-Bold.ttf +0 -0
  33. karaoke_gen/resources/Oswald-SemiBold.ttf +0 -0
  34. karaoke_gen/resources/Zurich_Cn_BT_Bold.ttf +0 -0
  35. karaoke_gen/style_loader.py +531 -0
  36. karaoke_gen/utils/__init__.py +18 -0
  37. karaoke_gen/utils/bulk_cli.py +492 -0
  38. karaoke_gen/utils/cli_args.py +432 -0
  39. karaoke_gen/utils/gen_cli.py +978 -0
  40. karaoke_gen/utils/remote_cli.py +3268 -0
  41. karaoke_gen/video_background_processor.py +351 -0
  42. karaoke_gen/video_generator.py +424 -0
  43. karaoke_gen-0.75.54.dist-info/METADATA +718 -0
  44. karaoke_gen-0.75.54.dist-info/RECORD +287 -0
  45. karaoke_gen-0.75.54.dist-info/WHEEL +4 -0
  46. karaoke_gen-0.75.54.dist-info/entry_points.txt +5 -0
  47. karaoke_gen-0.75.54.dist-info/licenses/LICENSE +21 -0
  48. lyrics_transcriber/__init__.py +10 -0
  49. lyrics_transcriber/cli/__init__.py +0 -0
  50. lyrics_transcriber/cli/cli_main.py +285 -0
  51. lyrics_transcriber/core/__init__.py +0 -0
  52. lyrics_transcriber/core/config.py +50 -0
  53. lyrics_transcriber/core/controller.py +594 -0
  54. lyrics_transcriber/correction/__init__.py +0 -0
  55. lyrics_transcriber/correction/agentic/__init__.py +9 -0
  56. lyrics_transcriber/correction/agentic/adapter.py +71 -0
  57. lyrics_transcriber/correction/agentic/agent.py +313 -0
  58. lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
  59. lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
  60. lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
  61. lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
  62. lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
  63. lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
  64. lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
  65. lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
  66. lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
  67. lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
  68. lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
  69. lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
  70. lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
  71. lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
  72. lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
  73. lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
  74. lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
  75. lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
  76. lyrics_transcriber/correction/agentic/models/enums.py +38 -0
  77. lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
  78. lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
  79. lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
  80. lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
  81. lyrics_transcriber/correction/agentic/models/utils.py +19 -0
  82. lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
  83. lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
  84. lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
  85. lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
  86. lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
  87. lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
  88. lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
  89. lyrics_transcriber/correction/agentic/providers/base.py +36 -0
  90. lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
  91. lyrics_transcriber/correction/agentic/providers/config.py +73 -0
  92. lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
  93. lyrics_transcriber/correction/agentic/providers/health.py +28 -0
  94. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
  95. lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
  96. lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
  97. lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
  98. lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
  99. lyrics_transcriber/correction/agentic/router.py +35 -0
  100. lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
  101. lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
  102. lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
  103. lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
  104. lyrics_transcriber/correction/anchor_sequence.py +919 -0
  105. lyrics_transcriber/correction/corrector.py +760 -0
  106. lyrics_transcriber/correction/feedback/__init__.py +2 -0
  107. lyrics_transcriber/correction/feedback/schemas.py +107 -0
  108. lyrics_transcriber/correction/feedback/store.py +236 -0
  109. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  110. lyrics_transcriber/correction/handlers/base.py +52 -0
  111. lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
  112. lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
  113. lyrics_transcriber/correction/handlers/llm.py +293 -0
  114. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  115. lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
  116. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
  117. lyrics_transcriber/correction/handlers/repeat.py +88 -0
  118. lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
  119. lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
  120. lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
  121. lyrics_transcriber/correction/handlers/word_operations.py +187 -0
  122. lyrics_transcriber/correction/operations.py +352 -0
  123. lyrics_transcriber/correction/phrase_analyzer.py +435 -0
  124. lyrics_transcriber/correction/text_utils.py +30 -0
  125. lyrics_transcriber/frontend/.gitignore +23 -0
  126. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
  127. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  128. lyrics_transcriber/frontend/README.md +50 -0
  129. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
  130. lyrics_transcriber/frontend/__init__.py +25 -0
  131. lyrics_transcriber/frontend/eslint.config.js +28 -0
  132. lyrics_transcriber/frontend/index.html +18 -0
  133. lyrics_transcriber/frontend/package.json +42 -0
  134. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  135. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  136. lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
  137. lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
  138. lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
  139. lyrics_transcriber/frontend/public/favicon.ico +0 -0
  140. lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
  141. lyrics_transcriber/frontend/src/App.tsx +214 -0
  142. lyrics_transcriber/frontend/src/api.ts +254 -0
  143. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
  144. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  145. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
  146. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
  147. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
  148. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
  149. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
  150. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
  151. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
  152. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  153. lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
  154. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
  155. lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
  156. lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
  157. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  158. lyrics_transcriber/frontend/src/components/Header.tsx +413 -0
  159. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1387 -0
  160. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +185 -0
  161. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +704 -0
  162. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +80 -0
  163. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +905 -0
  164. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
  165. lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +127 -0
  166. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
  167. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
  168. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
  169. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
  170. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +336 -0
  171. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
  172. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
  173. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
  174. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
  175. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
  176. lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
  177. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
  178. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
  179. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
  180. lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
  181. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
  182. lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
  183. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  184. lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
  185. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
  186. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  187. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
  188. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
  189. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
  190. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  191. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
  192. lyrics_transcriber/frontend/src/main.tsx +17 -0
  193. lyrics_transcriber/frontend/src/theme.ts +177 -0
  194. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  195. lyrics_transcriber/frontend/src/types.js +2 -0
  196. lyrics_transcriber/frontend/src/types.ts +199 -0
  197. lyrics_transcriber/frontend/src/validation.ts +132 -0
  198. lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
  199. lyrics_transcriber/frontend/tsconfig.app.json +26 -0
  200. lyrics_transcriber/frontend/tsconfig.json +25 -0
  201. lyrics_transcriber/frontend/tsconfig.node.json +23 -0
  202. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
  203. lyrics_transcriber/frontend/update_version.js +11 -0
  204. lyrics_transcriber/frontend/vite.config.d.ts +2 -0
  205. lyrics_transcriber/frontend/vite.config.js +10 -0
  206. lyrics_transcriber/frontend/vite.config.ts +11 -0
  207. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  208. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  209. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  210. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js +43288 -0
  211. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +1 -0
  212. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  213. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  214. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  215. lyrics_transcriber/frontend/web_assets/index.html +18 -0
  216. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  217. lyrics_transcriber/frontend/yarn.lock +3752 -0
  218. lyrics_transcriber/lyrics/__init__.py +0 -0
  219. lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
  220. lyrics_transcriber/lyrics/file_provider.py +95 -0
  221. lyrics_transcriber/lyrics/genius.py +384 -0
  222. lyrics_transcriber/lyrics/lrclib.py +231 -0
  223. lyrics_transcriber/lyrics/musixmatch.py +156 -0
  224. lyrics_transcriber/lyrics/spotify.py +290 -0
  225. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  226. lyrics_transcriber/output/__init__.py +0 -0
  227. lyrics_transcriber/output/ass/__init__.py +21 -0
  228. lyrics_transcriber/output/ass/ass.py +2088 -0
  229. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  230. lyrics_transcriber/output/ass/config.py +180 -0
  231. lyrics_transcriber/output/ass/constants.py +23 -0
  232. lyrics_transcriber/output/ass/event.py +94 -0
  233. lyrics_transcriber/output/ass/formatters.py +132 -0
  234. lyrics_transcriber/output/ass/lyrics_line.py +265 -0
  235. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  236. lyrics_transcriber/output/ass/section_detector.py +89 -0
  237. lyrics_transcriber/output/ass/section_screen.py +106 -0
  238. lyrics_transcriber/output/ass/style.py +187 -0
  239. lyrics_transcriber/output/cdg.py +619 -0
  240. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  241. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  242. lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
  243. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  244. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  245. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  246. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  247. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  248. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  249. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  250. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  251. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  252. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  253. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  254. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  255. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  256. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  257. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  258. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  259. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  260. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  261. lyrics_transcriber/output/countdown_processor.py +306 -0
  262. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  263. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  264. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  265. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  266. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  267. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  268. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  269. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  270. lyrics_transcriber/output/generator.py +257 -0
  271. lyrics_transcriber/output/lrc_to_cdg.py +61 -0
  272. lyrics_transcriber/output/lyrics_file.py +102 -0
  273. lyrics_transcriber/output/plain_text.py +96 -0
  274. lyrics_transcriber/output/segment_resizer.py +431 -0
  275. lyrics_transcriber/output/subtitles.py +397 -0
  276. lyrics_transcriber/output/video.py +544 -0
  277. lyrics_transcriber/review/__init__.py +0 -0
  278. lyrics_transcriber/review/server.py +676 -0
  279. lyrics_transcriber/storage/__init__.py +0 -0
  280. lyrics_transcriber/storage/dropbox.py +225 -0
  281. lyrics_transcriber/transcribers/__init__.py +0 -0
  282. lyrics_transcriber/transcribers/audioshake.py +379 -0
  283. lyrics_transcriber/transcribers/base_transcriber.py +157 -0
  284. lyrics_transcriber/transcribers/whisper.py +330 -0
  285. lyrics_transcriber/types.py +650 -0
  286. lyrics_transcriber/utils/__init__.py +0 -0
  287. lyrics_transcriber/utils/word_utils.py +27 -0
@@ -0,0 +1,424 @@
1
+ import os
2
+ import logging
3
+ import importlib.resources as pkg_resources
4
+ import shutil
5
+ from PIL import Image, ImageDraw, ImageFont
6
+
7
+
8
+ # Placeholder class or functions for video/image generation
9
+ class VideoGenerator:
10
+ def __init__(self, logger, ffmpeg_base_command, render_bounding_boxes, output_png, output_jpg):
11
+ self.logger = logger
12
+ self.ffmpeg_base_command = ffmpeg_base_command
13
+ self.render_bounding_boxes = render_bounding_boxes
14
+ self.output_png = output_png
15
+ self.output_jpg = output_jpg
16
+
17
+ def parse_region(self, region_str):
18
+ if region_str:
19
+ try:
20
+ parts = region_str.split(",")
21
+ if len(parts) != 4:
22
+ raise ValueError(f"Invalid region format: {region_str}. Expected 4 elements: 'x,y,width,height'")
23
+ return tuple(map(int, parts))
24
+ except ValueError as e:
25
+ # Re-raise specific format errors or general ValueError for int conversion issues
26
+ if "Expected 4 elements" in str(e):
27
+ raise e
28
+ raise ValueError(f"Invalid region format: {region_str}. Could not convert to integers. Expected format: 'x,y,width,height'") from e
29
+ return None
30
+
31
+ def hex_to_rgb(self, hex_color):
32
+ """Convert hex color to RGB tuple."""
33
+ hex_color = hex_color.lstrip("#")
34
+ return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
35
+
36
+ # Placeholder methods - to be filled by user moving code
37
+ def create_video(
38
+ self,
39
+ extra_text,
40
+ title_text,
41
+ artist_text,
42
+ format,
43
+ output_image_filepath_noext,
44
+ output_video_filepath,
45
+ existing_image=None,
46
+ duration=5,
47
+ ):
48
+ """Create a video with title, artist, and optional extra text."""
49
+ self.logger.debug(f"Creating video with extra_text: '{extra_text}'")
50
+ self.logger.debug(f"Format settings: {format}")
51
+
52
+ resolution = (3840, 2160) # 4K resolution
53
+ self.logger.info(f"Creating video with format: {format}")
54
+ self.logger.info(f"extra_text: {extra_text}, artist_text: {artist_text}, title_text: {title_text}")
55
+
56
+ if existing_image:
57
+ return self._handle_existing_image(existing_image, output_image_filepath_noext, output_video_filepath, duration)
58
+
59
+ # Create or load background
60
+ background = self._create_background(format, resolution)
61
+ draw = ImageDraw.Draw(background)
62
+
63
+ if format["font"] is not None:
64
+ self.logger.info(f"Using font: {format['font']}")
65
+ # Check if the font path is absolute
66
+ if os.path.isabs(format["font"]):
67
+ font_path = format["font"]
68
+ if not os.path.exists(font_path):
69
+ self.logger.warning(f"Font file not found at {font_path}, falling back to default font")
70
+ font_path = None
71
+ else:
72
+ # Try to load from package resources
73
+ try:
74
+ with pkg_resources.path("karaoke_gen.resources", format["font"]) as font_path:
75
+ font_path = str(font_path)
76
+ except Exception as e:
77
+ self.logger.warning(f"Could not load font from resources: {e}, falling back to default font")
78
+ font_path = None
79
+
80
+ # Render all text elements
81
+ self._render_all_text(
82
+ draw,
83
+ font_path,
84
+ title_text,
85
+ artist_text,
86
+ format,
87
+ self.render_bounding_boxes,
88
+ )
89
+ else:
90
+ self.logger.info("No font specified, skipping text rendering")
91
+
92
+ # Save images and create video
93
+ self._save_output_files(
94
+ background, output_image_filepath_noext, output_video_filepath, duration, resolution
95
+ )
96
+
97
+ def calculate_text_size_to_fit(self, draw, text, font_path, region):
98
+ font_size = 500 # Start with a large font size
99
+ font = ImageFont.truetype(font_path, size=font_size) if font_path and os.path.exists(font_path) else ImageFont.load_default()
100
+
101
+ def get_text_size(text, font):
102
+ bbox = draw.textbbox((0, 0), text, font=font)
103
+ # Use the actual text height without the font's internal padding
104
+ return bbox[2], bbox[3] - bbox[1]
105
+
106
+ text_width, text_height = get_text_size(text, font)
107
+ target_height = region[3] # Use full region height as target
108
+
109
+ while text_width > region[2] or text_height > target_height:
110
+ font_size -= 10
111
+ if font_size <= 150:
112
+ # Split the text into two lines
113
+ words = text.split()
114
+ mid = len(words) // 2
115
+ line1 = " ".join(words[:mid])
116
+ line2 = " ".join(words[mid:])
117
+
118
+ # Reset font size for two-line layout
119
+ font_size = 500
120
+ font = ImageFont.truetype(font_path, size=font_size) if font_path and os.path.exists(font_path) else ImageFont.load_default()
121
+
122
+ while True:
123
+ text_width1, text_height1 = get_text_size(line1, font)
124
+ text_width2, text_height2 = get_text_size(line2, font)
125
+ total_height = text_height1 + text_height2
126
+
127
+ # Add a small gap between lines (10% of line height)
128
+ line_gap = text_height1 * 0.1
129
+ total_height_with_gap = total_height + line_gap
130
+
131
+ if max(text_width1, text_width2) <= region[2] and total_height_with_gap <= target_height:
132
+ return font, (line1, line2)
133
+
134
+ font_size -= 10
135
+ if font_size <= 0:
136
+ raise ValueError("Cannot fit text within the defined region.")
137
+ font = ImageFont.truetype(font_path, size=font_size) if font_path and os.path.exists(font_path) else ImageFont.load_default()
138
+
139
+ font = ImageFont.truetype(font_path, size=font_size) if font_path and os.path.exists(font_path) else ImageFont.load_default()
140
+ text_width, text_height = get_text_size(text, font)
141
+
142
+ return font, text
143
+
144
+ def _render_text_in_region(self, draw, text, font_path, region, color, gradient=None, font=None):
145
+ """Helper method to render text within a specified region."""
146
+ self.logger.debug(f"Rendering text: '{text}' in region: {region} with color: {color} gradient: {gradient}")
147
+
148
+ if text is None:
149
+ self.logger.debug("Text is None, skipping rendering")
150
+ return region
151
+
152
+ if region is None:
153
+ self.logger.debug("Region is None, skipping rendering")
154
+ return region
155
+
156
+ if font is None:
157
+ font, text_lines = self.calculate_text_size_to_fit(draw, text, font_path, region)
158
+ else:
159
+ text_lines = text
160
+
161
+ self.logger.debug(f"Using text_lines: {text_lines}")
162
+
163
+ x, y, width, height = region
164
+
165
+ # Get font metrics
166
+ ascent, descent = font.getmetrics()
167
+ font_height = ascent + descent
168
+
169
+ def render_text_with_gradient(text, position, bbox):
170
+ # Convert position coordinates to integers
171
+ position = (int(position[0]), int(position[1]))
172
+
173
+ if gradient is None:
174
+ draw.text(position, text, fill=color, font=font)
175
+ else:
176
+ # Create a temporary image for this text
177
+ text_layer = Image.new("RGBA", (bbox[2], bbox[3]), (0, 0, 0, 0))
178
+ text_draw = ImageDraw.Draw(text_layer)
179
+
180
+ # Draw text in first color
181
+ text_draw.text((0, 0), text, fill=gradient["color1"], font=font)
182
+
183
+ # Create and apply gradient mask
184
+ mask = self._create_gradient_mask((bbox[2], bbox[3]), gradient)
185
+
186
+ # Create second color layer
187
+ color2_layer = Image.new("RGBA", (bbox[2], bbox[3]), (0, 0, 0, 0))
188
+ color2_draw = ImageDraw.Draw(color2_layer)
189
+ color2_draw.text((0, 0), text, fill=gradient["color2"], font=font)
190
+
191
+ # Composite using gradient mask
192
+ text_layer.paste(color2_layer, mask=mask)
193
+
194
+ # Paste onto main image
195
+ draw._image.paste(text_layer, position, text_layer)
196
+
197
+ if isinstance(text_lines, tuple): # Two lines
198
+ line1, line2 = text_lines
199
+ bbox1 = draw.textbbox((0, 0), line1, font=font)
200
+ bbox2 = draw.textbbox((0, 0), line2, font=font)
201
+
202
+ # Calculate line heights using bounding boxes
203
+ line1_height = bbox1[3] - bbox1[1]
204
+ line2_height = bbox2[3] - bbox2[1]
205
+
206
+ # Use a small gap between lines (20% of average line height)
207
+ line_gap = int((line1_height + line2_height) * 0.1)
208
+
209
+ # Calculate total height needed
210
+ total_height = line1_height + line_gap + line2_height
211
+
212
+ # Center the entire text block vertically in the region
213
+ y_start = y + (height - total_height) // 2
214
+
215
+ # Draw first line
216
+ pos1 = (x + (width - bbox1[2]) // 2, y_start)
217
+ render_text_with_gradient(line1, pos1, bbox1)
218
+
219
+ # Draw second line
220
+ pos2 = (x + (width - bbox2[2]) // 2, y_start + line1_height + line_gap)
221
+ render_text_with_gradient(line2, pos2, bbox2)
222
+ else:
223
+ # Single line
224
+ bbox = draw.textbbox((0, 0), text_lines, font=font)
225
+
226
+ # Center text vertically using font metrics
227
+ y_pos = y + (height - font_height) // 2
228
+
229
+ position = (x + (width - bbox[2]) // 2, y_pos)
230
+ render_text_with_gradient(text_lines, position, bbox)
231
+
232
+ return region
233
+
234
+ def _draw_bounding_box(self, draw, region, color):
235
+ """Helper method to draw a bounding box around a region."""
236
+ if region is None:
237
+ self.logger.debug("Region is None, skipping drawing bounding box")
238
+ return
239
+
240
+ x, y, width, height = region
241
+ draw.rectangle([x, y, x + width, y + height], outline=color, width=2)
242
+
243
+ def _create_gradient_mask(self, size, gradient_config):
244
+ """Create a gradient mask for text coloring.
245
+
246
+ Args:
247
+ size (tuple): (width, height) of the mask
248
+ gradient_config (dict): Configuration with keys:
249
+ - color1: First color (hex)
250
+ - color2: Second color (hex)
251
+ - direction: 'horizontal' or 'vertical'
252
+ - start: Start point of gradient transition (0-1)
253
+ - stop: Stop point of gradient transition (0-1)
254
+ """
255
+ mask = Image.new("L", size)
256
+ draw = ImageDraw.Draw(mask)
257
+
258
+ width, height = size
259
+ start = gradient_config["start"]
260
+ stop = gradient_config["stop"]
261
+
262
+ if gradient_config["direction"] == "horizontal":
263
+ for x in range(width):
264
+ # Calculate position in gradient (0 to 1)
265
+ pos = x / width
266
+
267
+ # Calculate color intensity
268
+ if pos < start:
269
+ intensity = 0
270
+ elif pos > stop:
271
+ intensity = 255
272
+ else:
273
+ # Linear interpolation between start and stop
274
+ intensity = int(255 * (pos - start) / (stop - start))
275
+
276
+ draw.line([(x, 0), (x, height)], fill=intensity)
277
+ else: # vertical
278
+ for y in range(height):
279
+ pos = y / height
280
+ if pos < start:
281
+ intensity = 0
282
+ elif pos > stop:
283
+ intensity = 255
284
+ else:
285
+ intensity = int(255 * (pos - start) / (stop - start))
286
+
287
+ draw.line([(0, y), (width, y)], fill=intensity)
288
+
289
+ return mask
290
+
291
+ def _handle_existing_image(self, existing_image, output_image_filepath_noext, output_video_filepath, duration):
292
+ """Handle case where an existing image is provided."""
293
+ self.logger.info(f"Using existing image file: {existing_image}")
294
+ existing_extension = os.path.splitext(existing_image)[1]
295
+
296
+ if existing_extension == ".png":
297
+ self.logger.info(f"Copying existing PNG image file: {existing_image}")
298
+ shutil.copy2(existing_image, output_image_filepath_noext + existing_extension)
299
+ else:
300
+ self.logger.info(f"Converting existing image to PNG")
301
+ existing_image_obj = Image.open(existing_image)
302
+ existing_image_obj.save(output_image_filepath_noext + ".png")
303
+
304
+ if existing_extension != ".jpg":
305
+ self.logger.info(f"Converting existing image to JPG")
306
+ existing_image_obj = Image.open(existing_image)
307
+ if existing_image_obj.mode == "RGBA":
308
+ existing_image_obj = existing_image_obj.convert("RGB")
309
+ existing_image_obj.save(output_image_filepath_noext + ".jpg", quality=95)
310
+
311
+ if duration > 0:
312
+ self._create_video_from_image(output_image_filepath_noext + ".png", output_video_filepath, duration)
313
+
314
+ def _create_background(self, format, resolution):
315
+ """Create or load the background image."""
316
+ if format["background_image"] and os.path.exists(format["background_image"]):
317
+ self.logger.info(f"Using background image file: {format['background_image']}")
318
+ background = Image.open(format["background_image"])
319
+ else:
320
+ self.logger.info(f"Using background color: {format['background_color']}")
321
+ background = Image.new("RGB", resolution, color=self.hex_to_rgb(format["background_color"]))
322
+
323
+ return background.resize(resolution)
324
+
325
+ def _render_all_text(self, draw, font_path, title_text, artist_text, format, render_bounding_boxes):
326
+ """Render all text elements on the image."""
327
+ # Render title
328
+ if format["title_region"]:
329
+ region_parsed = self.parse_region(format["title_region"])
330
+ region = self._render_text_in_region(
331
+ draw, title_text, font_path, region_parsed, format["title_color"], gradient=format.get("title_gradient")
332
+ )
333
+ if render_bounding_boxes:
334
+ self._draw_bounding_box(draw, region, format["title_color"])
335
+
336
+ # Render artist
337
+ if format["artist_region"]:
338
+ region_parsed = self.parse_region(format["artist_region"])
339
+ region = self._render_text_in_region(
340
+ draw, artist_text, font_path, region_parsed, format["artist_color"], gradient=format.get("artist_gradient")
341
+ )
342
+ if render_bounding_boxes:
343
+ self._draw_bounding_box(draw, region, format["artist_color"])
344
+
345
+ # Render extra text if provided
346
+ if format["extra_text"]:
347
+ region_parsed = self.parse_region(format["extra_text_region"])
348
+ region = self._render_text_in_region(
349
+ draw, format["extra_text"], font_path, region_parsed, format["extra_text_color"], gradient=format.get("extra_text_gradient")
350
+ )
351
+ if render_bounding_boxes:
352
+ self._draw_bounding_box(draw, region, format["extra_text_color"])
353
+
354
+ def _save_output_files(
355
+ self, background, output_image_filepath_noext, output_video_filepath, duration, resolution
356
+ ):
357
+ """Save the output image files and create video if needed."""
358
+ # Save static background image
359
+ if self.output_png:
360
+ background.save(f"{output_image_filepath_noext}.png")
361
+
362
+ if self.output_jpg:
363
+ # Save static background image as JPG for smaller filesize
364
+ background_rgb = background.convert("RGB")
365
+ background_rgb.save(f"{output_image_filepath_noext}.jpg", quality=95)
366
+
367
+ if duration > 0:
368
+ self._create_video_from_image(f"{output_image_filepath_noext}.png", output_video_filepath, duration, resolution)
369
+
370
+ def _create_video_from_image(self, image_path, video_path, duration, resolution=(3840, 2160)):
371
+ """Create a video from a static image."""
372
+ ffmpeg_command = (
373
+ f'{self.ffmpeg_base_command} -y -loop 1 -framerate 30 -i "{image_path}" '
374
+ f"-f lavfi -i anullsrc -c:v libx264 -r 30 -t {duration} -pix_fmt yuv420p "
375
+ f'-vf scale={resolution[0]}:{resolution[1]} -c:a aac -shortest "{video_path}"'
376
+ )
377
+
378
+ self.logger.info("Generating video...")
379
+ self.logger.debug(f"Running command: {ffmpeg_command}")
380
+ os.system(ffmpeg_command)
381
+
382
+ def _transform_text(self, text, transform_type):
383
+ """Helper method to transform text based on specified type."""
384
+ if text is None:
385
+ return None # Return None if input is None
386
+ if transform_type == "uppercase":
387
+ return text.upper()
388
+ elif transform_type == "lowercase":
389
+ return text.lower()
390
+ elif transform_type == "propercase":
391
+ return text.title()
392
+ return text # "none" or any other value returns original text
393
+
394
+ def create_title_video(
395
+ self, artist, title, format, output_image_filepath_noext, output_video_filepath, existing_title_image, intro_video_duration
396
+ ):
397
+ title_text = self._transform_text(title, format["title_text_transform"])
398
+ artist_text = self._transform_text(artist, format["artist_text_transform"])
399
+ self.create_video(
400
+ title_text=title_text,
401
+ artist_text=artist_text,
402
+ extra_text=format["extra_text"],
403
+ format=format,
404
+ output_image_filepath_noext=output_image_filepath_noext,
405
+ output_video_filepath=output_video_filepath,
406
+ existing_image=existing_title_image,
407
+ duration=intro_video_duration,
408
+ )
409
+
410
+ def create_end_video(
411
+ self, artist, title, format, output_image_filepath_noext, output_video_filepath, existing_end_image, end_video_duration
412
+ ):
413
+ title_text = self._transform_text(title, format["title_text_transform"])
414
+ artist_text = self._transform_text(artist, format["artist_text_transform"])
415
+ self.create_video(
416
+ title_text=title_text,
417
+ artist_text=artist_text,
418
+ extra_text=format["extra_text"],
419
+ format=format,
420
+ output_image_filepath_noext=output_image_filepath_noext,
421
+ output_video_filepath=output_video_filepath,
422
+ existing_image=existing_end_image,
423
+ duration=end_video_duration,
424
+ )