pycaption 2.3.3__tar.gz → 2.3.4__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.
Files changed (71) hide show
  1. {pycaption-2.3.3/pycaption.egg-info → pycaption-2.3.4}/PKG-INFO +1 -1
  2. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/base.py +24 -4
  3. {pycaption-2.3.3 → pycaption-2.3.4/pycaption.egg-info}/PKG-INFO +1 -1
  4. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption.egg-info/SOURCES.txt +2 -0
  5. {pycaption-2.3.3 → pycaption-2.3.4}/setup.py +1 -1
  6. pycaption-2.3.4/tests/test_bytes_input.py +161 -0
  7. pycaption-2.3.4/tests/test_double_encoding.py +89 -0
  8. {pycaption-2.3.3 → pycaption-2.3.4}/LICENSE +0 -0
  9. {pycaption-2.3.3 → pycaption-2.3.4}/MANIFEST.in +0 -0
  10. {pycaption-2.3.3 → pycaption-2.3.4}/README.rst +0 -0
  11. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/__init__.py +0 -0
  12. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/dfxp/__init__.py +0 -0
  13. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/dfxp/constants.py +0 -0
  14. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/dfxp/extras.py +0 -0
  15. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/dfxp/reader.py +0 -0
  16. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/dfxp/writer.py +0 -0
  17. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/exceptions.py +0 -0
  18. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/geometry.py +0 -0
  19. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/microdvd.py +0 -0
  20. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/sami/__init__.py +0 -0
  21. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/sami/constants.py +0 -0
  22. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/sami/parser.py +0 -0
  23. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/sami/reader.py +0 -0
  24. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/sami/writer.py +0 -0
  25. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/scc/__init__.py +0 -0
  26. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/scc/constants.py +0 -0
  27. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/scc/reader.py +0 -0
  28. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/scc/specialized_collections.py +0 -0
  29. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/scc/state_machines.py +0 -0
  30. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/scc/translator.py +0 -0
  31. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/scc/writer.py +0 -0
  32. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/srt.py +0 -0
  33. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/transcript.py +0 -0
  34. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/utils.py +0 -0
  35. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/webvtt/__init__.py +0 -0
  36. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/webvtt/constants.py +0 -0
  37. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/webvtt/reader.py +0 -0
  38. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption/webvtt/writer.py +0 -0
  39. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption.egg-info/dependency_links.txt +0 -0
  40. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption.egg-info/requires.txt +0 -0
  41. {pycaption-2.3.3 → pycaption-2.3.4}/pycaption.egg-info/top_level.txt +0 -0
  42. {pycaption-2.3.3 → pycaption-2.3.4}/setup.cfg +0 -0
  43. {pycaption-2.3.3 → pycaption-2.3.4}/tests/__init__.py +0 -0
  44. {pycaption-2.3.3 → pycaption-2.3.4}/tests/conftest.py +0 -0
  45. {pycaption-2.3.3 → pycaption-2.3.4}/tests/fixtures/__init__.py +0 -0
  46. {pycaption-2.3.3 → pycaption-2.3.4}/tests/fixtures/dfxp.py +0 -0
  47. {pycaption-2.3.3 → pycaption-2.3.4}/tests/fixtures/microdvd.py +0 -0
  48. {pycaption-2.3.3 → pycaption-2.3.4}/tests/fixtures/sami.py +0 -0
  49. {pycaption-2.3.3 → pycaption-2.3.4}/tests/fixtures/scc.py +0 -0
  50. {pycaption-2.3.3 → pycaption-2.3.4}/tests/fixtures/srt.py +0 -0
  51. {pycaption-2.3.3 → pycaption-2.3.4}/tests/fixtures/translated_scc.py +0 -0
  52. {pycaption-2.3.3 → pycaption-2.3.4}/tests/fixtures/webvtt.py +0 -0
  53. {pycaption-2.3.3 → pycaption-2.3.4}/tests/mixins.py +0 -0
  54. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_base.py +0 -0
  55. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_dfxp.py +0 -0
  56. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_dfxp_conversion.py +0 -0
  57. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_dfxp_extras.py +0 -0
  58. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_functions.py +0 -0
  59. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_geometry.py +0 -0
  60. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_microdvd.py +0 -0
  61. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_microdvd_conversion.py +0 -0
  62. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_sami.py +0 -0
  63. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_sami_conversion.py +0 -0
  64. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_scc.py +0 -0
  65. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_scc_conversion.py +0 -0
  66. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_scc_translator.py +0 -0
  67. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_scc_writer.py +0 -0
  68. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_srt.py +0 -0
  69. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_srt_conversion.py +0 -0
  70. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_webvtt.py +0 -0
  71. {pycaption-2.3.3 → pycaption-2.3.4}/tests/test_webvtt_conversion.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycaption
3
- Version: 2.3.3
3
+ Version: 2.3.4
4
4
  Summary: Closed caption converter
5
5
  Author: Joe Norton
6
6
  Author-email: joey@nortoncrew.com
@@ -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, captions, styles=None, layout_info=None, regions=None,
415
+ self,
416
+ captions,
417
+ styles=None,
418
+ layout_info=None,
419
+ regions=None,
400
420
  visual_alignment_default=None,
401
421
  ):
402
422
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycaption
3
- Version: 2.3.3
3
+ Version: 2.3.4
4
4
  Summary: Closed caption converter
5
5
  Author: Joe Norton
6
6
  Author-email: joey@nortoncrew.com
@@ -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
@@ -20,7 +20,7 @@ transcript_dependencies = ["nltk==3.10.0"]
20
20
 
21
21
  setup(
22
22
  name="pycaption",
23
- version="2.3.3",
23
+ version="2.3.4",
24
24
  description="Closed caption converter",
25
25
  long_description=open(README_PATH).read(),
26
26
  author="Joe Norton",
@@ -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>&nbsp;</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
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