lyrics-transcriber 0.30.0__py3-none-any.whl → 0.32.1__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 (88) hide show
  1. lyrics_transcriber/__init__.py +2 -1
  2. lyrics_transcriber/cli/{main.py → cli_main.py} +47 -14
  3. lyrics_transcriber/core/config.py +35 -0
  4. lyrics_transcriber/core/controller.py +164 -166
  5. lyrics_transcriber/correction/anchor_sequence.py +471 -0
  6. lyrics_transcriber/correction/corrector.py +256 -0
  7. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  8. lyrics_transcriber/correction/handlers/base.py +30 -0
  9. lyrics_transcriber/correction/handlers/extend_anchor.py +91 -0
  10. lyrics_transcriber/correction/handlers/levenshtein.py +147 -0
  11. lyrics_transcriber/correction/handlers/no_space_punct_match.py +98 -0
  12. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +55 -0
  13. lyrics_transcriber/correction/handlers/repeat.py +71 -0
  14. lyrics_transcriber/correction/handlers/sound_alike.py +223 -0
  15. lyrics_transcriber/correction/handlers/syllables_match.py +182 -0
  16. lyrics_transcriber/correction/handlers/word_count_match.py +54 -0
  17. lyrics_transcriber/correction/handlers/word_operations.py +135 -0
  18. lyrics_transcriber/correction/phrase_analyzer.py +426 -0
  19. lyrics_transcriber/correction/text_utils.py +30 -0
  20. lyrics_transcriber/lyrics/base_lyrics_provider.py +125 -0
  21. lyrics_transcriber/lyrics/genius.py +73 -0
  22. lyrics_transcriber/lyrics/spotify.py +82 -0
  23. lyrics_transcriber/output/ass/__init__.py +21 -0
  24. lyrics_transcriber/output/{ass.py → ass/ass.py} +150 -690
  25. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  26. lyrics_transcriber/output/ass/config.py +37 -0
  27. lyrics_transcriber/output/ass/constants.py +23 -0
  28. lyrics_transcriber/output/ass/event.py +94 -0
  29. lyrics_transcriber/output/ass/formatters.py +132 -0
  30. lyrics_transcriber/output/ass/lyrics_line.py +219 -0
  31. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  32. lyrics_transcriber/output/ass/section_detector.py +89 -0
  33. lyrics_transcriber/output/ass/section_screen.py +106 -0
  34. lyrics_transcriber/output/ass/style.py +187 -0
  35. lyrics_transcriber/output/cdg.py +503 -0
  36. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  37. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  38. lyrics_transcriber/output/cdgmaker/composer.py +1919 -0
  39. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  40. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  41. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  42. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  43. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  44. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  45. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  46. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  47. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  48. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  49. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  50. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  51. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  52. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  53. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  54. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  55. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  56. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  57. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  58. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  59. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  60. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  61. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  62. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  63. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  64. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  65. lyrics_transcriber/output/generator.py +140 -171
  66. lyrics_transcriber/output/lyrics_file.py +102 -0
  67. lyrics_transcriber/output/plain_text.py +91 -0
  68. lyrics_transcriber/output/segment_resizer.py +416 -0
  69. lyrics_transcriber/output/subtitles.py +328 -302
  70. lyrics_transcriber/output/video.py +219 -0
  71. lyrics_transcriber/review/__init__.py +1 -0
  72. lyrics_transcriber/review/server.py +138 -0
  73. lyrics_transcriber/storage/dropbox.py +110 -134
  74. lyrics_transcriber/transcribers/audioshake.py +171 -105
  75. lyrics_transcriber/transcribers/base_transcriber.py +149 -0
  76. lyrics_transcriber/transcribers/whisper.py +267 -133
  77. lyrics_transcriber/types.py +454 -0
  78. {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/METADATA +14 -3
  79. lyrics_transcriber-0.32.1.dist-info/RECORD +86 -0
  80. {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/WHEEL +1 -1
  81. lyrics_transcriber-0.32.1.dist-info/entry_points.txt +4 -0
  82. lyrics_transcriber/core/corrector.py +0 -56
  83. lyrics_transcriber/core/fetcher.py +0 -143
  84. lyrics_transcriber/storage/tokens.py +0 -116
  85. lyrics_transcriber/transcribers/base.py +0 -31
  86. lyrics_transcriber-0.30.0.dist-info/RECORD +0 -22
  87. lyrics_transcriber-0.30.0.dist-info/entry_points.txt +0 -3
  88. {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/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
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ RENDERED_BLANK = 0
14
+ RENDERED_MASK = 1
15
+ RENDERED_FILL = 1
16
+ RENDERED_STROKE = 2
17
+
18
+
19
+ def get_wrapped_text(
20
+ text: str,
21
+ font: ImageFont.FreeTypeFont,
22
+ width: int,
23
+ ) -> str:
24
+ """
25
+ Add newlines to text such that it fits within the specified width
26
+ using the specified font.
27
+
28
+ Existing newlines are preserved.
29
+
30
+ Parameters
31
+ ----------
32
+ text : str
33
+ Text to add newlines to.
34
+ font : `PIL.ImageFont.FreeTypeFont`
35
+ Font in which text will be rendered.
36
+ width : int
37
+ Maximum width of text lines in pixels.
38
+
39
+ Returns
40
+ -------
41
+ str
42
+ Text with inserted newlines.
43
+ """
44
+ lines: list[str] = []
45
+ for text_line in text.split("\n"):
46
+ words: list[str] = []
47
+ for word in text_line.split():
48
+ if font.getlength(" ".join(words + [word])) > width:
49
+ lines.append(" ".join(words))
50
+ words.clear()
51
+ words.append(word)
52
+ lines.append(" ".join(words))
53
+ words.clear()
54
+ return "\n".join(lines)
55
+
56
+
57
+ def render_text(
58
+ text: str,
59
+ font: ImageFont.FreeTypeFont,
60
+ fill: int = RENDERED_FILL,
61
+ stroke_fill: int = RENDERED_STROKE,
62
+ stroke_width: int = 0,
63
+ stroke_type: StrokeType = StrokeType.OCTAGON,
64
+ ) -> Image.Image:
65
+ """
66
+ Render one text line as a `PIL.Image.Image` in `P` mode.
67
+
68
+ There may be horizontal padding on both sides of the image. However,
69
+ for the same text prefix or suffix, the padding on that side will be
70
+ the same.
71
+
72
+ Parameters
73
+ ----------
74
+ text : str
75
+ Text line to render.
76
+ font : `PIL.ImageFont.FreeTypeFont`
77
+ Font to render text with.
78
+ config : `config.Settings`
79
+ Config settings.
80
+ fill : int, default 1
81
+ Color index of the text fill.
82
+ stroke_fill : int, default 2
83
+ Color index of the text stroke.
84
+ stroke_width : int, default 0
85
+ Width of the text stroke.
86
+ stroke_type : `StrokeType`, default `StrokeType.OCTAGON`
87
+ Stroke type.
88
+
89
+ Returns
90
+ -------
91
+ `PIL.Image.Image`
92
+ Image with rendered text.
93
+ """
94
+ # Get relevant dimensions for font
95
+ _, _, text_width, _ = font.getbbox(text)
96
+ ascent, descent = font.getmetrics()
97
+ (_, _), (offset_x, _) = font.font.getsize(text)
98
+
99
+ image_width = text_width - offset_x
100
+ image_height = ascent + descent
101
+ # Add space on left/right for stroke
102
+ image_width += 2 * stroke_width
103
+ # Add space on top/bottom for stroke
104
+ image_height += 2 * stroke_width
105
+ # HACK I don't know exactly why, but sometimes a few pixels are cut
106
+ # off on the sides, so we add some horizontal padding here. (This is
107
+ # cropped by another function, so it's okay.)
108
+ padding_x = font.size * 4
109
+ image_width += padding_x
110
+ offset_x -= padding_x // 2
111
+
112
+ image = Image.new("P", (image_width, image_height), 0)
113
+ draw = ImageDraw.Draw(image)
114
+ # Turn off antialiasing
115
+ draw.fontmode = "1"
116
+
117
+ draw_x = stroke_width - offset_x
118
+ draw_y = stroke_width
119
+ # If we are to draw a text stroke
120
+ if stroke_width and stroke_fill is not None:
121
+ # NOTE PIL allows text to be drawn with a stroke, but this
122
+ # stroke is anti-aliased, and you can't turn off the anti-
123
+ # aliasing on it. So instead, we're simulating a stroke by
124
+ # drawing the text multiple times at various offsets.
125
+ stroke_coords = list(it.product(
126
+ range(-stroke_width, stroke_width + 1), repeat=2,
127
+ ))
128
+ match stroke_type:
129
+ case StrokeType.CIRCLE:
130
+ stroke_coords = [
131
+ (x, y)
132
+ for x, y in stroke_coords
133
+ if x**2 + y**2 <= stroke_width ** 2
134
+ ]
135
+ case StrokeType.SQUARE:
136
+ pass
137
+ case StrokeType.OCTAGON:
138
+ stroke_coords = [
139
+ (x, y)
140
+ for x, y in stroke_coords
141
+
142
+ if (abs(x) + abs(y)) * 2 <= stroke_width * 3
143
+ ]
144
+
145
+ # Create image for text stroke
146
+ stroke_image = Image.new("P", image.size, 0)
147
+ stroke_draw = ImageDraw.Draw(stroke_image)
148
+ # Turn off antialiasing
149
+ stroke_draw.fontmode = "1"
150
+
151
+ # Render text stroke
152
+ stroke_draw.text((draw_x, draw_y), text, stroke_fill, font)
153
+ # Create mask for text stroke
154
+ stroke_mask = stroke_image.point(lambda v: v and 255, mode="1")
155
+ # Draw text stroke at various offsets
156
+ for x, y in stroke_coords:
157
+ image.paste(stroke_image, (x, y), mask=stroke_mask)
158
+ # NOTE Drawing the stroke once and pasting it multiple times is
159
+ # faster than drawing the stroke multiple times.
160
+
161
+ # Draw text fill
162
+ draw.text((draw_x, draw_y), text, fill, font)
163
+ return image
164
+
165
+
166
+ def render_lines_and_masks(
167
+ lines: Sequence[Sequence[str]],
168
+ font: ImageFont.FreeTypeFont,
169
+ stroke_width: int = 0,
170
+ stroke_type: StrokeType = StrokeType.OCTAGON,
171
+ render_masks: bool = True,
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
+ ]