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.
Files changed (74) hide show
  1. {pycaption-2.3.10/pycaption.egg-info → pycaption-2.3.13}/PKG-INFO +1 -1
  2. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/__init__.py +1 -0
  3. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/base.py +3 -2
  4. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/dfxp/constants.py +1 -0
  5. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/dfxp/extras.py +4 -2
  6. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/dfxp/reader.py +8 -7
  7. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/dfxp/writer.py +9 -4
  8. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/geometry.py +3 -0
  9. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/microdvd.py +1 -0
  10. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/sami/constants.py +1 -0
  11. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/sami/reader.py +9 -2
  12. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/sami/writer.py +3 -1
  13. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/scc/__init__.py +1 -0
  14. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/scc/constants.py +5 -0
  15. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/scc/reader.py +43 -7
  16. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/scc/specialized_collections.py +516 -57
  17. pycaption-2.3.13/pycaption/scc/state_machines.py +450 -0
  18. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/scc/writer.py +37 -13
  19. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/srt.py +20 -4
  20. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/transcript.py +7 -1
  21. pycaption-2.3.13/pycaption/utils.py +26 -0
  22. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/webvtt/__init__.py +1 -0
  23. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/webvtt/constants.py +1 -0
  24. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/webvtt/reader.py +1 -1
  25. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/webvtt/writer.py +3 -1
  26. {pycaption-2.3.10 → pycaption-2.3.13/pycaption.egg-info}/PKG-INFO +1 -1
  27. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption.egg-info/SOURCES.txt +0 -1
  28. pycaption-2.3.13/setup.cfg +4 -0
  29. {pycaption-2.3.10 → pycaption-2.3.13}/setup.py +3 -2
  30. {pycaption-2.3.10 → pycaption-2.3.13}/tests/conftest.py +59 -7
  31. {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/dfxp.py +18 -0
  32. {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/sami.py +35 -0
  33. {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/scc.py +826 -7
  34. {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/webvtt.py +1 -1
  35. {pycaption-2.3.10 → pycaption-2.3.13}/tests/mixins.py +1 -0
  36. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_base.py +72 -0
  37. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_bytes_input.py +0 -2
  38. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_dfxp.py +46 -0
  39. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_dfxp_conversion.py +4 -3
  40. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_dfxp_extras.py +20 -0
  41. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_double_encoding.py +1 -3
  42. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_sami.py +65 -9
  43. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_sami_conversion.py +4 -7
  44. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_scc.py +893 -45
  45. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_scc_conversion.py +86 -24
  46. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_scc_translator.py +1 -3
  47. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_scc_writer.py +107 -49
  48. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_srt.py +66 -1
  49. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_webvtt.py +12 -32
  50. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_webvtt_conversion.py +21 -61
  51. pycaption-2.3.10/pycaption/scc/state_machines.py +0 -207
  52. pycaption-2.3.10/pycaption/utils.py +0 -13
  53. pycaption-2.3.10/setup.cfg +0 -14
  54. {pycaption-2.3.10 → pycaption-2.3.13}/LICENSE +0 -0
  55. {pycaption-2.3.10 → pycaption-2.3.13}/MANIFEST.in +0 -0
  56. {pycaption-2.3.10 → pycaption-2.3.13}/README.rst +0 -0
  57. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/dfxp/__init__.py +0 -0
  58. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/exceptions.py +0 -0
  59. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/sami/__init__.py +0 -0
  60. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/sami/parser.py +0 -0
  61. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption/scc/translator.py +0 -0
  62. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption.egg-info/dependency_links.txt +0 -0
  63. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption.egg-info/requires.txt +0 -0
  64. {pycaption-2.3.10 → pycaption-2.3.13}/pycaption.egg-info/top_level.txt +0 -0
  65. {pycaption-2.3.10 → pycaption-2.3.13}/tests/__init__.py +0 -0
  66. {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/__init__.py +0 -0
  67. {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/microdvd.py +0 -0
  68. {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/srt.py +0 -0
  69. {pycaption-2.3.10 → pycaption-2.3.13}/tests/fixtures/translated_scc.py +0 -0
  70. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_functions.py +0 -0
  71. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_geometry.py +0 -0
  72. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_microdvd.py +0 -0
  73. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_microdvd_conversion.py +0 -0
  74. {pycaption-2.3.10 → pycaption-2.3.13}/tests/test_srt_conversion.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycaption
3
- Version: 2.3.10
3
+ Version: 2.3.13
4
4
  Summary: Closed caption converter
5
5
  Author: Joe Norton
6
6
  Author-email: joey@nortoncrew.com
@@ -21,6 +21,7 @@ from .srt import SRTReader, SRTWriter
21
21
  from .transcript import TranscriptWriter
22
22
  from .webvtt import WebVTTReader, WebVTTWriter
23
23
 
24
+
24
25
  __all__ = [
25
26
  "CaptionConverter",
26
27
  "DFXPReader",
@@ -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
@@ -8,6 +8,7 @@ import re
8
8
 
9
9
  from ..geometry import Alignment, HorizontalAlignmentEnum, Layout, VerticalAlignmentEnum
10
10
 
11
+
11
12
  DFXP_BASE_MARKUP = """
12
13
  <tt xmlns="http://www.w3.org/ns/ttml"
13
14
  xmlns:tts="http://www.w3.org/ns/ttml#styling">
@@ -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
- _LEADING_WHITESPACE_RE = re.compile("^(?:[\n\r]+\\s*)?(.+)")
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, styles=style_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 = self._recreate_text(caption, dfxp, caption_set, lang)
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
 
@@ -5,6 +5,7 @@ A special line {0}{0}fps_value sets the framerate (default 25 fps).
5
5
  """
6
6
 
7
7
  import re
8
+
8
9
  from copy import deepcopy
9
10
 
10
11
  from .base import (
@@ -10,6 +10,7 @@ from cssutils import log
10
10
 
11
11
  from ..geometry import HorizontalAlignmentEnum
12
12
 
13
+
13
14
  log.setLevel(FATAL)
14
15
 
15
16
  SAMI_BASE_MARKUP = """
@@ -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
- tag_text = tag_text.lstrip()
206
- tag_text = tag_text.rstrip("\n\r")
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
 
@@ -3,4 +3,5 @@
3
3
  from .reader import SCCReader
4
4
  from .writer import SCC_TOKENS_PER_CAPTION_MAX, SCCWriter
5
5
 
6
+
6
7
  __all__ = ["SCCReader", "SCCWriter", "SCC_TOKENS_PER_CAPTION_MAX"]
@@ -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
- f"around {cap.format_start()} - {line} - Length {len(line)}"
246
- for cap in (c.to_real_caption() for c in self.caption_stash._collection)
247
- for line in cap.get_text().split("\n")
248
- if len(line) > 32
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
- self.last_command = ""
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: