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.
- {pycaption-2.2.28/pycaption.egg-info → pycaption-2.3.0}/PKG-INFO +2 -2
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/__init__.py +17 -4
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/base.py +136 -45
- pycaption-2.3.0/pycaption/dfxp/__init__.py +22 -0
- pycaption-2.3.0/pycaption/dfxp/constants.py +96 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/dfxp/extras.py +53 -23
- pycaption-2.3.0/pycaption/dfxp/reader.py +876 -0
- pycaption-2.3.0/pycaption/dfxp/writer.py +500 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/exceptions.py +12 -14
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/geometry.py +39 -95
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/microdvd.py +26 -0
- pycaption-2.3.0/pycaption/sami/__init__.py +9 -0
- pycaption-2.3.0/pycaption/sami/constants.py +29 -0
- pycaption-2.3.0/pycaption/sami/parser.py +185 -0
- pycaption-2.3.0/pycaption/sami/reader.py +320 -0
- pycaption-2.3.0/pycaption/sami/writer.py +279 -0
- pycaption-2.3.0/pycaption/scc/__init__.py +6 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/scc/constants.py +1395 -967
- pycaption-2.2.28/pycaption/scc/__init__.py → pycaption-2.3.0/pycaption/scc/reader.py +167 -344
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/scc/specialized_collections.py +57 -4
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/scc/state_machines.py +6 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/scc/translator.py +2 -0
- pycaption-2.3.0/pycaption/scc/writer.py +597 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/srt.py +32 -3
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption/transcript.py +24 -4
- pycaption-2.3.0/pycaption/utils.py +13 -0
- pycaption-2.3.0/pycaption/webvtt/__init__.py +12 -0
- pycaption-2.3.0/pycaption/webvtt/constants.py +113 -0
- pycaption-2.3.0/pycaption/webvtt/reader.py +1113 -0
- pycaption-2.3.0/pycaption/webvtt/writer.py +618 -0
- {pycaption-2.2.28 → pycaption-2.3.0/pycaption.egg-info}/PKG-INFO +2 -2
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption.egg-info/SOURCES.txt +15 -3
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption.egg-info/requires.txt +1 -1
- pycaption-2.3.0/setup.cfg +14 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/setup.py +2 -2
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/conftest.py +3 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/dfxp.py +81 -9
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/sami.py +6 -3
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/webvtt.py +48 -48
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_dfxp.py +163 -5
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_dfxp_conversion.py +2 -3
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_dfxp_extras.py +1 -1
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_sami_conversion.py +94 -4
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_scc_writer.py +290 -1
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_webvtt.py +291 -4
- pycaption-2.3.0/tests/test_webvtt_conversion.py +669 -0
- pycaption-2.2.28/pycaption/dfxp/__init__.py +0 -2
- pycaption-2.2.28/pycaption/dfxp/base.py +0 -1395
- pycaption-2.2.28/pycaption/sami.py +0 -809
- pycaption-2.2.28/pycaption/utils.py +0 -10
- pycaption-2.2.28/pycaption/webvtt.py +0 -1130
- pycaption-2.2.28/setup.cfg +0 -4
- pycaption-2.2.28/tests/test_webvtt_conversion.py +0 -331
- {pycaption-2.2.28 → pycaption-2.3.0}/LICENSE +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/MANIFEST.in +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/README.rst +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption.egg-info/dependency_links.txt +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/pycaption.egg-info/top_level.txt +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/__init__.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/__init__.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/microdvd.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/scc.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/srt.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/fixtures/translated_scc.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/mixins.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_base.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_functions.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_geometry.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_microdvd.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_microdvd_conversion.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_sami.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_scc.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_scc_conversion.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_scc_translator.py +0 -0
- {pycaption-2.2.28 → pycaption-2.3.0}/tests/test_srt.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
95
|
-
|
|
128
|
+
"""Serialize a CaptionSet. Subclasses override this.
|
|
96
129
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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=
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
274
|
+
:rtype: list[str]
|
|
275
|
+
"""
|
|
276
|
+
result = []
|
|
277
|
+
for node in self.nodes:
|
|
236
278
|
if node.type_ == CaptionNode.TEXT:
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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=
|
|
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
|