karaoke-gen 0.57.0__py3-none-any.whl → 0.71.23__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 +1815 -0
  26. karaoke_gen/video_background_processor.py +351 -0
  27. karaoke_gen-0.71.23.dist-info/METADATA +610 -0
  28. karaoke_gen-0.71.23.dist-info/RECORD +275 -0
  29. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/WHEEL +1 -1
  30. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.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.23.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,346 @@
1
+ from collections.abc import Sequence
2
+ import itertools as it
3
+
4
+ from PIL import Image, ImageChops, ImageDraw, ImageFont
5
+
6
+ from .config import *
7
+
8
+
9
+ import logging
10
+
11
+
12
+ RENDERED_BLANK = 0
13
+ RENDERED_MASK = 1
14
+ RENDERED_FILL = 1
15
+ RENDERED_STROKE = 2
16
+
17
+
18
+ def get_wrapped_text(
19
+ text: str,
20
+ font: ImageFont.FreeTypeFont,
21
+ width: int,
22
+ ) -> str:
23
+ """
24
+ Add newlines to text such that it fits within the specified width
25
+ using the specified font.
26
+
27
+ Existing newlines are preserved.
28
+
29
+ Parameters
30
+ ----------
31
+ text : str
32
+ Text to add newlines to.
33
+ font : `PIL.ImageFont.FreeTypeFont`
34
+ Font in which text will be rendered.
35
+ width : int
36
+ Maximum width of text lines in pixels.
37
+
38
+ Returns
39
+ -------
40
+ str
41
+ Text with inserted newlines.
42
+ """
43
+ lines: list[str] = []
44
+ for text_line in text.split("\n"):
45
+ words: list[str] = []
46
+ for word in text_line.split():
47
+ if font.getlength(" ".join(words + [word])) > width:
48
+ lines.append(" ".join(words))
49
+ words.clear()
50
+ words.append(word)
51
+ lines.append(" ".join(words))
52
+ words.clear()
53
+ return "\n".join(lines)
54
+
55
+
56
+ def render_text(
57
+ text: str,
58
+ font: ImageFont.FreeTypeFont,
59
+ fill: int = RENDERED_FILL,
60
+ stroke_fill: int = RENDERED_STROKE,
61
+ stroke_width: int = 0,
62
+ stroke_type: StrokeType = StrokeType.OCTAGON,
63
+ ) -> Image.Image:
64
+ """
65
+ Render one text line as a `PIL.Image.Image` in `P` mode.
66
+
67
+ There may be horizontal padding on both sides of the image. However,
68
+ for the same text prefix or suffix, the padding on that side will be
69
+ the same.
70
+
71
+ Parameters
72
+ ----------
73
+ text : str
74
+ Text line to render.
75
+ font : `PIL.ImageFont.FreeTypeFont`
76
+ Font to render text with.
77
+ config : `config.Settings`
78
+ Config settings.
79
+ fill : int, default 1
80
+ Color index of the text fill.
81
+ stroke_fill : int, default 2
82
+ Color index of the text stroke.
83
+ stroke_width : int, default 0
84
+ Width of the text stroke.
85
+ stroke_type : `StrokeType`, default `StrokeType.OCTAGON`
86
+ Stroke type.
87
+
88
+ Returns
89
+ -------
90
+ `PIL.Image.Image`
91
+ Image with rendered text.
92
+ """
93
+ # Get relevant dimensions for font
94
+ _, _, text_width, _ = font.getbbox(text)
95
+ ascent, descent = font.getmetrics()
96
+ (_, _), (offset_x, _) = font.font.getsize(text)
97
+
98
+ image_width = text_width - offset_x
99
+ image_height = ascent + descent
100
+ # Add space on left/right for stroke
101
+ image_width += 2 * stroke_width
102
+ # Add space on top/bottom for stroke
103
+ image_height += 2 * stroke_width
104
+ # HACK I don't know exactly why, but sometimes a few pixels are cut
105
+ # off on the sides, so we add some horizontal padding here. (This is
106
+ # cropped by another function, so it's okay.)
107
+ padding_x = font.size * 4
108
+ image_width += padding_x
109
+ offset_x -= padding_x // 2
110
+
111
+ image = Image.new("P", (image_width, image_height), 0)
112
+ draw = ImageDraw.Draw(image)
113
+ # Turn off antialiasing
114
+ draw.fontmode = "1"
115
+
116
+ draw_x = stroke_width - offset_x
117
+ draw_y = stroke_width
118
+ # If we are to draw a text stroke
119
+ if stroke_width and stroke_fill is not None:
120
+ # NOTE PIL allows text to be drawn with a stroke, but this
121
+ # stroke is anti-aliased, and you can't turn off the anti-
122
+ # aliasing on it. So instead, we're simulating a stroke by
123
+ # drawing the text multiple times at various offsets.
124
+ stroke_coords = list(it.product(
125
+ range(-stroke_width, stroke_width + 1), repeat=2,
126
+ ))
127
+ match stroke_type:
128
+ case StrokeType.CIRCLE:
129
+ stroke_coords = [
130
+ (x, y)
131
+ for x, y in stroke_coords
132
+ if x**2 + y**2 <= stroke_width ** 2
133
+ ]
134
+ case StrokeType.SQUARE:
135
+ pass
136
+ case StrokeType.OCTAGON:
137
+ stroke_coords = [
138
+ (x, y)
139
+ for x, y in stroke_coords
140
+
141
+ if (abs(x) + abs(y)) * 2 <= stroke_width * 3
142
+ ]
143
+
144
+ # Create image for text stroke
145
+ stroke_image = Image.new("P", image.size, 0)
146
+ stroke_draw = ImageDraw.Draw(stroke_image)
147
+ # Turn off antialiasing
148
+ stroke_draw.fontmode = "1"
149
+
150
+ # Render text stroke
151
+ stroke_draw.text((draw_x, draw_y), text, stroke_fill, font)
152
+ # Create mask for text stroke
153
+ stroke_mask = stroke_image.point(lambda v: v and 255, mode="1")
154
+ # Draw text stroke at various offsets
155
+ for x, y in stroke_coords:
156
+ image.paste(stroke_image, (x, y), mask=stroke_mask)
157
+ # NOTE Drawing the stroke once and pasting it multiple times is
158
+ # faster than drawing the stroke multiple times.
159
+
160
+ # Draw text fill
161
+ draw.text((draw_x, draw_y), text, fill, font)
162
+ return image
163
+
164
+
165
+ def render_lines_and_masks(
166
+ lines: Sequence[Sequence[str]],
167
+ font: ImageFont.FreeTypeFont,
168
+ stroke_width: int = 0,
169
+ stroke_type: StrokeType = StrokeType.OCTAGON,
170
+ render_masks: bool = True,
171
+ logger: logging.Logger = logging.getLogger(__name__),
172
+ ) -> tuple[list[Image.Image], list[list[Image.Image]]]:
173
+ """
174
+ Render set of karaoke lines as `PIL.Image.Image`s, and masks for
175
+ each syllable as lists of `PIL.Image.Image`s.
176
+
177
+ The line images will be cropped as much as possible on the left,
178
+ right, and bottom sides. The top side of all line images will be
179
+ cropped by the largest amount that does not shrink any of their
180
+ bounding boxes.
181
+
182
+ Parameters
183
+ ----------
184
+ lines : list of list of str
185
+ Lines as lists of syllables.
186
+ font : `PIL.ImageFont.FreeTypeFont`
187
+ Font to render text with.
188
+ stroke_width : int, default 0
189
+ WIdth of the text stroke.
190
+ stroke_type : `StrokeType`, default `StrokeType.OCTAGON`
191
+ Stroke type.
192
+ render_masks : bool, default True
193
+ If true, render masks for each line.
194
+
195
+ Returns
196
+ -------
197
+ list of `PIL.Image.Image`
198
+ Images with rendered lines.
199
+ list of list of `PIL.Image.Image`
200
+ Images with rendered masks for each syllable for each line.
201
+ """
202
+ logger.debug("rendering line images")
203
+ # Render line images
204
+ uncropped_line_images = [
205
+ render_text(
206
+ text="".join(line),
207
+ font=font,
208
+ fill=RENDERED_FILL,
209
+ stroke_fill=RENDERED_STROKE,
210
+ stroke_width=stroke_width,
211
+ stroke_type=stroke_type,
212
+ )
213
+ for line in lines
214
+ ]
215
+ # Calculate how much the tops of the lines can be cropped
216
+ top_crop = min(
217
+ (
218
+ bbox[1]
219
+ for image in uncropped_line_images
220
+ if (bbox := image.getbbox()) is not None
221
+ ),
222
+ default=0,
223
+ )
224
+ logger.debug(
225
+ f"line images will be cropped by {top_crop} pixel(s) on the top"
226
+ )
227
+
228
+ # Crop line images
229
+ line_images: list[Image.Image] = []
230
+ bboxes: list[Sequence[int]] = []
231
+ logger.debug("cropping line images")
232
+ for image in uncropped_line_images:
233
+ bbox = image.getbbox()
234
+ if bbox is None:
235
+ # Create empty bounding box if image is empty
236
+ bbox = (0, 0, 0, 0)
237
+ else:
238
+ # Crop top of bounding box is image is not empty
239
+ bbox = list(bbox)
240
+ bbox[1] = top_crop
241
+
242
+ bboxes.append(bbox)
243
+ line_images.append(image.crop(bbox))
244
+
245
+ if not render_masks:
246
+ logger.debug("not rendering masks")
247
+ return line_images, []
248
+
249
+ # Render mask images
250
+ line_masks: list[list[Image.Image]] = []
251
+ logger.debug("rendering/cropping masks")
252
+ for line, bbox in zip(lines, bboxes):
253
+ # HACK For whatever reason, the presence or absence of certain
254
+ # characters of text can cause the rendered text to be 1 pixel
255
+ # off. We fix this by adding the entire rest of the text after
256
+ # each rendered part of it, so this mysterious offset is at
257
+ # least consistent.
258
+ extra_text = "".join(line)
259
+ # NOTE We will prefix the extra text with way more spaces than
260
+ # necessary, so it doesn't show up in the mask images.
261
+ text_padding = " " * bbox[2]
262
+ # REVIEW More testing is needed. Which characters does this
263
+ # happen for? Why does this even happen?
264
+ # Using Old Sans Black, this happens with at least "t" and "!".
265
+
266
+ # Get masks of the line's text from the start up to each
267
+ # syllable
268
+ # e.g. ["Don't ", "walk ", "a", "way"] ->
269
+ # ["Don't ", "Don't walk ", "Don't walk a", "Don't walk away"]
270
+ full_line_masks = [
271
+ render_text(
272
+ text="".join(line[:i+1]) + text_padding + extra_text,
273
+ font=font,
274
+ fill=RENDERED_MASK,
275
+ stroke_fill=RENDERED_MASK,
276
+ stroke_width=stroke_width,
277
+ stroke_type=stroke_type,
278
+ ).crop(bbox)
279
+ for i in range(len(line))
280
+ ]
281
+
282
+ line_mask: list[Image.Image] = []
283
+ # If this line has any syllables
284
+ if full_line_masks:
285
+ # Start with the first syllable's mask...
286
+ line_mask = [full_line_masks[0]] + [
287
+ # ...then get the pixel-by-pixel difference between each
288
+ # pair of full-line masks
289
+ ImageChops.difference(prev_mask, next_mask)
290
+ for prev_mask, next_mask in it.pairwise(full_line_masks)
291
+ ]
292
+ # NOTE This will isolate the pixels that make up this syllable,
293
+ # by basically "cancelling out" the previous syllables of the
294
+ # line.
295
+ line_masks.append(line_mask)
296
+
297
+ return line_images, line_masks
298
+
299
+
300
+ def render_lines(
301
+ lines: Sequence[Sequence[str]],
302
+ font: ImageFont.FreeTypeFont,
303
+ stroke_width: int = 0,
304
+ stroke_type: StrokeType = StrokeType.OCTAGON,
305
+ ) -> list[Image.Image]:
306
+ """
307
+ Render set of karaoke lines as `PIL.Image.Image`s.
308
+
309
+ The line images will be cropped as much as possible on the left,
310
+ right, and bottom sides. The top side of all line images will be
311
+ cropped by the largest amount that does not shrink any of their
312
+ bounding boxes.
313
+
314
+ Parameters
315
+ ----------
316
+ lines : list of list of str
317
+ Lines as lists of syllables.
318
+ font : `PIL.ImageFont.FreeTypeFont`
319
+ Font to render text with.
320
+ stroke_width : int, default 0
321
+ WIdth of the text stroke.
322
+ stroke_type : `StrokeType`, default `StrokeType.OCTAGON`
323
+ Stroke type.
324
+
325
+ Returns
326
+ -------
327
+ list of `PIL.Image.Image`
328
+ Images with rendered lines.
329
+ """
330
+ images, _ = render_lines_and_masks(
331
+ lines,
332
+ font=font,
333
+ stroke_width=stroke_width,
334
+ stroke_type=stroke_type,
335
+ render_masks=False,
336
+ )
337
+ return images
338
+
339
+
340
+ __all__ = [
341
+ "RENDERED_BLANK", "RENDERED_MASK", "RENDERED_FILL",
342
+ "RENDERED_STROKE",
343
+
344
+ "get_wrapped_text", "render_text", "render_lines_and_masks",
345
+ "render_lines",
346
+ ]
@@ -0,0 +1,132 @@
1
+ from collections.abc import Iterable, Iterator, Sequence
2
+ import itertools as it
3
+ import operator
4
+ from typing import Any, TypeVar, overload
5
+
6
+
7
+ _T = TypeVar("_T")
8
+
9
+
10
+ @overload
11
+ def ceildiv(a: int, b: int) -> int: ...
12
+ @overload
13
+ def ceildiv(a: float, b: float) -> float: ...
14
+ def ceildiv(a, b):
15
+ """
16
+ Return the ceiling of `a / b`.
17
+
18
+ Parameters
19
+ ----------
20
+ a : int or float
21
+ Dividend.
22
+ b : int or float
23
+ Divisor.
24
+
25
+ Returns
26
+ -------
27
+ int or float
28
+ The ceiling of the quotient of `a` and `b`.
29
+ """
30
+ return -(a // -b)
31
+
32
+
33
+ def distribute(
34
+ sequence: Sequence[_T],
35
+ start: float = 0,
36
+ stop: float = 1,
37
+ ) -> Iterator[tuple[float, _T]]:
38
+ """
39
+ Enumerate the sequence evenly over the interval (`start`, `stop`).
40
+
41
+ Based on https://stackoverflow.com/a/59594546 .
42
+
43
+ Parameters
44
+ ----------
45
+ sequence : array-like
46
+ Sequence to enumerate.
47
+ start : float, default 0
48
+ Start of interval (exclusive).
49
+ stop : float, default 1
50
+ End of interval (exclusive).
51
+
52
+ Yields
53
+ ------
54
+ position : float
55
+ Position of sequence item in interval.
56
+ item
57
+ Sequence item.
58
+
59
+ Examples
60
+ --------
61
+ >>> list(distribute("abc"))
62
+ [(0.25, 'a'), (0.5, 'b'), (0.75, 'c')]
63
+ >>> list(distribute("abc", 1, 4))
64
+ [(1.75, 'a'), (2.5, 'b'), (3.25, 'c')]
65
+ """
66
+ m = len(sequence) + 1
67
+ for i, v in enumerate(sequence, 1):
68
+ yield start + (stop - start) * i / m, v
69
+
70
+
71
+ def intersperse(*sequences: Sequence[_T]) -> Iterator[_T]:
72
+ """
73
+ Evenly intersperse the sequences.
74
+
75
+ Based on https://stackoverflow.com/a/59594546 .
76
+
77
+ Parameters
78
+ ----------
79
+ *sequences
80
+ Sequences to intersperse.
81
+
82
+ Yields
83
+ ------
84
+ item
85
+ Sequence item.
86
+
87
+ Examples
88
+ --------
89
+ >>> list(intersperse(range(10), "abc"))
90
+ [0, 1, 'a', 2, 3, 4, 'b', 5, 6, 7, 'c', 8, 9]
91
+ >>> list(intersperse("XY", range(10), "abc"))
92
+ [0, 1, 'a', 2, 'X', 3, 4, 'b', 5, 6, 'Y', 7, 'c', 8, 9]
93
+ >>> "".join(intersperse("hlwl", "eood", "l r!"))
94
+ 'hello world!'
95
+ """
96
+ distributions = map(distribute, sequences)
97
+ for _, v in sorted(it.chain(*distributions), key=operator.itemgetter(0)):
98
+ yield v
99
+
100
+
101
+ def pad(
102
+ iterable: Iterable[_T],
103
+ size: int,
104
+ padvalue: Any = None,
105
+ ) -> Iterable[_T]:
106
+ """
107
+ Pad an iterable to a specified size.
108
+
109
+ If the iterable is longer than the specified size, it is truncated.
110
+ If it is shorter, `padvalue` is appended until the specified size is
111
+ reached.
112
+
113
+ Parameters
114
+ ----------
115
+ iterable : iterable
116
+ Iterable to pad.
117
+ size : int
118
+ Size to pad iterable to.
119
+ padvalue : any, default None
120
+ Value to pad iterable with.
121
+
122
+ Returns
123
+ -------
124
+ iterable
125
+ Padded iterable.
126
+ """
127
+ return it.islice(it.chain(iterable, it.repeat(padvalue)), size)
128
+
129
+
130
+ __all__ = [
131
+ "ceildiv", "distribute", "intersperse", "pad",
132
+ ]