pycaption 2.2.28__tar.gz → 2.3.0__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 (76) hide show
  1. {pycaption-2.2.28/pycaption.egg-info → pycaption-2.3.0}/PKG-INFO +2 -2
  2. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/__init__.py +17 -4
  3. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/base.py +136 -45
  4. pycaption-2.3.0/pycaption/dfxp/__init__.py +22 -0
  5. pycaption-2.3.0/pycaption/dfxp/constants.py +96 -0
  6. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/dfxp/extras.py +53 -23
  7. pycaption-2.3.0/pycaption/dfxp/reader.py +876 -0
  8. pycaption-2.3.0/pycaption/dfxp/writer.py +500 -0
  9. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/exceptions.py +12 -14
  10. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/geometry.py +39 -95
  11. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/microdvd.py +26 -0
  12. pycaption-2.3.0/pycaption/sami/__init__.py +9 -0
  13. pycaption-2.3.0/pycaption/sami/constants.py +29 -0
  14. pycaption-2.3.0/pycaption/sami/parser.py +185 -0
  15. pycaption-2.3.0/pycaption/sami/reader.py +320 -0
  16. pycaption-2.3.0/pycaption/sami/writer.py +279 -0
  17. pycaption-2.3.0/pycaption/scc/__init__.py +6 -0
  18. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/scc/constants.py +1395 -967
  19. pycaption-2.2.28/pycaption/scc/__init__.py → pycaption-2.3.0/pycaption/scc/reader.py +167 -344
  20. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/scc/specialized_collections.py +57 -4
  21. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/scc/state_machines.py +6 -0
  22. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/scc/translator.py +2 -0
  23. pycaption-2.3.0/pycaption/scc/writer.py +597 -0
  24. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/srt.py +32 -3
  25. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/transcript.py +24 -4
  26. pycaption-2.3.0/pycaption/utils.py +13 -0
  27. pycaption-2.3.0/pycaption/webvtt/__init__.py +12 -0
  28. pycaption-2.3.0/pycaption/webvtt/constants.py +113 -0
  29. pycaption-2.3.0/pycaption/webvtt/reader.py +1113 -0
  30. pycaption-2.3.0/pycaption/webvtt/writer.py +618 -0
  31. {pycaption-2.2.28 → pycaption-2.3.0/pycaption.egg-info}/PKG-INFO +2 -2
  32. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption.egg-info/SOURCES.txt +15 -3
  33. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption.egg-info/requires.txt +1 -1
  34. pycaption-2.3.0/setup.cfg +14 -0
  35. {pycaption-2.2.28 → pycaption-2.3.0}/setup.py +2 -2
  36. {pycaption-2.2.28 → pycaption-2.3.0}/tests/conftest.py +3 -0
  37. {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/dfxp.py +81 -9
  38. {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/sami.py +6 -3
  39. {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/webvtt.py +48 -48
  40. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_dfxp.py +163 -5
  41. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_dfxp_conversion.py +2 -3
  42. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_dfxp_extras.py +1 -1
  43. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_sami_conversion.py +94 -4
  44. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_scc_writer.py +290 -1
  45. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_webvtt.py +291 -4
  46. pycaption-2.3.0/tests/test_webvtt_conversion.py +669 -0
  47. pycaption-2.2.28/pycaption/dfxp/__init__.py +0 -2
  48. pycaption-2.2.28/pycaption/dfxp/base.py +0 -1395
  49. pycaption-2.2.28/pycaption/sami.py +0 -809
  50. pycaption-2.2.28/pycaption/utils.py +0 -10
  51. pycaption-2.2.28/pycaption/webvtt.py +0 -1130
  52. pycaption-2.2.28/setup.cfg +0 -4
  53. pycaption-2.2.28/tests/test_webvtt_conversion.py +0 -331
  54. {pycaption-2.2.28 → pycaption-2.3.0}/LICENSE +0 -0
  55. {pycaption-2.2.28 → pycaption-2.3.0}/MANIFEST.in +0 -0
  56. {pycaption-2.2.28 → pycaption-2.3.0}/README.rst +0 -0
  57. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption.egg-info/dependency_links.txt +0 -0
  58. {pycaption-2.2.28 → pycaption-2.3.0}/pycaption.egg-info/top_level.txt +0 -0
  59. {pycaption-2.2.28 → pycaption-2.3.0}/tests/__init__.py +0 -0
  60. {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/__init__.py +0 -0
  61. {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/microdvd.py +0 -0
  62. {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/scc.py +0 -0
  63. {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/srt.py +0 -0
  64. {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/translated_scc.py +0 -0
  65. {pycaption-2.2.28 → pycaption-2.3.0}/tests/mixins.py +0 -0
  66. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_base.py +0 -0
  67. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_functions.py +0 -0
  68. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_geometry.py +0 -0
  69. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_microdvd.py +0 -0
  70. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_microdvd_conversion.py +0 -0
  71. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_sami.py +0 -0
  72. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_scc.py +0 -0
  73. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_scc_conversion.py +0 -0
  74. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_scc_translator.py +0 -0
  75. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_srt.py +0 -0
  76. {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_srt_conversion.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycaption
3
- Version: 2.2.28
3
+ Version: 2.3.0
4
4
  Summary: Closed caption converter
5
5
  Author: Joe Norton
6
6
  Author-email: joey@nortoncrew.com
@@ -27,7 +27,7 @@ Provides-Extra: dev
27
27
  Requires-Dist: pytest; extra == "dev"
28
28
  Requires-Dist: pytest-lazy-fixture; extra == "dev"
29
29
  Provides-Extra: transcript
30
- Requires-Dist: nltk==3.9.4; extra == "transcript"
30
+ Requires-Dist: nltk==3.10.0; extra == "transcript"
31
31
  Dynamic: author
32
32
  Dynamic: author-email
33
33
  Dynamic: classifier
@@ -1,10 +1,17 @@
1
+ """pycaption — closed caption reading/writing library.
2
+
3
+ Reads captions from DFXP/TTML, SRT, SAMI, SCC, WebVTT, and MicroDVD into
4
+ a common intermediate representation (CaptionSet), and writes them back
5
+ to any supported format.
6
+ """
7
+
1
8
  from .base import Caption, CaptionConverter, CaptionList, CaptionNode, CaptionSet
2
9
  from .dfxp import DFXPReader, DFXPWriter
3
10
  from .exceptions import (
4
- CaptionLineLengthError,
5
11
  CaptionReadError,
6
12
  CaptionReadNoCaptions,
7
13
  CaptionReadSyntaxError,
14
+ CaptionReadWarning,
8
15
  )
9
16
  from .microdvd import MicroDVDReader, MicroDVDWriter
10
17
  from .sami import SAMIReader, SAMIWriter
@@ -32,6 +39,7 @@ __all__ = [
32
39
  "CaptionReadError",
33
40
  "CaptionReadNoCaptions",
34
41
  "CaptionReadSyntaxError",
42
+ "CaptionReadWarning",
35
43
  "detect_format",
36
44
  "CaptionNode",
37
45
  "Caption",
@@ -51,10 +59,15 @@ SUPPORTED_READERS = (
51
59
 
52
60
 
53
61
  def detect_format(caps):
54
- """
55
- Detect the format of the provided caption string.
62
+ """Detect the caption format of the provided string.
63
+
64
+ Tries each reader's ``detect()`` method in order and returns the
65
+ first matching reader class, or None if no format matches.
56
66
 
57
- :returns: the reader class for the detected format.
67
+ :param caps: Raw caption file content.
68
+ :returns: The reader class for the detected format, or None.
69
+ :rtype: type | None
70
+ :raises CaptionReadNoCaptions: if caps is empty.
58
71
  """
59
72
  if not len(caps):
60
73
  raise CaptionReadNoCaptions("Empty caption file")
@@ -1,5 +1,11 @@
1
+ """Core data model for pycaption.
2
+
3
+ Defines the intermediate representation used by all readers and writers:
4
+ CaptionSet -> CaptionList -> Caption -> CaptionNode. Also provides the
5
+ CaptionConverter orchestrator and base classes for readers/writers.
6
+ """
7
+
1
8
  import os
2
- from collections import defaultdict
3
9
  from datetime import timedelta
4
10
  from numbers import Number
5
11
 
@@ -9,20 +15,26 @@ from .exceptions import CaptionReadError, CaptionReadTimingError
9
15
  DEFAULT_LANGUAGE_CODE = os.getenv("PYCAPTION_DEFAULT_LANG", "und")
10
16
 
11
17
 
12
- def force_byte_string(content):
13
- try:
14
- return content.encode("UTF-8")
15
- except UnicodeEncodeError:
16
- raise RuntimeError("Invalid content encoding")
17
- except UnicodeDecodeError:
18
- return content
18
+ class CaptionConverter:
19
+ """High-level orchestrator: read content with a reader, write with a writer.
19
20
 
21
+ Usage::
22
+
23
+ converter = CaptionConverter()
24
+ converter.read(srt_content, SRTReader())
25
+ output = converter.write(WebVTTWriter())
26
+ """
20
27
 
21
- class CaptionConverter:
22
28
  def __init__(self, captions=None):
23
29
  self.captions = captions if captions else []
24
30
 
25
31
  def read(self, content, caption_reader):
32
+ """Parse caption content using the given reader.
33
+
34
+ :param content: Raw caption file content (string).
35
+ :param caption_reader: A BaseReader subclass instance.
36
+ :returns: self (for chaining).
37
+ """
26
38
  try:
27
39
  self.captions = caption_reader.read(content)
28
40
  except AttributeError as e:
@@ -30,6 +42,12 @@ class CaptionConverter:
30
42
  return self
31
43
 
32
44
  def write(self, caption_writer):
45
+ """Serialize the stored CaptionSet using the given writer.
46
+
47
+ :param caption_writer: A BaseWriter subclass instance.
48
+ :returns: The serialized caption string.
49
+ :rtype: str
50
+ """
33
51
  try:
34
52
  return caption_writer.write(self.captions)
35
53
  except AttributeError as e:
@@ -37,20 +55,31 @@ class CaptionConverter:
37
55
 
38
56
 
39
57
  class BaseReader:
58
+ """Abstract base class for caption format readers."""
59
+
40
60
  def __init__(self, *args, **kwargs):
41
61
  pass
42
62
 
43
63
  def detect(self, content):
44
- if content:
45
- return True
46
- else:
47
- return False
64
+ """Return True if content appears to be in this reader's format.
65
+
66
+ :param content: Raw caption file content.
67
+ :rtype: bool
68
+ """
69
+ return bool(content)
48
70
 
49
71
  def read(self, content):
72
+ """Parse content into a CaptionSet.
73
+
74
+ :param content: Raw caption file content.
75
+ :rtype: CaptionSet
76
+ """
50
77
  return CaptionSet({DEFAULT_LANGUAGE_CODE: []})
51
78
 
52
79
 
53
80
  class BaseWriter:
81
+ """Abstract base class for caption format writers."""
82
+
54
83
  def __init__(
55
84
  self, relativize=True, video_width=None, video_height=None, fit_to_screen=True
56
85
  ):
@@ -79,6 +108,11 @@ class BaseWriter:
79
108
  self.fit_to_screen = fit_to_screen
80
109
 
81
110
  def _relativize_and_fit_to_screen(self, layout_info):
111
+ """Apply relativization and fit-to-screen adjustments to a Layout.
112
+
113
+ :param layout_info: A Layout instance (or None).
114
+ :rtype: Layout | None
115
+ """
82
116
  if layout_info:
83
117
  if self.relativize:
84
118
  # Transform absolute values (e.g. px) into percentages
@@ -91,12 +125,12 @@ class BaseWriter:
91
125
  return layout_info
92
126
 
93
127
  def write(self, content):
94
- return content
95
-
128
+ """Serialize a CaptionSet. Subclasses override this.
96
129
 
97
- class Style:
98
- def __init__(self):
99
- pass
130
+ :type content: CaptionSet
131
+ :rtype: str
132
+ """
133
+ return content
100
134
 
101
135
 
102
136
  class CaptionNode:
@@ -146,6 +180,7 @@ class CaptionNode:
146
180
 
147
181
  @staticmethod
148
182
  def create_text(text, layout_info=None, position=None):
183
+ """Create a TEXT node with the given content string."""
149
184
  return CaptionNode(
150
185
  type_=CaptionNode.TEXT,
151
186
  layout_info=layout_info,
@@ -155,6 +190,7 @@ class CaptionNode:
155
190
 
156
191
  @staticmethod
157
192
  def create_style(start, content, layout_info=None):
193
+ """Create a STYLE node (start=True opens, start=False closes)."""
158
194
  return CaptionNode(
159
195
  type_=CaptionNode.STYLE,
160
196
  layout_info=layout_info,
@@ -164,6 +200,7 @@ class CaptionNode:
164
200
 
165
201
  @staticmethod
166
202
  def create_break(layout_info=None, content=None):
203
+ """Create a BREAK (line-break) node."""
167
204
  return CaptionNode(
168
205
  type_=CaptionNode.BREAK, layout_info=layout_info, content=content
169
206
  )
@@ -175,7 +212,7 @@ class Caption:
175
212
  for its display.
176
213
  """
177
214
 
178
- def __init__(self, start, end, nodes, style={}, layout_info=None):
215
+ def __init__(self, start, end, nodes, style=None, layout_info=None):
179
216
  """
180
217
  Initialize the Caption object
181
218
  :param start: The start time in microseconds
@@ -192,35 +229,39 @@ class Caption:
192
229
  """
193
230
  if not isinstance(start, Number):
194
231
  raise CaptionReadTimingError(
195
- "Captions must be initialized with a" " valid start time"
232
+ "Captions must be initialized with a valid start time"
196
233
  )
197
234
  if not isinstance(end, Number):
198
235
  raise CaptionReadTimingError(
199
- "Captions must be initialized with a" " valid end time"
236
+ "Captions must be initialized with a valid end time"
200
237
  )
201
238
  if not nodes:
202
239
  raise CaptionReadError("Node list cannot be empty")
203
240
  self.start = start
204
241
  self.end = end
205
242
  self.nodes = nodes
206
- self.style = style
243
+ self.style = style or {}
207
244
  self.layout_info = layout_info
208
245
 
209
246
  def is_empty(self):
210
- return len(self.nodes) == 0
247
+ """Return True if this caption has no nodes."""
248
+ return not self.nodes
211
249
 
212
250
  def format_start(self, msec_separator=None):
213
- """
214
- Format the start time value in milliseconds into a string
215
- value suitable for some of the supported output formats (ex.
216
- SRT, DFXP).
251
+ """Format start time as HH:MM:SS.mmm string.
252
+
253
+ :param msec_separator: Character between seconds and milliseconds
254
+ (default '.').
255
+ :rtype: str
217
256
  """
218
257
  return self._format_timestamp(self.start, msec_separator)
219
258
 
220
259
  def format_end(self, msec_separator=None):
221
- """
222
- Format the end time value in milliseconds into a string value suitable
223
- for some of the supported output formats (ex. SRT, DFXP).
260
+ """Format end time as HH:MM:SS.mmm string.
261
+
262
+ :param msec_separator: Character between seconds and milliseconds
263
+ (default '.').
264
+ :rtype: str
224
265
  """
225
266
  return self._format_timestamp(self.end, msec_separator)
226
267
 
@@ -228,24 +269,28 @@ class Caption:
228
269
  return repr(f"{self.format_start()} --> {self.format_end()}\n{self.get_text()}")
229
270
 
230
271
  def get_text_nodes(self):
231
- """
232
- Get the text of the caption.
233
- """
272
+ """Return list of text content strings (with '\\n' for breaks).
234
273
 
235
- def get_text_for_node(node):
274
+ :rtype: list[str]
275
+ """
276
+ result = []
277
+ for node in self.nodes:
236
278
  if node.type_ == CaptionNode.TEXT:
237
- return node.content
238
- if node.type_ == CaptionNode.BREAK:
239
- return "\n"
240
- return ""
241
-
242
- return [get_text_for_node(node) for node in self.nodes]
279
+ result.append(node.content)
280
+ elif node.type_ == CaptionNode.BREAK:
281
+ result.append("\n")
282
+ return result
243
283
 
244
284
  def get_text(self):
285
+ """Return the plain text content of this caption (no markup).
286
+
287
+ :rtype: str
288
+ """
245
289
  text_nodes = self.get_text_nodes()
246
290
  return "".join(text_nodes).strip()
247
291
 
248
292
  def _format_timestamp(self, microseconds, msec_separator=None):
293
+ """Convert microseconds to HH:MM:SS{sep}mmm string."""
249
294
  duration = timedelta(microseconds=microseconds)
250
295
  hours, rem = divmod(duration.seconds, 3600)
251
296
  minutes, seconds = divmod(rem, 60)
@@ -269,9 +314,6 @@ class CaptionList(list):
269
314
  args = [iterable] if iterable else []
270
315
  super().__init__(*args)
271
316
 
272
- def __getslice__(self, i, j):
273
- return CaptionList(list.__getslice__(self, i, j), layout_info=self.layout_info)
274
-
275
317
  def __getitem__(self, y):
276
318
  item = list.__getitem__(self, y)
277
319
  if isinstance(item, Caption):
@@ -306,23 +348,39 @@ class CaptionSet:
306
348
  by all the children.
307
349
  """
308
350
 
309
- def __init__(self, captions, styles={}, layout_info=None):
351
+ def __init__(self, captions, styles=None, layout_info=None, regions=None):
310
352
  """
311
353
  :param captions: A dictionary of the format {'language': CaptionList}
312
354
  :param styles: A dictionary with CSS-like styling rules
313
355
  :param Layout layout_info: A Layout object with the positioning info
356
+ :param regions: A dictionary mapping region id to raw settings dict
314
357
  """
315
358
  self._captions = captions
316
- self._styles = styles
359
+ self._styles = styles or {}
360
+ self._regions = regions or {}
317
361
  self.layout_info = layout_info
318
362
 
319
363
  def set_captions(self, lang, captions):
364
+ """Replace the caption list for a given language.
365
+
366
+ :param lang: Language code (e.g. 'en-US').
367
+ :param captions: A CaptionList instance.
368
+ """
320
369
  self._captions[lang] = captions
321
370
 
322
371
  def get_languages(self):
372
+ """Return list of language codes in this caption set.
373
+
374
+ :rtype: list[str]
375
+ """
323
376
  return list(self._captions.keys())
324
377
 
325
378
  def get_captions(self, lang):
379
+ """Return the CaptionList for the given language, or empty list.
380
+
381
+ :param lang: Language code.
382
+ :rtype: CaptionList | list
383
+ """
326
384
  return self._captions.get(lang, [])
327
385
 
328
386
  def add_style(self, selector, rules):
@@ -342,18 +400,51 @@ class CaptionSet:
342
400
  return self._styles.get(selector, {})
343
401
 
344
402
  def get_styles(self):
403
+ """Return all styles as sorted (selector, rules) pairs.
404
+
405
+ :rtype: list[tuple[str, dict]]
406
+ """
345
407
  return sorted(self._styles.items())
346
408
 
347
409
  def set_styles(self, styles):
410
+ """Replace all styles with the given dictionary.
411
+
412
+ :param styles: dict mapping selectors to rule dictionaries.
413
+ """
348
414
  self._styles = styles
349
415
 
416
+ def get_regions(self):
417
+ """Return raw region definitions for the writer to re-emit.
418
+
419
+ :rtype: dict[str, dict[str, str]]
420
+ """
421
+ return self._regions
422
+
423
+ def set_regions(self, regions):
424
+ """Replace all region definitions.
425
+
426
+ :param regions: dict mapping region id to settings dict.
427
+ """
428
+ self._regions = regions
429
+
350
430
  def is_empty(self):
431
+ """Return True if no language contains any captions."""
351
432
  return all([len(captions) == 0 for captions in list(self._captions.values())])
352
433
 
353
434
  def set_layout_info(self, lang, layout_info):
435
+ """Set the layout_info on the CaptionList for a given language.
436
+
437
+ :param lang: Language code.
438
+ :param layout_info: A Layout instance.
439
+ """
354
440
  self._captions[lang].layout_info = layout_info
355
441
 
356
442
  def get_layout_info(self, lang):
443
+ """Return the layout_info for a given language's CaptionList.
444
+
445
+ :param lang: Language code.
446
+ :rtype: Layout | None
447
+ """
357
448
  caption_list = self._captions.get(lang)
358
449
  if caption_list:
359
450
  return caption_list.layout_info
@@ -0,0 +1,22 @@
1
+ """DFXP/TTML caption format reader and writer package.
2
+
3
+ Provides DFXPReader for parsing DFXP/TTML files into CaptionSet objects,
4
+ DFXPWriter for serializing CaptionSet objects to DFXP/TTML, and legacy/
5
+ single-positioning writer variants.
6
+ """
7
+
8
+ from .constants import ( # noqa: F401
9
+ DFXP_ATTR_XML_ID,
10
+ DFXP_ATTR_XML_LANG,
11
+ DFXP_BASE_MARKUP,
12
+ DFXP_DEFAULT_LANGUAGE_CODE,
13
+ DFXP_DEFAULT_REGION,
14
+ DFXP_DEFAULT_REGION_ID,
15
+ DFXP_DEFAULT_STYLE,
16
+ DFXP_DEFAULT_STYLE_ID,
17
+ HORIZONTAL_ALIGNMENT_TO_DFXP,
18
+ VERTICAL_ALIGNMENT_TO_DFXP,
19
+ )
20
+ from .extras import LegacyDFXPWriter, SinglePositioningDFXPWriter # noqa: F401
21
+ from .reader import DFXPReader # noqa: F401
22
+ from .writer import DFXPWriter # noqa: F401
@@ -0,0 +1,96 @@
1
+ """Constants for the DFXP/TTML reader and writer.
2
+
3
+ Defines default styles, regions, time-expression patterns, alignment
4
+ mappings, and frame-rate defaults used throughout the DFXP package.
5
+ """
6
+
7
+ import re
8
+
9
+ from ..geometry import Alignment, HorizontalAlignmentEnum, Layout, VerticalAlignmentEnum
10
+
11
+ DFXP_BASE_MARKUP = """
12
+ <tt xmlns="http://www.w3.org/ns/ttml"
13
+ xmlns:tts="http://www.w3.org/ns/ttml#styling">
14
+ <head>
15
+ <styling/>
16
+ <layout/>
17
+ </head>
18
+ <body/>
19
+ </tt>
20
+ """
21
+
22
+ DFXP_DEFAULT_STYLE = {
23
+ "color": "white",
24
+ "font-family": "monospace",
25
+ "font-size": "1c",
26
+ }
27
+
28
+ DFXP_DEFAULT_REGION = Layout(
29
+ alignment=Alignment(HorizontalAlignmentEnum.START, VerticalAlignmentEnum.BOTTOM)
30
+ )
31
+
32
+ DFXP_DEFAULT_STYLE_ID = "default"
33
+ DFXP_DEFAULT_REGION_ID = "bottom"
34
+
35
+ CLOCK_TIME_PATTERN = (
36
+ r"(?P<clock_time>(?P<hours>\d+):(?P<minutes>\d{2}):(?P<seconds>\d{2})"
37
+ r"(:(?P<frames>\d{2})|\.(?P<sub_frames>\d+))?)"
38
+ )
39
+ OFFSET_TIME_PATTERN = (
40
+ r"(?P<offset_time>(?P<time_count>\d+(\.\d+)?)" r"(?P<metric>h|m|s|ms|f|t))"
41
+ )
42
+ TIME_EXPRESSION_PATTERN = re.compile(rf"^({CLOCK_TIME_PATTERN}|{OFFSET_TIME_PATTERN})$")
43
+
44
+ MICROSECONDS_PER_UNIT = {
45
+ "hours": 3600000000,
46
+ "minutes": 60000000,
47
+ "seconds": 1000000,
48
+ "milliseconds": 1000,
49
+ }
50
+
51
+ DFXP_DEFAULT_LANGUAGE_CODE = "en"
52
+
53
+ DFXP_ATTR_XML_LANG = "xml:lang"
54
+ DFXP_ATTR_XML_ID = "xml:id"
55
+
56
+ HORIZONTAL_ALIGNMENT_TO_DFXP = {
57
+ HorizontalAlignmentEnum.LEFT: "left",
58
+ HorizontalAlignmentEnum.CENTER: "center",
59
+ HorizontalAlignmentEnum.RIGHT: "right",
60
+ HorizontalAlignmentEnum.START: "start",
61
+ HorizontalAlignmentEnum.END: "end",
62
+ }
63
+
64
+ VERTICAL_ALIGNMENT_TO_DFXP = {
65
+ VerticalAlignmentEnum.TOP: "before",
66
+ VerticalAlignmentEnum.CENTER: "center",
67
+ VerticalAlignmentEnum.BOTTOM: "after",
68
+ }
69
+
70
+ DFXP_DEFAULT_FRAMERATE = 30
71
+ DFXP_DEFAULT_FRAMERATE_MULTIPLIER = "1 1"
72
+ DFXP_DEFAULT_SUBFRAMERATE = 1
73
+ DFXP_DEFAULT_TICKRATE = DFXP_DEFAULT_FRAMERATE * DFXP_DEFAULT_SUBFRAMERATE
74
+
75
+
76
+ def _create_external_alignment(alignment):
77
+ """Convert an Alignment object to a dict of DFXP attributes.
78
+
79
+ :type alignment: Alignment
80
+ :rtype: dict
81
+ """
82
+ result = {}
83
+ if not alignment:
84
+ return result
85
+ if not (alignment.horizontal or alignment.vertical):
86
+ return result
87
+
88
+ horizontal = HORIZONTAL_ALIGNMENT_TO_DFXP.get(alignment.horizontal)
89
+ if horizontal:
90
+ result["tts:textAlign"] = horizontal
91
+
92
+ vertical = VERTICAL_ALIGNMENT_TO_DFXP.get(alignment.vertical)
93
+ if vertical:
94
+ result["tts:displayAlign"] = vertical
95
+
96
+ return result