pycaption 2.3.10__tar.gz → 2.3.13__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.10/pycaption.egg-info → pycaption-2.3.13}/PKG-INFO +1 -1
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/__init__.py +1 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/base.py +3 -2
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/dfxp/constants.py +1 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/dfxp/extras.py +4 -2
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/dfxp/reader.py +8 -7
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/dfxp/writer.py +9 -4
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/geometry.py +3 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/microdvd.py +1 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/sami/constants.py +1 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/sami/reader.py +9 -2
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/sami/writer.py +3 -1
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/scc/__init__.py +1 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/scc/constants.py +5 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/scc/reader.py +43 -7
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/scc/specialized_collections.py +516 -57
- pycaption-2.3.13/pycaption/scc/state_machines.py +450 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/scc/writer.py +37 -13
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/srt.py +20 -4
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/transcript.py +7 -1
- pycaption-2.3.13/pycaption/utils.py +26 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/webvtt/__init__.py +1 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/webvtt/constants.py +1 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/webvtt/reader.py +1 -1
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/webvtt/writer.py +3 -1
- {pycaption-2.3.10 → pycaption-2.3.13/pycaption.egg-info}/PKG-INFO +1 -1
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption.egg-info/SOURCES.txt +0 -1
- pycaption-2.3.13/setup.cfg +4 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/setup.py +3 -2
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/conftest.py +59 -7
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/dfxp.py +18 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/sami.py +35 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/scc.py +826 -7
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/webvtt.py +1 -1
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/mixins.py +1 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_base.py +72 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_bytes_input.py +0 -2
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_dfxp.py +46 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_dfxp_conversion.py +4 -3
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_dfxp_extras.py +20 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_double_encoding.py +1 -3
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_sami.py +65 -9
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_sami_conversion.py +4 -7
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_scc.py +893 -45
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_scc_conversion.py +86 -24
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_scc_translator.py +1 -3
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_scc_writer.py +107 -49
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_srt.py +66 -1
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_webvtt.py +12 -32
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_webvtt_conversion.py +21 -61
- pycaption-2.3.10/pycaption/scc/state_machines.py +0 -207
- pycaption-2.3.10/pycaption/utils.py +0 -13
- pycaption-2.3.10/setup.cfg +0 -14
- {pycaption-2.3.10 → pycaption-2.3.13}/LICENSE +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/MANIFEST.in +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/README.rst +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/dfxp/__init__.py +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/exceptions.py +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/sami/__init__.py +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/sami/parser.py +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/scc/translator.py +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption.egg-info/dependency_links.txt +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption.egg-info/requires.txt +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/pycaption.egg-info/top_level.txt +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/__init__.py +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/__init__.py +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/microdvd.py +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/srt.py +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/translated_scc.py +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_functions.py +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_geometry.py +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_microdvd.py +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_microdvd_conversion.py +0 -0
- {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_srt_conversion.py +0 -0
|
@@ -7,11 +7,13 @@ CaptionConverter orchestrator and base classes for readers/writers.
|
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
|
+
|
|
10
11
|
from datetime import timedelta
|
|
11
12
|
from numbers import Number
|
|
12
13
|
|
|
13
14
|
from .exceptions import CaptionReadError, CaptionReadTimingError, InvalidInputError
|
|
14
15
|
|
|
16
|
+
|
|
15
17
|
logger = logging.getLogger(__name__)
|
|
16
18
|
|
|
17
19
|
# `und` a special identifier for an undetermined language according to ISO 639-2
|
|
@@ -40,8 +42,7 @@ class CaptionConverter:
|
|
|
40
42
|
"""
|
|
41
43
|
if not hasattr(caption_reader, "read"):
|
|
42
44
|
raise InvalidInputError(
|
|
43
|
-
"The caption_reader must be a BaseReader instance "
|
|
44
|
-
"with a read() method."
|
|
45
|
+
"The caption_reader must be a BaseReader instance with a read() method."
|
|
45
46
|
)
|
|
46
47
|
self.captions = caption_reader.read(content)
|
|
47
48
|
return self
|
|
@@ -10,9 +10,11 @@ from xml.sax.saxutils import escape
|
|
|
10
10
|
from bs4 import BeautifulSoup
|
|
11
11
|
|
|
12
12
|
from ..base import BaseWriter, CaptionNode, merge_concurrent_captions
|
|
13
|
+
from ..utils import encode_leading_nbsp
|
|
13
14
|
from .constants import DFXP_DEFAULT_REGION
|
|
14
15
|
from .writer import DFXPWriter
|
|
15
16
|
|
|
17
|
+
|
|
16
18
|
LEGACY_DFXP_BASE_MARKUP = """
|
|
17
19
|
<tt xmlns="http://www.w3.org/ns/ttml"
|
|
18
20
|
xmlns:tts="http://www.w3.org/ns/ttml#styling">
|
|
@@ -203,7 +205,7 @@ class LegacyDFXPWriter(BaseWriter):
|
|
|
203
205
|
start = caption.format_start()
|
|
204
206
|
end = caption.format_end()
|
|
205
207
|
p = dfxp.new_tag("p", begin=start, end=end)
|
|
206
|
-
p.string = self._recreate_text(caption, dfxp)
|
|
208
|
+
p.string = encode_leading_nbsp(self._recreate_text(caption, dfxp))
|
|
207
209
|
|
|
208
210
|
if dfxp.find("style", {"xml:id": "p"}):
|
|
209
211
|
p["style"] = "p"
|
|
@@ -255,7 +257,7 @@ class LegacyDFXPWriter(BaseWriter):
|
|
|
255
257
|
|
|
256
258
|
return line
|
|
257
259
|
|
|
258
|
-
def _recreate_style(self, content, dfxp):
|
|
260
|
+
def _recreate_style(self, content, dfxp): # noqa: C901
|
|
259
261
|
"""Convert an internal style dict to DFXP/TTS attributes.
|
|
260
262
|
|
|
261
263
|
:type content: dict
|
|
@@ -47,7 +47,9 @@ from .constants import (
|
|
|
47
47
|
VERTICAL_ALIGNMENT_TO_DFXP,
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
|
|
51
|
+
# Pretty-print indentation only: \s would also eat U+00A0 line indentation.
|
|
52
|
+
_LEADING_WHITESPACE_RE = re.compile("^(?:[\n\r]+[ \t\n\r]*)?(.+)")
|
|
51
53
|
|
|
52
54
|
_DFXP_WRITING_MODE_MAP = {
|
|
53
55
|
"tbrl": WritingDirectionEnum.VERTICAL_RL,
|
|
@@ -147,7 +149,8 @@ class DFXPReader(BaseReader):
|
|
|
147
149
|
style_dict[id_] = self._convert_style(style)
|
|
148
150
|
|
|
149
151
|
caption_set = CaptionSet(
|
|
150
|
-
caption_dict,
|
|
152
|
+
caption_dict,
|
|
153
|
+
styles=style_dict,
|
|
151
154
|
visual_alignment_default=HorizontalAlignmentEnum.START,
|
|
152
155
|
)
|
|
153
156
|
|
|
@@ -179,8 +182,7 @@ class DFXPReader(BaseReader):
|
|
|
179
182
|
tickrate = float(tt_attrs["ttp:tickrate"])
|
|
180
183
|
except ValueError:
|
|
181
184
|
raise CaptionReadSyntaxError(
|
|
182
|
-
f"ttp:tickRate must be a number, "
|
|
183
|
-
f"got '{tt_attrs['ttp:tickrate']}'"
|
|
185
|
+
f"ttp:tickRate must be a number, got '{tt_attrs['ttp:tickrate']}'"
|
|
184
186
|
)
|
|
185
187
|
if tickrate <= 0:
|
|
186
188
|
raise CaptionReadSyntaxError(
|
|
@@ -203,8 +205,7 @@ class DFXPReader(BaseReader):
|
|
|
203
205
|
framerate_int = int(framerate_str)
|
|
204
206
|
except ValueError:
|
|
205
207
|
raise CaptionReadSyntaxError(
|
|
206
|
-
f"ttp:frameRate must be a positive integer, "
|
|
207
|
-
f"got '{framerate_str}'"
|
|
208
|
+
f"ttp:frameRate must be a positive integer, got '{framerate_str}'"
|
|
208
209
|
)
|
|
209
210
|
self.tickrate = float(framerate_int * sub_framerate)
|
|
210
211
|
|
|
@@ -396,7 +397,7 @@ class DFXPReader(BaseReader):
|
|
|
396
397
|
self._convert_tag_to_node(a)
|
|
397
398
|
|
|
398
399
|
@staticmethod
|
|
399
|
-
def _convert_style(tag):
|
|
400
|
+
def _convert_style(tag): # noqa: C901
|
|
400
401
|
"""Convert DFXP/TTS style attributes on a tag to an internal style dict.
|
|
401
402
|
|
|
402
403
|
Maps tts:fontStyle, tts:fontWeight, tts:textDecoration, tts:textAlign,
|
|
@@ -5,6 +5,7 @@ including style elements, region-based positioning, and writing direction.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
|
+
|
|
8
9
|
from copy import deepcopy
|
|
9
10
|
from xml.sax.saxutils import escape
|
|
10
11
|
|
|
@@ -12,6 +13,7 @@ from bs4 import BeautifulSoup
|
|
|
12
13
|
|
|
13
14
|
from ..base import BaseWriter, CaptionNode
|
|
14
15
|
from ..geometry import HorizontalAlignmentEnum, LineAlignmentEnum, WritingDirectionEnum
|
|
16
|
+
from ..utils import encode_leading_nbsp
|
|
15
17
|
from .constants import (
|
|
16
18
|
DFXP_ATTR_XML_ID,
|
|
17
19
|
DFXP_ATTR_XML_LANG,
|
|
@@ -26,6 +28,7 @@ from .constants import (
|
|
|
26
28
|
_create_external_alignment,
|
|
27
29
|
)
|
|
28
30
|
|
|
31
|
+
|
|
29
32
|
_WRITING_DIRECTION_TO_DFXP = {
|
|
30
33
|
WritingDirectionEnum.VERTICAL_RL: "tbrl",
|
|
31
34
|
WritingDirectionEnum.VERTICAL_LR: "tblr",
|
|
@@ -204,7 +207,9 @@ class DFXPWriter(BaseWriter):
|
|
|
204
207
|
start = caption.format_start()
|
|
205
208
|
end = caption.format_end()
|
|
206
209
|
p = dfxp.new_tag("p", begin=start, end=end)
|
|
207
|
-
p.string =
|
|
210
|
+
p.string = encode_leading_nbsp(
|
|
211
|
+
self._recreate_text(caption, dfxp, caption_set, lang)
|
|
212
|
+
)
|
|
208
213
|
|
|
209
214
|
if dfxp.find("style", {DFXP_ATTR_XML_ID: "p"}):
|
|
210
215
|
p["style"] = "p"
|
|
@@ -213,7 +218,7 @@ class DFXPWriter(BaseWriter):
|
|
|
213
218
|
|
|
214
219
|
return p
|
|
215
220
|
|
|
216
|
-
def _recreate_text(self, caption, dfxp, caption_set=None, lang=None):
|
|
221
|
+
def _recreate_text(self, caption, dfxp, caption_set=None, lang=None): # noqa: C901
|
|
217
222
|
"""Serialize all nodes of a caption into DFXP inline markup.
|
|
218
223
|
|
|
219
224
|
Handles text nodes, line breaks (<br/>), and style spans. Nodes are
|
|
@@ -756,7 +761,7 @@ def _text_shadow_to_outline(value):
|
|
|
756
761
|
return thickness
|
|
757
762
|
|
|
758
763
|
|
|
759
|
-
def _recreate_style(content, dfxp):
|
|
764
|
+
def _recreate_style(content, dfxp): # noqa: C901
|
|
760
765
|
"""Convert an internal style dict to DFXP/TTS style attributes.
|
|
761
766
|
|
|
762
767
|
Maps pycaption's internal keys (class, italics, bold, underline, color,
|
|
@@ -807,7 +812,7 @@ def _recreate_style(content, dfxp):
|
|
|
807
812
|
return dfxp_style
|
|
808
813
|
|
|
809
814
|
|
|
810
|
-
def _convert_layout_to_attributes(layout, fallback_alignment=None):
|
|
815
|
+
def _convert_layout_to_attributes(layout, fallback_alignment=None): # noqa: C901
|
|
811
816
|
"""Convert a Layout object to a dict of DFXP region attributes.
|
|
812
817
|
|
|
813
818
|
Maps origin, extent, padding, alignment, and writing_direction to their
|
|
@@ -7,12 +7,15 @@ CONVENTIONS:
|
|
|
7
7
|
responsible for the recalculation should return a new object with the
|
|
8
8
|
necessary modifications.
|
|
9
9
|
"""
|
|
10
|
+
|
|
10
11
|
import re
|
|
12
|
+
|
|
11
13
|
from enum import Enum
|
|
12
14
|
from functools import total_ordering
|
|
13
15
|
|
|
14
16
|
from .exceptions import CaptionReadSyntaxError, RelativizationError
|
|
15
17
|
|
|
18
|
+
|
|
16
19
|
_UNIT_MISMATCH_MSG = "The sizes should have the same measure units."
|
|
17
20
|
|
|
18
21
|
|
|
@@ -4,6 +4,8 @@ Parses SAMI documents into pycaption CaptionSet objects, handling CSS
|
|
|
4
4
|
styles, margin-based positioning, and multi-language content.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import re
|
|
8
|
+
|
|
7
9
|
from bs4 import BeautifulSoup, NavigableString
|
|
8
10
|
|
|
9
11
|
from ..base import BaseReader, Caption, CaptionList, CaptionNode, CaptionSet
|
|
@@ -11,8 +13,12 @@ from ..exceptions import CaptionReadNoCaptions, CaptionReadTimingError
|
|
|
11
13
|
from ..geometry import Alignment, HorizontalAlignmentEnum, Layout, Padding, Size
|
|
12
14
|
from .parser import SAMIParser
|
|
13
15
|
|
|
16
|
+
|
|
14
17
|
_TAG_TO_STYLE = {"i": "italics", "b": "bold", "u": "underline"}
|
|
15
18
|
|
|
19
|
+
# A line break plus the pretty-print indentation of the closing tag.
|
|
20
|
+
_TRAILING_WHITESPACE_RE = re.compile("[\n\r][ \t\n\r]*$")
|
|
21
|
+
|
|
16
22
|
|
|
17
23
|
class SAMIReader(BaseReader):
|
|
18
24
|
"""Reads SAMI caption files into a CaptionSet.
|
|
@@ -202,8 +208,9 @@ class SAMIReader(BaseReader):
|
|
|
202
208
|
if isinstance(tag, NavigableString):
|
|
203
209
|
tag_text = str(tag)
|
|
204
210
|
if tag_text and tag_text[0] in "\n\r":
|
|
205
|
-
|
|
206
|
-
|
|
211
|
+
# Not a bare lstrip(): U+00A0 is line indentation, not markup.
|
|
212
|
+
tag_text = tag_text.lstrip(" \t\n\r")
|
|
213
|
+
tag_text = _TRAILING_WHITESPACE_RE.sub("", tag_text)
|
|
207
214
|
if not tag_text:
|
|
208
215
|
return
|
|
209
216
|
self.line.append(CaptionNode.create_text(tag_text, inherit_from))
|
|
@@ -11,8 +11,10 @@ from bs4 import BeautifulSoup
|
|
|
11
11
|
|
|
12
12
|
from ..base import BaseWriter, CaptionNode
|
|
13
13
|
from ..geometry import HorizontalAlignmentEnum
|
|
14
|
+
from ..utils import encode_leading_nbsp
|
|
14
15
|
from .constants import HORIZONTAL_ALIGNMENT_MAP, SAMI_BASE_MARKUP
|
|
15
16
|
|
|
17
|
+
|
|
16
18
|
_NON_CSS_KEYS = frozenset(
|
|
17
19
|
{
|
|
18
20
|
"classes",
|
|
@@ -103,7 +105,7 @@ class SAMIWriter(BaseWriter):
|
|
|
103
105
|
p["style"] = p_style
|
|
104
106
|
|
|
105
107
|
p["class"] = self._recreate_p_lang(caption, lang, captions)
|
|
106
|
-
p.string = self._recreate_text(caption.nodes)
|
|
108
|
+
p.string = encode_leading_nbsp(self._recreate_text(caption.nodes))
|
|
107
109
|
|
|
108
110
|
sync.append(p)
|
|
109
111
|
|
|
@@ -7,8 +7,10 @@ background color codes, and derived inverse lookup tables.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import re as _re
|
|
10
|
+
|
|
10
11
|
from itertools import product
|
|
11
12
|
|
|
13
|
+
|
|
12
14
|
COMMANDS = {
|
|
13
15
|
"9420": "",
|
|
14
16
|
"9429": "",
|
|
@@ -1317,6 +1319,9 @@ SPECIAL_OR_EXTENDED_CHAR_TO_CODE = {
|
|
|
1317
1319
|
SPECIAL_OR_EXTENDED_CHAR_TO_CODE.update(
|
|
1318
1320
|
{character: code for code, character in SPECIAL_CHARS.items()}
|
|
1319
1321
|
)
|
|
1322
|
+
# A non-breaking space is what CEA-608's transparent space is: a blank column
|
|
1323
|
+
# that is not a break opportunity. Unmapped, it fell back to the pound sign.
|
|
1324
|
+
SPECIAL_OR_EXTENDED_CHAR_TO_CODE["\xa0"] = "91b9"
|
|
1320
1325
|
|
|
1321
1326
|
# Time to transmit a single codeword = 1 second / 29.97
|
|
1322
1327
|
MICROSECONDS_PER_CODEWORD = 1000.0 * 1000.0 / (30.0 * 1000.0 / 1001.0)
|
|
@@ -78,6 +78,7 @@ http://www.theneitherworld.com/mcpoodle/SCC_TOOLS/DOCS/SCC_FORMAT.HTML
|
|
|
78
78
|
"""
|
|
79
79
|
|
|
80
80
|
import re
|
|
81
|
+
|
|
81
82
|
from collections import deque
|
|
82
83
|
from copy import deepcopy
|
|
83
84
|
|
|
@@ -108,8 +109,13 @@ from .specialized_collections import (
|
|
|
108
109
|
)
|
|
109
110
|
from .state_machines import DefaultProvidingPositionTracker
|
|
110
111
|
|
|
112
|
+
|
|
111
113
|
_TIMECODE_RE = re.compile(r"\d{2}:\d{2}:\d{2}[:;](\d{1,2})")
|
|
112
114
|
|
|
115
|
+
# Appended to a too-long line whose text kept going on the row with no PAC or
|
|
116
|
+
# row change of its own. Matched downstream, so the wording is part of the API.
|
|
117
|
+
ROW_CONTINUATION_SUFFIX = " (continues on the row without a row change)"
|
|
118
|
+
|
|
113
119
|
|
|
114
120
|
class SCCReader(BaseReader):
|
|
115
121
|
"""Converts a given unicode string to a CaptionSet.
|
|
@@ -241,18 +247,43 @@ class SCCReader(BaseReader):
|
|
|
241
247
|
|
|
242
248
|
def _validate_line_lengths(self):
|
|
243
249
|
"""Raise CaptionLineLengthError if any line exceeds 32 characters."""
|
|
244
|
-
violations = [
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
+
violations = []
|
|
251
|
+
for precap in self.caption_stash._collection:
|
|
252
|
+
cap = precap.to_real_caption()
|
|
253
|
+
raw = "".join(cap.get_text_nodes())
|
|
254
|
+
# get_text() strips the caption, dropping any lines it opens with
|
|
255
|
+
skipped = raw[: len(raw) - len(raw.lstrip())].count("\n")
|
|
256
|
+
for index, line in enumerate(cap.get_text().split("\n")):
|
|
257
|
+
if len(line) > 32:
|
|
258
|
+
description = self._describe_line_length(
|
|
259
|
+
line, (index + skipped) in precap.continued_rows
|
|
260
|
+
)
|
|
261
|
+
violations.append(f"around {cap.format_start()} - {description}")
|
|
250
262
|
if violations:
|
|
251
263
|
raise CaptionLineLengthError(
|
|
252
264
|
"32 character limit for caption cue in scc file.\n"
|
|
253
265
|
"Lines longer than 32:\n" + "\n".join(violations)
|
|
254
266
|
)
|
|
255
267
|
|
|
268
|
+
@staticmethod
|
|
269
|
+
def _describe_line_length(line, continues_row=False):
|
|
270
|
+
"""Describe a line too long for the row, separating the text from the
|
|
271
|
+
indentation that places it, which can be what pushes it off the row,
|
|
272
|
+
and naming a missing PAC or row change when that is what joined it.
|
|
273
|
+
"""
|
|
274
|
+
text = line.lstrip("\xa0")
|
|
275
|
+
indent = len(line) - len(text)
|
|
276
|
+
if not indent:
|
|
277
|
+
description = f"{line} - Length {len(line)}"
|
|
278
|
+
else:
|
|
279
|
+
description = (
|
|
280
|
+
f"{text} - Length {len(text)} plus a {indent}-column indent "
|
|
281
|
+
f"= {len(line)}"
|
|
282
|
+
)
|
|
283
|
+
if continues_row:
|
|
284
|
+
description += ROW_CONTINUATION_SUFFIX
|
|
285
|
+
return description
|
|
286
|
+
|
|
256
287
|
@staticmethod
|
|
257
288
|
def _fix_last_captions_without_ending(caption_list):
|
|
258
289
|
"""Set end = start + 4s for trailing captions that were never ended."""
|
|
@@ -297,6 +328,7 @@ class SCCReader(BaseReader):
|
|
|
297
328
|
parts = r.findall(line.lower())
|
|
298
329
|
|
|
299
330
|
self.time_translator.start_at(parts[0][0])
|
|
331
|
+
self.position_tracker.start_burst()
|
|
300
332
|
word_list = parts[0][2].split(" ")
|
|
301
333
|
|
|
302
334
|
for idx, word in enumerate(word_list):
|
|
@@ -364,7 +396,11 @@ class SCCReader(BaseReader):
|
|
|
364
396
|
if self._is_doubled_type(word) and word == self.last_command:
|
|
365
397
|
if word in CUE_STARTING_COMMAND:
|
|
366
398
|
self.double_starter = True
|
|
367
|
-
|
|
399
|
+
# A doubled PAC stays on record so that a Tab Offset following it is
|
|
400
|
+
# still recognised as belonging to a PAC and applied; clearing it
|
|
401
|
+
# would leave the offset looking like a stray duplicate and drop the
|
|
402
|
+
# indentation it carries.
|
|
403
|
+
self.last_command = word if self._is_pac_command(word) else ""
|
|
368
404
|
return True
|
|
369
405
|
|
|
370
406
|
if self._is_pac_command(word) and word in self.last_command:
|