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.
- lyrics_transcriber/__init__.py +2 -1
- lyrics_transcriber/cli/{main.py → cli_main.py} +47 -14
- lyrics_transcriber/core/config.py +35 -0
- lyrics_transcriber/core/controller.py +164 -166
- lyrics_transcriber/correction/anchor_sequence.py +471 -0
- lyrics_transcriber/correction/corrector.py +256 -0
- lyrics_transcriber/correction/handlers/__init__.py +0 -0
- lyrics_transcriber/correction/handlers/base.py +30 -0
- lyrics_transcriber/correction/handlers/extend_anchor.py +91 -0
- lyrics_transcriber/correction/handlers/levenshtein.py +147 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +98 -0
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +55 -0
- lyrics_transcriber/correction/handlers/repeat.py +71 -0
- lyrics_transcriber/correction/handlers/sound_alike.py +223 -0
- lyrics_transcriber/correction/handlers/syllables_match.py +182 -0
- lyrics_transcriber/correction/handlers/word_count_match.py +54 -0
- lyrics_transcriber/correction/handlers/word_operations.py +135 -0
- lyrics_transcriber/correction/phrase_analyzer.py +426 -0
- lyrics_transcriber/correction/text_utils.py +30 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +125 -0
- lyrics_transcriber/lyrics/genius.py +73 -0
- lyrics_transcriber/lyrics/spotify.py +82 -0
- lyrics_transcriber/output/ass/__init__.py +21 -0
- lyrics_transcriber/output/{ass.py → ass/ass.py} +150 -690
- lyrics_transcriber/output/ass/ass_specs.txt +732 -0
- lyrics_transcriber/output/ass/config.py +37 -0
- lyrics_transcriber/output/ass/constants.py +23 -0
- lyrics_transcriber/output/ass/event.py +94 -0
- lyrics_transcriber/output/ass/formatters.py +132 -0
- lyrics_transcriber/output/ass/lyrics_line.py +219 -0
- lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
- lyrics_transcriber/output/ass/section_detector.py +89 -0
- lyrics_transcriber/output/ass/section_screen.py +106 -0
- lyrics_transcriber/output/ass/style.py +187 -0
- lyrics_transcriber/output/cdg.py +503 -0
- lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
- lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
- lyrics_transcriber/output/cdgmaker/composer.py +1919 -0
- lyrics_transcriber/output/cdgmaker/config.py +151 -0
- lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
- lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
- lyrics_transcriber/output/cdgmaker/pack.py +507 -0
- lyrics_transcriber/output/cdgmaker/render.py +346 -0
- lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
- lyrics_transcriber/output/cdgmaker/utils.py +132 -0
- lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
- lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
- lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/arial.ttf +0 -0
- lyrics_transcriber/output/fonts/georgia.ttf +0 -0
- lyrics_transcriber/output/fonts/verdana.ttf +0 -0
- lyrics_transcriber/output/generator.py +140 -171
- lyrics_transcriber/output/lyrics_file.py +102 -0
- lyrics_transcriber/output/plain_text.py +91 -0
- lyrics_transcriber/output/segment_resizer.py +416 -0
- lyrics_transcriber/output/subtitles.py +328 -302
- lyrics_transcriber/output/video.py +219 -0
- lyrics_transcriber/review/__init__.py +1 -0
- lyrics_transcriber/review/server.py +138 -0
- lyrics_transcriber/storage/dropbox.py +110 -134
- lyrics_transcriber/transcribers/audioshake.py +171 -105
- lyrics_transcriber/transcribers/base_transcriber.py +149 -0
- lyrics_transcriber/transcribers/whisper.py +267 -133
- lyrics_transcriber/types.py +454 -0
- {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/METADATA +14 -3
- lyrics_transcriber-0.32.1.dist-info/RECORD +86 -0
- {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/WHEEL +1 -1
- lyrics_transcriber-0.32.1.dist-info/entry_points.txt +4 -0
- lyrics_transcriber/core/corrector.py +0 -56
- lyrics_transcriber/core/fetcher.py +0 -143
- lyrics_transcriber/storage/tokens.py +0 -116
- lyrics_transcriber/transcribers/base.py +0 -31
- lyrics_transcriber-0.30.0.dist-info/RECORD +0 -22
- lyrics_transcriber-0.30.0.dist-info/entry_points.txt +0 -3
- {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
|
+
]
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -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
|
+
]
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|