pycaption 2.3.6__tar.gz → 2.3.7__tar.gz
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.
- {pycaption-2.3.6/pycaption.egg-info → pycaption-2.3.7}/PKG-INFO +1 -1
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/sami/reader.py +4 -6
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/sami/writer.py +2 -4
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/scc/writer.py +15 -11
- {pycaption-2.3.6 → pycaption-2.3.7/pycaption.egg-info}/PKG-INFO +1 -1
- {pycaption-2.3.6 → pycaption-2.3.7}/setup.py +1 -1
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_scc_writer.py +61 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_webvtt_conversion.py +22 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/LICENSE +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/MANIFEST.in +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/README.rst +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/__init__.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/base.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/dfxp/__init__.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/dfxp/constants.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/dfxp/extras.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/dfxp/reader.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/dfxp/writer.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/exceptions.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/geometry.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/microdvd.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/sami/__init__.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/sami/constants.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/sami/parser.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/scc/__init__.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/scc/constants.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/scc/reader.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/scc/specialized_collections.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/scc/state_machines.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/scc/translator.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/srt.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/transcript.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/utils.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/webvtt/__init__.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/webvtt/constants.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/webvtt/reader.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption/webvtt/writer.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption.egg-info/SOURCES.txt +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption.egg-info/dependency_links.txt +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption.egg-info/requires.txt +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/pycaption.egg-info/top_level.txt +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/setup.cfg +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/__init__.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/conftest.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/fixtures/__init__.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/fixtures/dfxp.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/fixtures/microdvd.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/fixtures/sami.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/fixtures/scc.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/fixtures/srt.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/fixtures/translated_scc.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/fixtures/webvtt.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/mixins.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_base.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_bytes_input.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_dfxp.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_dfxp_conversion.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_dfxp_extras.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_double_encoding.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_functions.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_geometry.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_microdvd.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_microdvd_conversion.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_sami.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_sami_conversion.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_scc.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_scc_conversion.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_scc_translator.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_srt.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_srt_conversion.py +0 -0
- {pycaption-2.3.6 → pycaption-2.3.7}/tests/test_webvtt.py +0 -0
|
@@ -7,10 +7,7 @@ styles, margin-based positioning, and multi-language content.
|
|
|
7
7
|
from bs4 import BeautifulSoup, NavigableString
|
|
8
8
|
|
|
9
9
|
from ..base import BaseReader, Caption, CaptionList, CaptionNode, CaptionSet
|
|
10
|
-
from ..exceptions import
|
|
11
|
-
CaptionReadNoCaptions,
|
|
12
|
-
CaptionReadTimingError,
|
|
13
|
-
)
|
|
10
|
+
from ..exceptions import CaptionReadNoCaptions, CaptionReadTimingError
|
|
14
11
|
from ..geometry import Alignment, HorizontalAlignmentEnum, Layout, Padding, Size
|
|
15
12
|
from .parser import SAMIParser
|
|
16
13
|
|
|
@@ -65,7 +62,8 @@ class SAMIReader(BaseReader):
|
|
|
65
62
|
caption_dict[language] = lang_captions
|
|
66
63
|
|
|
67
64
|
caption_set = CaptionSet(
|
|
68
|
-
caption_dict,
|
|
65
|
+
caption_dict,
|
|
66
|
+
layout_info=global_layout,
|
|
69
67
|
visual_alignment_default=HorizontalAlignmentEnum.LEFT,
|
|
70
68
|
)
|
|
71
69
|
|
|
@@ -244,7 +242,7 @@ class SAMIReader(BaseReader):
|
|
|
244
242
|
attrs = {}
|
|
245
243
|
css_attrs = tag.attrs
|
|
246
244
|
|
|
247
|
-
if "class" in css_attrs:
|
|
245
|
+
if "class" in css_attrs and css_attrs["class"]:
|
|
248
246
|
attrs["class"] = css_attrs["class"][0].lower()
|
|
249
247
|
if "id" in css_attrs:
|
|
250
248
|
attrs["class"] = css_attrs["id"].lower()
|
|
@@ -42,9 +42,7 @@ class SAMIWriter(BaseWriter):
|
|
|
42
42
|
"""Serialize a CaptionSet into a SAMI document string."""
|
|
43
43
|
caption_set = deepcopy(caption_set)
|
|
44
44
|
source_default = self._get_visual_alignment_default(caption_set)
|
|
45
|
-
self._promote_center =
|
|
46
|
-
source_default == HorizontalAlignmentEnum.CENTER
|
|
47
|
-
)
|
|
45
|
+
self._promote_center = source_default == HorizontalAlignmentEnum.CENTER
|
|
48
46
|
sami = BeautifulSoup(SAMI_BASE_MARKUP, "lxml-xml")
|
|
49
47
|
|
|
50
48
|
caption_set.layout_info = self._relativize_and_fit_to_screen(
|
|
@@ -310,7 +308,7 @@ class SAMIWriter(BaseWriter):
|
|
|
310
308
|
|
|
311
309
|
style = ""
|
|
312
310
|
klass = ""
|
|
313
|
-
if "classes" in content:
|
|
311
|
+
if "classes" in content and content["classes"]:
|
|
314
312
|
klass += f' class="{" ".join(content["classes"])}"'
|
|
315
313
|
elif "class" in content:
|
|
316
314
|
klass += f' class="{content["class"]}"'
|
|
@@ -99,6 +99,7 @@ class SCCWriter(BaseWriter):
|
|
|
99
99
|
codes = self._encode_all(classified)
|
|
100
100
|
codes = self._adjust_timing(codes)
|
|
101
101
|
codes = self._deduplicate_timestamps(codes)
|
|
102
|
+
codes = self._clamp_end_times(codes)
|
|
102
103
|
output += self._render_mixed(codes)
|
|
103
104
|
|
|
104
105
|
return output
|
|
@@ -195,6 +196,17 @@ class SCCWriter(BaseWriter):
|
|
|
195
196
|
last_emitted_frame = self._microseconds_to_frame(start)
|
|
196
197
|
return codes
|
|
197
198
|
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _clamp_end_times(codes):
|
|
201
|
+
"""Suppress end times that drift behind start after timing adjustments.
|
|
202
|
+
When dense cues push start forward beyond the original end, emit no
|
|
203
|
+
clear command (end=None) rather than producing an invalid negative-
|
|
204
|
+
duration cue."""
|
|
205
|
+
for index, (code, start, end, mode, depth) in enumerate(codes):
|
|
206
|
+
if end is not None and end <= start:
|
|
207
|
+
codes[index] = (code, start, None, mode, depth)
|
|
208
|
+
return codes
|
|
209
|
+
|
|
198
210
|
def _render_mixed(self, codes):
|
|
199
211
|
"""Serialize all cues in chronological order, emitting mode-appropriate
|
|
200
212
|
preambles inline. Handles pop-on (ENM+RCL...EDM+EOC), roll-up
|
|
@@ -225,12 +237,7 @@ class SCCWriter(BaseWriter):
|
|
|
225
237
|
code_tokens = code.split()
|
|
226
238
|
|
|
227
239
|
if len(code_tokens) <= max_payload:
|
|
228
|
-
return
|
|
229
|
-
f"{ts}\t"
|
|
230
|
-
"94ae 94ae 9420 9420 "
|
|
231
|
-
f"{code}"
|
|
232
|
-
"942c 942c 942f 942f\n\n"
|
|
233
|
-
)
|
|
240
|
+
return f"{ts}\t" "94ae 94ae 9420 9420 " f"{code}" "942c 942c 942f 942f\n\n"
|
|
234
241
|
|
|
235
242
|
output = ""
|
|
236
243
|
offset = 0
|
|
@@ -243,11 +250,7 @@ class SCCWriter(BaseWriter):
|
|
|
243
250
|
is_last = offset + max_payload >= len(code_tokens)
|
|
244
251
|
if is_last:
|
|
245
252
|
line = line + ["942c", "942c", "942f", "942f"]
|
|
246
|
-
output += (
|
|
247
|
-
f"{self._format_timestamp(start)}\t"
|
|
248
|
-
+ " ".join(line)
|
|
249
|
-
+ "\n\n"
|
|
250
|
-
)
|
|
253
|
+
output += f"{self._format_timestamp(start)}\t" + " ".join(line) + "\n\n"
|
|
251
254
|
offset += max_payload
|
|
252
255
|
if not is_last:
|
|
253
256
|
start += MICROSECONDS_PER_CODEWORD
|
|
@@ -329,6 +332,7 @@ class SCCWriter(BaseWriter):
|
|
|
329
332
|
base_row = max(1, min(15, round((y.value - 5) / 90.0 * 15) + 1))
|
|
330
333
|
else:
|
|
331
334
|
base_row = 15
|
|
335
|
+
base_row = max(1, min(base_row, 16 - num_lines))
|
|
332
336
|
row = base_row + line_index
|
|
333
337
|
return min(row, 15)
|
|
334
338
|
return max(1, min(15, 16 - num_lines + line_index))
|
|
@@ -185,6 +185,30 @@ class TestSCCWriterOverlappingCues:
|
|
|
185
185
|
# (only the last cue gets a clear-screen at its end time)
|
|
186
186
|
assert len(clear_lines) <= 1
|
|
187
187
|
|
|
188
|
+
def test_dense_cues_never_produce_negative_duration(self):
|
|
189
|
+
"""When many dense cues push start times forward via timing
|
|
190
|
+
adjustments, the end time must never be less than the start."""
|
|
191
|
+
lines = ["WEBVTT", ""]
|
|
192
|
+
for i in range(20):
|
|
193
|
+
start_ms = 1000 + i * 200
|
|
194
|
+
end_ms = start_ms + 150
|
|
195
|
+
s, e = start_ms / 1000, end_ms / 1000
|
|
196
|
+
lines.append(
|
|
197
|
+
f"00:00:{int(s):02d}.{int((s % 1) * 1000):03d} --> "
|
|
198
|
+
f"00:00:{int(e):02d}.{int((e % 1) * 1000):03d}"
|
|
199
|
+
)
|
|
200
|
+
lines.append(
|
|
201
|
+
f"Caption number {i + 1:02d} with a lot of extra "
|
|
202
|
+
f"text padding here for testing!!"
|
|
203
|
+
)
|
|
204
|
+
lines.append("")
|
|
205
|
+
captions = WebVTTReader().read("\n".join(lines))
|
|
206
|
+
scc_output = SCCWriter().write(captions)
|
|
207
|
+
reread = SCCReader().read(scc_output)
|
|
208
|
+
caps = reread.get_captions(reread.get_languages()[0])
|
|
209
|
+
for cap in caps:
|
|
210
|
+
assert cap.end >= cap.start
|
|
211
|
+
|
|
188
212
|
|
|
189
213
|
class TestSCCWriterSplitLongCaption:
|
|
190
214
|
def test_split_caption_exceeding_80_tokens(self):
|
|
@@ -319,6 +343,43 @@ class TestSCCWriterPositioning:
|
|
|
319
343
|
pac_col_28 = WRITER_PAC_CODES[(15, 28, "plain")]
|
|
320
344
|
assert pac_col_28 in output
|
|
321
345
|
|
|
346
|
+
def test_multiline_bottom_stacks_upward(self):
|
|
347
|
+
vtt = (
|
|
348
|
+
"WEBVTT\n\n"
|
|
349
|
+
"00:00:01.000 --> 00:00:04.000 line:100%\n"
|
|
350
|
+
"This is line one\n"
|
|
351
|
+
"This is line two\n"
|
|
352
|
+
)
|
|
353
|
+
captions = WebVTTReader().read(vtt)
|
|
354
|
+
scc_output = SCCWriter().write(captions)
|
|
355
|
+
reread = SCCReader().read(scc_output)
|
|
356
|
+
caps = reread.get_captions(reread.get_languages()[0])
|
|
357
|
+
assert caps[0].get_text() == "This is line one\nThis is line two"
|
|
358
|
+
pac_row_14 = WRITER_PAC_CODES[(14, 0, "plain")]
|
|
359
|
+
pac_row_15 = WRITER_PAC_CODES[(15, 0, "plain")]
|
|
360
|
+
assert pac_row_14 in scc_output
|
|
361
|
+
assert pac_row_15 in scc_output
|
|
362
|
+
|
|
363
|
+
def test_three_line_at_86_percent_stacks_upward(self):
|
|
364
|
+
vtt = (
|
|
365
|
+
"WEBVTT\n\n"
|
|
366
|
+
"00:00:01.000 --> 00:00:04.000 line:86%\n"
|
|
367
|
+
"First line\n"
|
|
368
|
+
"Second line\n"
|
|
369
|
+
"Third line\n"
|
|
370
|
+
)
|
|
371
|
+
captions = WebVTTReader().read(vtt)
|
|
372
|
+
scc_output = SCCWriter().write(captions)
|
|
373
|
+
reread = SCCReader().read(scc_output)
|
|
374
|
+
caps = reread.get_captions(reread.get_languages()[0])
|
|
375
|
+
assert caps[0].get_text() == "First line\nSecond line\nThird line"
|
|
376
|
+
pac_row_13 = WRITER_PAC_CODES[(13, 0, "plain")]
|
|
377
|
+
pac_row_14 = WRITER_PAC_CODES[(14, 0, "plain")]
|
|
378
|
+
pac_row_15 = WRITER_PAC_CODES[(15, 0, "plain")]
|
|
379
|
+
assert pac_row_13 in scc_output
|
|
380
|
+
assert pac_row_14 in scc_output
|
|
381
|
+
assert pac_row_15 in scc_output
|
|
382
|
+
|
|
322
383
|
|
|
323
384
|
class TestSCCWriterStyles:
|
|
324
385
|
def test_vtt_italic_emits_mid_row_code(self):
|
|
@@ -672,3 +672,25 @@ class TestGlobalCueColorPropagation:
|
|
|
672
672
|
style_opens = [n for n in nodes if n.type_ == CaptionNode.STYLE and n.start]
|
|
673
673
|
assert len(style_opens) == 1
|
|
674
674
|
assert style_opens[0].content.get("color") == "red"
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
class TestYouTubeKaraokeEmptyClassRoundtrip:
|
|
678
|
+
def test_empty_class_vtt_to_sami_roundtrip(self):
|
|
679
|
+
vtt = (
|
|
680
|
+
"WEBVTT\n\n"
|
|
681
|
+
"00:00:01.000 --> 00:00:04.000\n"
|
|
682
|
+
"<c> Hello</c><c> world</c>\n"
|
|
683
|
+
)
|
|
684
|
+
caption_set = WebVTTReader().read(vtt)
|
|
685
|
+
sami_output = SAMIWriter().write(caption_set)
|
|
686
|
+
|
|
687
|
+
assert 'class=""' not in sami_output
|
|
688
|
+
assert "Hello" in sami_output
|
|
689
|
+
assert "world" in sami_output
|
|
690
|
+
|
|
691
|
+
reread_set = SAMIReader().read(sami_output)
|
|
692
|
+
captions = reread_set.get_captions(reread_set.get_languages()[0])
|
|
693
|
+
assert len(captions) == 1
|
|
694
|
+
text = captions[0].get_text()
|
|
695
|
+
assert "Hello" in text
|
|
696
|
+
assert "world" in text
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|