pycaption 2.3.3__tar.gz → 2.3.5__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.3/pycaption.egg-info → pycaption-2.3.5}/PKG-INFO +1 -1
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/base.py +24 -4
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/dfxp/constants.py +1 -1
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/dfxp/writer.py +3 -18
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/geometry.py +0 -9
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/sami/writer.py +0 -6
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/scc/specialized_collections.py +0 -1
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/webvtt/writer.py +0 -3
- {pycaption-2.3.3 → pycaption-2.3.5/pycaption.egg-info}/PKG-INFO +1 -1
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption.egg-info/SOURCES.txt +2 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/setup.py +1 -1
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/fixtures/webvtt.py +1 -1
- pycaption-2.3.5/tests/test_bytes_input.py +161 -0
- pycaption-2.3.5/tests/test_double_encoding.py +89 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_sami_conversion.py +4 -4
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_scc_conversion.py +4 -8
- {pycaption-2.3.3 → pycaption-2.3.5}/LICENSE +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/MANIFEST.in +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/README.rst +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/__init__.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/dfxp/__init__.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/dfxp/extras.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/dfxp/reader.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/exceptions.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/microdvd.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/sami/__init__.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/sami/constants.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/sami/parser.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/sami/reader.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/scc/__init__.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/scc/constants.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/scc/reader.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/scc/state_machines.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/scc/translator.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/scc/writer.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/srt.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/transcript.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/utils.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/webvtt/__init__.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/webvtt/constants.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption/webvtt/reader.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption.egg-info/dependency_links.txt +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption.egg-info/requires.txt +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/pycaption.egg-info/top_level.txt +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/setup.cfg +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/__init__.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/conftest.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/fixtures/__init__.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/fixtures/dfxp.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/fixtures/microdvd.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/fixtures/sami.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/fixtures/scc.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/fixtures/srt.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/fixtures/translated_scc.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/mixins.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_base.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_dfxp.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_dfxp_conversion.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_dfxp_extras.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_functions.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_geometry.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_microdvd.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_microdvd_conversion.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_sami.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_scc.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_scc_translator.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_scc_writer.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_srt.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_srt_conversion.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_webvtt.py +0 -0
- {pycaption-2.3.3 → pycaption-2.3.5}/tests/test_webvtt_conversion.py +0 -0
|
@@ -5,12 +5,15 @@ CaptionSet -> CaptionList -> Caption -> CaptionNode. Also provides the
|
|
|
5
5
|
CaptionConverter orchestrator and base classes for readers/writers.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import logging
|
|
8
9
|
import os
|
|
9
10
|
from datetime import timedelta
|
|
10
11
|
from numbers import Number
|
|
11
12
|
|
|
12
13
|
from .exceptions import CaptionReadError, CaptionReadTimingError, InvalidInputError
|
|
13
14
|
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
14
17
|
# `und` a special identifier for an undetermined language according to ISO 639-2
|
|
15
18
|
DEFAULT_LANGUAGE_CODE = os.getenv("PYCAPTION_DEFAULT_LANG", "und")
|
|
16
19
|
|
|
@@ -79,9 +82,7 @@ class BaseReader:
|
|
|
79
82
|
try:
|
|
80
83
|
content = content.decode("utf-8-sig")
|
|
81
84
|
except UnicodeDecodeError as e:
|
|
82
|
-
raise InvalidInputError(
|
|
83
|
-
f"Content is not valid UTF-8: {e}"
|
|
84
|
-
) from e
|
|
85
|
+
raise InvalidInputError(f"Content is not valid UTF-8: {e}") from e
|
|
85
86
|
elif isinstance(content, str):
|
|
86
87
|
if content.startswith(""):
|
|
87
88
|
content = content[1:]
|
|
@@ -89,8 +90,23 @@ class BaseReader:
|
|
|
89
90
|
raise InvalidInputError(
|
|
90
91
|
"The content must be a unicode string or UTF-8 bytes."
|
|
91
92
|
)
|
|
93
|
+
content = BaseReader._repair_double_encoding(content)
|
|
92
94
|
return content
|
|
93
95
|
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _repair_double_encoding(text):
|
|
98
|
+
"""Fix double-encoded UTF-8 (bytes misread as CP-1252)."""
|
|
99
|
+
try:
|
|
100
|
+
repaired = text.encode("cp1252").decode("utf-8")
|
|
101
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
102
|
+
return text
|
|
103
|
+
if repaired != text:
|
|
104
|
+
logger.warning(
|
|
105
|
+
"Detected and repaired double-encoded UTF-8 in caption content"
|
|
106
|
+
)
|
|
107
|
+
return repaired
|
|
108
|
+
return text
|
|
109
|
+
|
|
94
110
|
def detect(self, content):
|
|
95
111
|
"""Return True if content appears to be in this reader's format.
|
|
96
112
|
|
|
@@ -396,7 +412,11 @@ class CaptionSet:
|
|
|
396
412
|
"""
|
|
397
413
|
|
|
398
414
|
def __init__(
|
|
399
|
-
self,
|
|
415
|
+
self,
|
|
416
|
+
captions,
|
|
417
|
+
styles=None,
|
|
418
|
+
layout_info=None,
|
|
419
|
+
regions=None,
|
|
400
420
|
visual_alignment_default=None,
|
|
401
421
|
):
|
|
402
422
|
"""
|
|
@@ -30,7 +30,7 @@ DFXP_DEFAULT_REGION = Layout(
|
|
|
30
30
|
alignment=Alignment(HorizontalAlignmentEnum.START, VerticalAlignmentEnum.BOTTOM)
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
-
# Writer fallback alignment used when layout is None
|
|
33
|
+
# Writer fallback alignment used when layout is None.
|
|
34
34
|
DFXP_WRITER_FALLBACK_ALIGNMENT = Alignment(
|
|
35
35
|
HorizontalAlignmentEnum.CENTER, VerticalAlignmentEnum.BOTTOM
|
|
36
36
|
)
|
|
@@ -294,8 +294,7 @@ class RegionCreator:
|
|
|
294
294
|
def _collect_unique_regions(caption_set, ignore_region):
|
|
295
295
|
"""Collect all unique Layout objects from the caption set.
|
|
296
296
|
|
|
297
|
-
Excludes None
|
|
298
|
-
SCC positional layouts (which map to the default center region).
|
|
297
|
+
Excludes None and ignore_region (typically the default region).
|
|
299
298
|
|
|
300
299
|
:type caption_set: CaptionSet
|
|
301
300
|
:param ignore_region: a Layout to exclude from the result
|
|
@@ -315,9 +314,6 @@ class RegionCreator:
|
|
|
315
314
|
|
|
316
315
|
unique_regions.pop(None, None)
|
|
317
316
|
unique_regions.pop(ignore_region, None)
|
|
318
|
-
for layout in list(unique_regions):
|
|
319
|
-
if layout and layout.is_positional_anchor:
|
|
320
|
-
unique_regions.pop(layout)
|
|
321
317
|
return unique_regions
|
|
322
318
|
|
|
323
319
|
def _create_unique_regions(self, unique_layouts, dfxp, id_factory):
|
|
@@ -432,9 +428,6 @@ class RegionCreator:
|
|
|
432
428
|
if not layout_info:
|
|
433
429
|
layout_info = caption_set.layout_info
|
|
434
430
|
|
|
435
|
-
if layout_info and layout_info.is_positional_anchor:
|
|
436
|
-
layout_info = None
|
|
437
|
-
|
|
438
431
|
region_id = self._region_map.get(layout_info)
|
|
439
432
|
if not region_id:
|
|
440
433
|
region_id = DFXP_DEFAULT_REGION_ID
|
|
@@ -506,8 +499,8 @@ def _convert_layout_to_attributes(layout, fallback_alignment=None):
|
|
|
506
499
|
Maps origin, extent, padding, alignment, and writing_direction to their
|
|
507
500
|
tts: namespace equivalents.
|
|
508
501
|
|
|
509
|
-
When layout is None
|
|
510
|
-
|
|
502
|
+
When layout is None, uses fallback_alignment if provided
|
|
503
|
+
(per RP 2052-10 center promotion).
|
|
511
504
|
|
|
512
505
|
:type layout: Layout | None
|
|
513
506
|
:param fallback_alignment: Alignment to use when the layout lacks one.
|
|
@@ -521,14 +514,6 @@ def _convert_layout_to_attributes(layout, fallback_alignment=None):
|
|
|
521
514
|
return _create_external_alignment(fallback_alignment)
|
|
522
515
|
return result
|
|
523
516
|
|
|
524
|
-
if layout.is_positional_anchor:
|
|
525
|
-
if fallback_alignment:
|
|
526
|
-
result.update(_create_external_alignment(fallback_alignment))
|
|
527
|
-
writing_mode = _WRITING_DIRECTION_TO_DFXP.get(layout.writing_direction)
|
|
528
|
-
if writing_mode:
|
|
529
|
-
result["tts:writingMode"] = writing_mode
|
|
530
|
-
return result
|
|
531
|
-
|
|
532
517
|
if layout.origin:
|
|
533
518
|
result["tts:origin"] = layout.origin.to_xml_attribute()
|
|
534
519
|
|
|
@@ -597,7 +597,6 @@ class Layout:
|
|
|
597
597
|
alignment=None,
|
|
598
598
|
webvtt_positioning=None,
|
|
599
599
|
writing_direction=None,
|
|
600
|
-
is_positional_anchor=False,
|
|
601
600
|
inherit_from=None,
|
|
602
601
|
):
|
|
603
602
|
"""
|
|
@@ -623,11 +622,6 @@ class Layout:
|
|
|
623
622
|
:type writing_direction: WritingDirectionEnum
|
|
624
623
|
:param writing_direction: WebVTT vertical writing direction (rl or lr).
|
|
625
624
|
|
|
626
|
-
:type is_positional_anchor: bool
|
|
627
|
-
:param is_positional_anchor: True when the origin/alignment describe a
|
|
628
|
-
coordinate-system anchor point (e.g. SCC row/col positioning) rather
|
|
629
|
-
than visual text alignment.
|
|
630
|
-
|
|
631
625
|
:type inherit_from: Layout
|
|
632
626
|
:param inherit_from: A Layout with the positioning parameters to be
|
|
633
627
|
used if not specified by the positioning arguments,
|
|
@@ -639,7 +633,6 @@ class Layout:
|
|
|
639
633
|
self.alignment = alignment
|
|
640
634
|
self.webvtt_positioning = webvtt_positioning
|
|
641
635
|
self.writing_direction = writing_direction
|
|
642
|
-
self.is_positional_anchor = is_positional_anchor
|
|
643
636
|
|
|
644
637
|
if inherit_from:
|
|
645
638
|
for attr_name in [
|
|
@@ -716,7 +709,6 @@ class Layout:
|
|
|
716
709
|
params = {
|
|
717
710
|
"alignment": self.alignment,
|
|
718
711
|
"writing_direction": self.writing_direction,
|
|
719
|
-
"is_positional_anchor": self.is_positional_anchor,
|
|
720
712
|
}
|
|
721
713
|
for attr_name in ["origin", "extent", "padding"]:
|
|
722
714
|
attr = getattr(self, attr_name)
|
|
@@ -751,7 +743,6 @@ class Layout:
|
|
|
751
743
|
padding=self.padding,
|
|
752
744
|
alignment=self.alignment,
|
|
753
745
|
writing_direction=self.writing_direction,
|
|
754
|
-
is_positional_anchor=self.is_positional_anchor,
|
|
755
746
|
)
|
|
756
747
|
|
|
757
748
|
def _corrected_extent(self, diff_horizontal, diff_vertical):
|
|
@@ -131,9 +131,6 @@ class SAMIWriter(BaseWriter):
|
|
|
131
131
|
if not layout_info:
|
|
132
132
|
return
|
|
133
133
|
|
|
134
|
-
if layout_info.is_positional_anchor:
|
|
135
|
-
return
|
|
136
|
-
|
|
137
134
|
if layout_info.origin:
|
|
138
135
|
if layout_info.origin.x:
|
|
139
136
|
parts.append(f"margin-left:{layout_info.origin.x};")
|
|
@@ -152,9 +149,6 @@ class SAMIWriter(BaseWriter):
|
|
|
152
149
|
if not layout_info:
|
|
153
150
|
return "center" if self._promote_center else None
|
|
154
151
|
|
|
155
|
-
if layout_info.is_positional_anchor:
|
|
156
|
-
return "center" if self._promote_center else None
|
|
157
|
-
|
|
158
152
|
if not layout_info.alignment:
|
|
159
153
|
return "center" if self._promote_center else None
|
|
160
154
|
|
|
@@ -41,9 +41,11 @@ tests/__init__.py
|
|
|
41
41
|
tests/conftest.py
|
|
42
42
|
tests/mixins.py
|
|
43
43
|
tests/test_base.py
|
|
44
|
+
tests/test_bytes_input.py
|
|
44
45
|
tests/test_dfxp.py
|
|
45
46
|
tests/test_dfxp_conversion.py
|
|
46
47
|
tests/test_dfxp_extras.py
|
|
48
|
+
tests/test_double_encoding.py
|
|
47
49
|
tests/test_functions.py
|
|
48
50
|
tests/test_geometry.py
|
|
49
51
|
tests/test_microdvd.py
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Tests for bytes input support across all readers.
|
|
2
|
+
|
|
3
|
+
All readers should accept bytes (raw file content) and decode as UTF-8
|
|
4
|
+
internally, preventing the double-encoding gibberish that occurs when
|
|
5
|
+
callers decode with the wrong system encoding (e.g., cp1252).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pycaption import (
|
|
9
|
+
DFXPReader,
|
|
10
|
+
MicroDVDReader,
|
|
11
|
+
SAMIReader,
|
|
12
|
+
SCCReader,
|
|
13
|
+
SRTReader,
|
|
14
|
+
WebVTTReader,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestSRTReaderBytes:
|
|
19
|
+
def setup_class(self):
|
|
20
|
+
self.reader = SRTReader()
|
|
21
|
+
|
|
22
|
+
def test_read_bytes_produces_same_result_as_str(self, sample_srt):
|
|
23
|
+
from_str = self.reader.read(sample_srt)
|
|
24
|
+
from_bytes = self.reader.read(sample_srt.encode("utf-8"))
|
|
25
|
+
str_captions = from_str.get_captions("en-US")
|
|
26
|
+
bytes_captions = from_bytes.get_captions("en-US")
|
|
27
|
+
assert len(str_captions) == len(bytes_captions)
|
|
28
|
+
for s, b in zip(str_captions, bytes_captions):
|
|
29
|
+
assert s.get_text() == b.get_text()
|
|
30
|
+
assert s.start == b.start
|
|
31
|
+
assert s.end == b.end
|
|
32
|
+
|
|
33
|
+
def test_read_bytes_preserves_music_notes(self, sample_srt):
|
|
34
|
+
captions = self.reader.read(sample_srt.encode("utf-8"))
|
|
35
|
+
texts = [c.get_text() for c in captions.get_captions("en-US")]
|
|
36
|
+
assert any("♪" in t for t in texts)
|
|
37
|
+
|
|
38
|
+
def test_read_bytes_with_bom(self, sample_srt):
|
|
39
|
+
content_with_bom = b"\xef\xbb\xbf" + sample_srt.encode("utf-8")
|
|
40
|
+
captions = self.reader.read(content_with_bom)
|
|
41
|
+
assert len(captions.get_captions("en-US")) == 7
|
|
42
|
+
|
|
43
|
+
def test_detect_bytes(self, sample_srt):
|
|
44
|
+
assert self.reader.detect(sample_srt.encode("utf-8")) is True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestWebVTTReaderBytes:
|
|
48
|
+
def setup_class(self):
|
|
49
|
+
self.reader = WebVTTReader()
|
|
50
|
+
|
|
51
|
+
def test_read_bytes_produces_same_result_as_str(self, sample_webvtt):
|
|
52
|
+
from_str = self.reader.read(sample_webvtt)
|
|
53
|
+
from_bytes = self.reader.read(sample_webvtt.encode("utf-8"))
|
|
54
|
+
str_captions = from_str.get_captions("en-US")
|
|
55
|
+
bytes_captions = from_bytes.get_captions("en-US")
|
|
56
|
+
assert len(str_captions) == len(bytes_captions)
|
|
57
|
+
for s, b in zip(str_captions, bytes_captions):
|
|
58
|
+
assert s.get_text() == b.get_text()
|
|
59
|
+
|
|
60
|
+
def test_read_bytes_with_bom(self, sample_webvtt):
|
|
61
|
+
content_with_bom = b"\xef\xbb\xbf" + sample_webvtt.encode("utf-8")
|
|
62
|
+
captions = self.reader.read(content_with_bom)
|
|
63
|
+
assert len(captions.get_captions("en-US")) > 0
|
|
64
|
+
|
|
65
|
+
def test_detect_bytes(self, sample_webvtt):
|
|
66
|
+
assert self.reader.detect(sample_webvtt.encode("utf-8")) is True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestDFXPReaderBytes:
|
|
70
|
+
def setup_class(self):
|
|
71
|
+
self.reader = DFXPReader()
|
|
72
|
+
|
|
73
|
+
def test_read_bytes_produces_same_result_as_str(self, sample_dfxp):
|
|
74
|
+
from_str = self.reader.read(sample_dfxp)
|
|
75
|
+
from_bytes = self.reader.read(sample_dfxp.encode("utf-8"))
|
|
76
|
+
for lang in from_str.get_languages():
|
|
77
|
+
str_captions = from_str.get_captions(lang)
|
|
78
|
+
bytes_captions = from_bytes.get_captions(lang)
|
|
79
|
+
assert len(str_captions) == len(bytes_captions)
|
|
80
|
+
for s, b in zip(str_captions, bytes_captions):
|
|
81
|
+
assert s.get_text() == b.get_text()
|
|
82
|
+
|
|
83
|
+
def test_read_bytes_with_bom(self, sample_dfxp):
|
|
84
|
+
content_with_bom = b"\xef\xbb\xbf" + sample_dfxp.encode("utf-8")
|
|
85
|
+
captions = self.reader.read(content_with_bom)
|
|
86
|
+
assert not captions.is_empty()
|
|
87
|
+
|
|
88
|
+
def test_detect_bytes(self, sample_dfxp):
|
|
89
|
+
assert self.reader.detect(sample_dfxp.encode("utf-8")) is True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestSAMIReaderBytes:
|
|
93
|
+
def setup_class(self):
|
|
94
|
+
self.reader = SAMIReader()
|
|
95
|
+
|
|
96
|
+
def test_read_bytes_produces_same_result_as_str(self, sample_sami):
|
|
97
|
+
from_str = self.reader.read(sample_sami)
|
|
98
|
+
from_bytes = self.reader.read(sample_sami.encode("utf-8"))
|
|
99
|
+
for lang in from_str.get_languages():
|
|
100
|
+
str_captions = from_str.get_captions(lang)
|
|
101
|
+
bytes_captions = from_bytes.get_captions(lang)
|
|
102
|
+
assert len(str_captions) == len(bytes_captions)
|
|
103
|
+
for s, b in zip(str_captions, bytes_captions):
|
|
104
|
+
assert s.get_text() == b.get_text()
|
|
105
|
+
|
|
106
|
+
def test_read_bytes_preserves_music_notes(self, sample_sami):
|
|
107
|
+
captions = self.reader.read(sample_sami.encode("utf-8"))
|
|
108
|
+
langs = list(captions.get_languages())
|
|
109
|
+
texts = [c.get_text() for c in captions.get_captions(langs[0])]
|
|
110
|
+
assert any("♪" in t for t in texts)
|
|
111
|
+
|
|
112
|
+
def test_read_bytes_with_bom(self, sample_sami):
|
|
113
|
+
content_with_bom = b"\xef\xbb\xbf" + sample_sami.encode("utf-8")
|
|
114
|
+
captions = self.reader.read(content_with_bom)
|
|
115
|
+
assert not captions.is_empty()
|
|
116
|
+
|
|
117
|
+
def test_detect_bytes(self, sample_sami):
|
|
118
|
+
assert self.reader.detect(sample_sami.encode("utf-8")) is True
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestSCCReaderBytes:
|
|
122
|
+
def test_read_bytes_produces_same_result_as_str(self, sample_scc_pop_on):
|
|
123
|
+
from_str = SCCReader().read(sample_scc_pop_on)
|
|
124
|
+
from_bytes = SCCReader().read(sample_scc_pop_on.encode("utf-8"))
|
|
125
|
+
str_captions = from_str.get_captions("en-US")
|
|
126
|
+
bytes_captions = from_bytes.get_captions("en-US")
|
|
127
|
+
assert len(str_captions) == len(bytes_captions)
|
|
128
|
+
for s, b in zip(str_captions, bytes_captions):
|
|
129
|
+
assert s.get_text() == b.get_text()
|
|
130
|
+
|
|
131
|
+
def test_read_bytes_with_bom(self, sample_scc_pop_on):
|
|
132
|
+
content_with_bom = b"\xef\xbb\xbf" + sample_scc_pop_on.encode("utf-8")
|
|
133
|
+
captions = SCCReader().read(content_with_bom)
|
|
134
|
+
assert len(captions.get_captions("en-US")) > 0
|
|
135
|
+
|
|
136
|
+
def test_detect_bytes(self, sample_scc_pop_on):
|
|
137
|
+
assert SCCReader().detect(sample_scc_pop_on.encode("utf-8")) is True
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TestMicroDVDReaderBytes:
|
|
141
|
+
def setup_class(self):
|
|
142
|
+
self.reader = MicroDVDReader()
|
|
143
|
+
|
|
144
|
+
def test_read_bytes_produces_same_result_as_str(self, sample_microdvd):
|
|
145
|
+
from_str = self.reader.read(sample_microdvd)
|
|
146
|
+
from_bytes = self.reader.read(sample_microdvd.encode("utf-8"))
|
|
147
|
+
str_captions = from_str.get_captions("und")
|
|
148
|
+
bytes_captions = from_bytes.get_captions("und")
|
|
149
|
+
assert len(str_captions) == len(bytes_captions)
|
|
150
|
+
for s, b in zip(str_captions, bytes_captions):
|
|
151
|
+
assert s.get_text() == b.get_text()
|
|
152
|
+
|
|
153
|
+
def test_read_bytes_with_bom(self, sample_microdvd):
|
|
154
|
+
content_with_bom = b"\xef\xbb\xbf" + sample_microdvd.encode("utf-8")
|
|
155
|
+
captions = self.reader.read(content_with_bom)
|
|
156
|
+
assert not captions.is_empty()
|
|
157
|
+
|
|
158
|
+
def test_detect_bytes(self, sample_microdvd):
|
|
159
|
+
assert self.reader.detect(sample_microdvd.encode("utf-8")) is True
|
|
160
|
+
|
|
161
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from pycaption import SAMIReader, SRTReader
|
|
6
|
+
from pycaption.base import BaseReader
|
|
7
|
+
from pycaption.dfxp import DFXPReader
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _double_encode(text):
|
|
11
|
+
"""Simulate double-encoding: UTF-8 bytes misread as CP-1252, re-encoded."""
|
|
12
|
+
return text.encode("utf-8").decode("cp1252")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
ORIGINAL_CHARS = ["♪", "—", "’", "é"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestRepairDoubleEncoding:
|
|
19
|
+
@pytest.mark.parametrize("original", ORIGINAL_CHARS)
|
|
20
|
+
def test_fixes_double_encoded(self, original):
|
|
21
|
+
garbled = _double_encode(original)
|
|
22
|
+
assert BaseReader._repair_double_encoding(garbled) == original
|
|
23
|
+
|
|
24
|
+
def test_leaves_clean_utf8_alone(self):
|
|
25
|
+
clean = "♪ This is — perfectly fine é text"
|
|
26
|
+
assert BaseReader._repair_double_encoding(clean) == clean
|
|
27
|
+
|
|
28
|
+
def test_logs_warning_on_repair(self, caplog):
|
|
29
|
+
garbled = _double_encode("♪")
|
|
30
|
+
with caplog.at_level(logging.WARNING, logger="pycaption.base"):
|
|
31
|
+
BaseReader._repair_double_encoding(garbled)
|
|
32
|
+
assert "double-encoded" in caplog.text.lower()
|
|
33
|
+
|
|
34
|
+
def test_no_warning_for_clean_input(self, caplog):
|
|
35
|
+
with caplog.at_level(logging.WARNING, logger="pycaption.base"):
|
|
36
|
+
BaseReader._repair_double_encoding("♪ Music ♪")
|
|
37
|
+
assert caplog.text == ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestDoubleEncodingEndToEnd:
|
|
41
|
+
def test_srt_reader(self):
|
|
42
|
+
garbled_note = _double_encode("♪")
|
|
43
|
+
content = (
|
|
44
|
+
"1\n"
|
|
45
|
+
"00:00:01,000 --> 00:00:02,000\n"
|
|
46
|
+
f"{garbled_note} Music {garbled_note}\n"
|
|
47
|
+
)
|
|
48
|
+
captions = SRTReader().read(content)
|
|
49
|
+
nodes = captions.get_captions("en-US")[0].nodes
|
|
50
|
+
text = "".join(n.content for n in nodes)
|
|
51
|
+
assert "♪" in text
|
|
52
|
+
assert garbled_note not in text
|
|
53
|
+
|
|
54
|
+
def test_dfxp_reader(self):
|
|
55
|
+
garbled = _double_encode("élève")
|
|
56
|
+
content = (
|
|
57
|
+
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
58
|
+
'<tt xml:lang="en" xmlns="http://www.w3.org/ns/ttml">\n'
|
|
59
|
+
" <body><div>\n"
|
|
60
|
+
f' <p begin="00:00:01.000" end="00:00:02.000">'
|
|
61
|
+
f"{garbled}</p>\n"
|
|
62
|
+
" </div></body>\n"
|
|
63
|
+
"</tt>\n"
|
|
64
|
+
)
|
|
65
|
+
captions = DFXPReader().read(content)
|
|
66
|
+
nodes = captions.get_captions("en")[0].nodes
|
|
67
|
+
text = "".join(n.content for n in nodes)
|
|
68
|
+
assert "élève" in text
|
|
69
|
+
assert garbled not in text
|
|
70
|
+
|
|
71
|
+
def test_sami_reader(self):
|
|
72
|
+
garbled_dash = _double_encode("—")
|
|
73
|
+
content = (
|
|
74
|
+
"<SAMI>\n"
|
|
75
|
+
"<Body>\n"
|
|
76
|
+
" <Sync Start=1000>\n"
|
|
77
|
+
f" <P Class=enCC>{garbled_dash} Hello</P>\n"
|
|
78
|
+
" </Sync>\n"
|
|
79
|
+
" <Sync Start=2000>\n"
|
|
80
|
+
" <P Class=enCC> </P>\n"
|
|
81
|
+
" </Sync>\n"
|
|
82
|
+
"</Body>\n"
|
|
83
|
+
"</SAMI>\n"
|
|
84
|
+
)
|
|
85
|
+
captions = SAMIReader().read(content)
|
|
86
|
+
lang = list(captions.get_languages())[0]
|
|
87
|
+
nodes = captions.get_captions(lang)[0].nodes
|
|
88
|
+
text = "".join(n.content for n in nodes)
|
|
89
|
+
assert "—" in text
|
|
@@ -211,10 +211,10 @@ class TestWebVTTtoSAMIStyles(SAMITestingMixIn):
|
|
|
211
211
|
|
|
212
212
|
|
|
213
213
|
class TestSCCtoSAMI(SAMITestingMixIn):
|
|
214
|
-
def
|
|
214
|
+
def test_scc_to_sami_preserves_positioning(self, sample_scc_pop_on):
|
|
215
215
|
caption_set = SCCReader().read(sample_scc_pop_on)
|
|
216
216
|
result = SAMIWriter().write(caption_set)
|
|
217
217
|
|
|
218
|
-
assert "text-align:
|
|
219
|
-
assert "margin-left"
|
|
220
|
-
assert "margin-top"
|
|
218
|
+
assert "text-align:left;" in result
|
|
219
|
+
assert "margin-left:" in result
|
|
220
|
+
assert "margin-top:" in result
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
2
|
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
3
|
from pycaption import (
|
|
6
4
|
DFXPWriter,
|
|
7
5
|
SCCReader,
|
|
@@ -42,9 +40,8 @@ class TestSCCtoDFXP:
|
|
|
42
40
|
caption_set = SCCReader().read(sample_scc_multiple_positioning)
|
|
43
41
|
dfxp = DFXPWriter(relativize=False, fit_to_screen=False).write(caption_set)
|
|
44
42
|
|
|
45
|
-
assert 'tts:textAlign="
|
|
46
|
-
assert
|
|
47
|
-
assert "tts:origin" not in dfxp
|
|
43
|
+
assert 'tts:textAlign="left"' in dfxp
|
|
44
|
+
assert "tts:origin" in dfxp
|
|
48
45
|
assert "abab" in dfxp
|
|
49
46
|
assert "ghgh" in dfxp
|
|
50
47
|
|
|
@@ -58,8 +55,7 @@ class TestSCCtoDFXP:
|
|
|
58
55
|
|
|
59
56
|
dfxp = DFXPWriter().write(caption_set)
|
|
60
57
|
|
|
61
|
-
assert 'tts:textAlign="
|
|
62
|
-
assert 'tts:textAlign="left"' not in dfxp
|
|
58
|
+
assert 'tts:textAlign="left"' in dfxp
|
|
63
59
|
assert 'tts:fontStyle="italic"' in dfxp
|
|
64
60
|
from bs4 import BeautifulSoup
|
|
65
61
|
|
|
@@ -72,7 +68,7 @@ class TestSCCtoDFXP:
|
|
|
72
68
|
|
|
73
69
|
dfxp = DFXPWriter().write(caption_set)
|
|
74
70
|
|
|
75
|
-
assert 'tts:textAlign="
|
|
71
|
+
assert 'tts:textAlign="left"' in dfxp
|
|
76
72
|
assert "&" in dfxp
|
|
77
73
|
from bs4 import BeautifulSoup
|
|
78
74
|
|
|
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
|