kernpy 0.0.2__py3-none-any.whl → 1.0.0__py3-none-any.whl
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.
- kernpy/__init__.py +215 -0
- kernpy/__main__.py +217 -0
- kernpy/core/__init__.py +119 -0
- kernpy/core/_io.py +48 -0
- kernpy/core/base_antlr_importer.py +61 -0
- kernpy/core/base_antlr_spine_parser_listener.py +196 -0
- kernpy/core/basic_spine_importer.py +43 -0
- kernpy/core/document.py +965 -0
- kernpy/core/dyn_importer.py +30 -0
- kernpy/core/dynam_spine_importer.py +42 -0
- kernpy/core/error_listener.py +51 -0
- kernpy/core/exporter.py +535 -0
- kernpy/core/fing_spine_importer.py +42 -0
- kernpy/core/generated/kernSpineLexer.interp +444 -0
- kernpy/core/generated/kernSpineLexer.py +535 -0
- kernpy/core/generated/kernSpineLexer.tokens +236 -0
- kernpy/core/generated/kernSpineParser.interp +425 -0
- kernpy/core/generated/kernSpineParser.py +9954 -0
- kernpy/core/generated/kernSpineParser.tokens +236 -0
- kernpy/core/generated/kernSpineParserListener.py +1200 -0
- kernpy/core/generated/kernSpineParserVisitor.py +673 -0
- kernpy/core/generic.py +426 -0
- kernpy/core/gkern.py +526 -0
- kernpy/core/graphviz_exporter.py +89 -0
- kernpy/core/harm_spine_importer.py +41 -0
- kernpy/core/import_humdrum_old.py +853 -0
- kernpy/core/importer.py +285 -0
- kernpy/core/importer_factory.py +43 -0
- kernpy/core/kern_spine_importer.py +73 -0
- kernpy/core/mens_spine_importer.py +23 -0
- kernpy/core/mhxm_spine_importer.py +44 -0
- kernpy/core/pitch_models.py +338 -0
- kernpy/core/root_spine_importer.py +58 -0
- kernpy/core/spine_importer.py +45 -0
- kernpy/core/text_spine_importer.py +43 -0
- kernpy/core/tokenizers.py +239 -0
- kernpy/core/tokens.py +2011 -0
- kernpy/core/transposer.py +300 -0
- kernpy/io/__init__.py +14 -0
- kernpy/io/public.py +355 -0
- kernpy/polish_scores/__init__.py +13 -0
- kernpy/polish_scores/download_polish_dataset.py +357 -0
- kernpy/polish_scores/iiif.py +47 -0
- kernpy/test_grammar.sh +22 -0
- kernpy/util/__init__.py +14 -0
- kernpy/util/helpers.py +55 -0
- kernpy/util/store_cache.py +35 -0
- kernpy/visualize_analysis.sh +23 -0
- kernpy-1.0.0.dist-info/METADATA +501 -0
- kernpy-1.0.0.dist-info/RECORD +51 -0
- {kernpy-0.0.2.dist-info → kernpy-1.0.0.dist-info}/WHEEL +1 -2
- kernpy/example.py +0 -1
- kernpy-0.0.2.dist-info/LICENSE +0 -19
- kernpy-0.0.2.dist-info/METADATA +0 -19
- kernpy-0.0.2.dist-info/RECORD +0 -7
- kernpy-0.0.2.dist-info/top_level.txt +0 -1
kernpy/core/tokens.py
ADDED
@@ -0,0 +1,2011 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from collections.abc import Sequence
|
5
|
+
from enum import Enum, auto
|
6
|
+
import copy
|
7
|
+
from typing import List, Dict, Set, Union, Optional
|
8
|
+
from unittest import result
|
9
|
+
|
10
|
+
TOKEN_SEPARATOR = '@'
|
11
|
+
DECORATION_SEPARATOR = '·'
|
12
|
+
GRAPHIC_TOKEN_SEPARATOR = ':'
|
13
|
+
HEADERS = {"**mens", "**kern", "**text", "**harm", "**mxhm", "**root", "**dyn", "**dynam", "**fing"}
|
14
|
+
CORE_HEADERS = {"**kern", "**mens"}
|
15
|
+
SPINE_OPERATIONS = {"*-", "*+", "*^", "*v", "*x"}
|
16
|
+
TERMINATOR = "*-"
|
17
|
+
EMPTY_TOKEN = "*"
|
18
|
+
ERROR_TOKEN = "Z"
|
19
|
+
|
20
|
+
|
21
|
+
# We don't use inheritance here for all elements but enum, because we don't need any polymorphism mechanism, just a grouping one
|
22
|
+
# TODO Poner todos los tipos - p.ej. también comandos de layout - slurs, etc...
|
23
|
+
class TokenCategory(Enum):
|
24
|
+
"""
|
25
|
+
Options for the category of a token.
|
26
|
+
|
27
|
+
This is used to determine what kind of token should be exported.
|
28
|
+
|
29
|
+
The categories are sorted the specific order they are compared to sorthem. But hierarchical order must be defined in other data structures.
|
30
|
+
"""
|
31
|
+
STRUCTURAL = auto() # header, spine operations
|
32
|
+
HEADER = auto() # **kern, **mens, **text, **harm, **mxhm, **root, **dyn, **dynam, **fing
|
33
|
+
SPINE_OPERATION = auto()
|
34
|
+
CORE = auto() # notes, rests, chords, etc.
|
35
|
+
ERROR = auto()
|
36
|
+
NOTE_REST = auto()
|
37
|
+
NOTE = auto()
|
38
|
+
DURATION = auto()
|
39
|
+
PITCH = auto()
|
40
|
+
ALTERATION = auto()
|
41
|
+
DECORATION = auto()
|
42
|
+
REST = auto()
|
43
|
+
CHORD = auto()
|
44
|
+
EMPTY = auto() # placeholders, null interpretation
|
45
|
+
SIGNATURES = auto()
|
46
|
+
CLEF = auto()
|
47
|
+
TIME_SIGNATURE = auto()
|
48
|
+
METER_SYMBOL = auto()
|
49
|
+
KEY_SIGNATURE = auto()
|
50
|
+
KEY_TOKEN = auto()
|
51
|
+
ENGRAVED_SYMBOLS = auto()
|
52
|
+
OTHER_CONTEXTUAL = auto()
|
53
|
+
BARLINES = auto()
|
54
|
+
COMMENTS = auto()
|
55
|
+
FIELD_COMMENTS = auto()
|
56
|
+
LINE_COMMENTS = auto()
|
57
|
+
DYNAMICS = auto()
|
58
|
+
HARMONY = auto()
|
59
|
+
FINGERING = auto()
|
60
|
+
LYRICS = auto()
|
61
|
+
INSTRUMENTS = auto()
|
62
|
+
IMAGE_ANNOTATIONS = auto()
|
63
|
+
BOUNDING_BOXES = auto()
|
64
|
+
LINE_BREAK = auto()
|
65
|
+
OTHER = auto()
|
66
|
+
MHXM = auto()
|
67
|
+
ROOT = auto()
|
68
|
+
|
69
|
+
def __lt__(self, other):
|
70
|
+
"""
|
71
|
+
Compare two TokenCategory.
|
72
|
+
Args:
|
73
|
+
other (TokenCategory): The other category to compare.
|
74
|
+
|
75
|
+
Returns (bool): True if this category is lower than the other, False otherwise.
|
76
|
+
|
77
|
+
Examples:
|
78
|
+
>>> TokenCategory.STRUCTURAL < TokenCategory.CORE
|
79
|
+
True
|
80
|
+
>>> TokenCategory.STRUCTURAL < TokenCategory.STRUCTURAL
|
81
|
+
False
|
82
|
+
>>> TokenCategory.CORE < TokenCategory.STRUCTURAL
|
83
|
+
False
|
84
|
+
>>> sorted([TokenCategory.STRUCTURAL, TokenCategory.CORE])
|
85
|
+
[TokenCategory.STRUCTURAL, TokenCategory.CORE]
|
86
|
+
"""
|
87
|
+
if isinstance(other, TokenCategory):
|
88
|
+
return self.value < other.value
|
89
|
+
return NotImplemented
|
90
|
+
|
91
|
+
@classmethod
|
92
|
+
def all(cls) -> Set[TokenCategory]:
|
93
|
+
f"""
|
94
|
+
Get all categories in the hierarchy.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
Set[TokenCategory]: The set of all categories in the hierarchy.
|
98
|
+
|
99
|
+
Examples:
|
100
|
+
>>> import kernpy as kp
|
101
|
+
>>> kp.TokenCategory.all()
|
102
|
+
set([<TokenCategory.MHXM: 29>, <TokenCategory.COMMENTS: 19>, <TokenCategory.BARLINES: 18>, <TokenCategory.CORE: 2>, <TokenCategory.BOUNDING_BOXES: 27>, <TokenCategory.NOTE_REST: 3>, <TokenCategory.NOTE: 4>, <TokenCategory.ENGRAVED_SYMBOLS: 16>, <TokenCategory.SIGNATURES: 11>, <TokenCategory.REST: 8>, <TokenCategory.METER_SYMBOL: 14>, <TokenCategory.HARMONY: 23>, <TokenCategory.KEY_SIGNATURE: 15>, <TokenCategory.EMPTY: 10>, <TokenCategory.PITCH: 6>, <TokenCategory.LINE_COMMENTS: 21>, <TokenCategory.FINGERING: 24>, <TokenCategory.DECORATION: 7>, <TokenCategory.OTHER: 28>, <TokenCategory.INSTRUMENTS: 26>, <TokenCategory.STRUCTURAL: 1>, <TokenCategory.FIELD_COMMENTS: 20>, <TokenCategory.LYRICS: 25>, <TokenCategory.CLEF: 12>, <TokenCategory.DURATION: 5>, <TokenCategory.DYNAMICS: 22>, <TokenCategory.CHORD: 9>, <TokenCategory.TIME_SIGNATURE: 13>, <TokenCategory.OTHER_CONTEXTUAL: 17>])
|
103
|
+
"""
|
104
|
+
return set([t for t in TokenCategory])
|
105
|
+
|
106
|
+
@classmethod
|
107
|
+
def tree(cls):
|
108
|
+
"""
|
109
|
+
Return a string representation of the category hierarchy
|
110
|
+
Returns (str): The string representation of the category hierarchy
|
111
|
+
|
112
|
+
Examples:
|
113
|
+
>>> import kernpy as kp
|
114
|
+
>>> print(kp.TokenCategory.tree())
|
115
|
+
.
|
116
|
+
├── TokenCategory.STRUCTURAL
|
117
|
+
├── TokenCategory.CORE
|
118
|
+
│ ├── TokenCategory.NOTE_REST
|
119
|
+
│ │ ├── TokenCategory.DURATION
|
120
|
+
│ │ ├── TokenCategory.NOTE
|
121
|
+
│ │ │ ├── TokenCategory.PITCH
|
122
|
+
│ │ │ └── TokenCategory.DECORATION
|
123
|
+
│ │ └── TokenCategory.REST
|
124
|
+
│ ├── TokenCategory.CHORD
|
125
|
+
│ └── TokenCategory.EMPTY
|
126
|
+
├── TokenCategory.SIGNATURES
|
127
|
+
│ ├── TokenCategory.CLEF
|
128
|
+
│ ├── TokenCategory.TIME_SIGNATURE
|
129
|
+
│ ├── TokenCategory.METER_SYMBOL
|
130
|
+
│ └── TokenCategory.KEY_SIGNATURE
|
131
|
+
├── TokenCategory.ENGRAVED_SYMBOLS
|
132
|
+
├── TokenCategory.OTHER_CONTEXTUAL
|
133
|
+
├── TokenCategory.BARLINES
|
134
|
+
├── TokenCategory.COMMENTS
|
135
|
+
│ ├── TokenCategory.FIELD_COMMENTS
|
136
|
+
│ └── TokenCategory.LINE_COMMENTS
|
137
|
+
├── TokenCategory.DYNAMICS
|
138
|
+
├── TokenCategory.HARMONY
|
139
|
+
├── TokenCategory.FINGERING
|
140
|
+
├── TokenCategory.LYRICS
|
141
|
+
├── TokenCategory.INSTRUMENTS
|
142
|
+
├── TokenCategory.BOUNDING_BOXES
|
143
|
+
└── TokenCategory.OTHER
|
144
|
+
"""
|
145
|
+
return TokenCategoryHierarchyMapper.tree()
|
146
|
+
|
147
|
+
@classmethod
|
148
|
+
def is_child(cls, *, child: TokenCategory, parent: TokenCategory) -> bool:
|
149
|
+
"""
|
150
|
+
Check if the child category is a child of the parent category.
|
151
|
+
|
152
|
+
Args:
|
153
|
+
child (TokenCategory): The child category.
|
154
|
+
parent (TokenCategory): The parent category.
|
155
|
+
|
156
|
+
Returns (bool): True if the child category is a child of the parent category, False otherwise.
|
157
|
+
"""
|
158
|
+
return TokenCategoryHierarchyMapper.is_child(parent=parent, child=child)
|
159
|
+
|
160
|
+
@classmethod
|
161
|
+
def children(cls, target: TokenCategory) -> Set[TokenCategory]:
|
162
|
+
"""
|
163
|
+
Get the children of the target category.
|
164
|
+
|
165
|
+
Args:
|
166
|
+
target (TokenCategory): The target category.
|
167
|
+
|
168
|
+
Returns (List[TokenCategory]): The list of child categories of the target category.
|
169
|
+
"""
|
170
|
+
return TokenCategoryHierarchyMapper.children(parent=target)
|
171
|
+
|
172
|
+
@classmethod
|
173
|
+
def valid(cls, *, include: Optional[Set[TokenCategory]] = None, exclude: Optional[Set[TokenCategory]] = None) -> Set[TokenCategory]:
|
174
|
+
"""
|
175
|
+
Get the valid categories based on the include and exclude sets.
|
176
|
+
|
177
|
+
Args:
|
178
|
+
include (Optional[Set[TokenCategory]]): The set of categories to include. Defaults to None. \
|
179
|
+
If None, all categories are included.
|
180
|
+
exclude (Optional[Set[TokenCategory]]): The set of categories to exclude. Defaults to None. \
|
181
|
+
If None, no categories are excluded.
|
182
|
+
|
183
|
+
Returns (Set[TokenCategory]): The list of valid categories based on the include and exclude sets.
|
184
|
+
"""
|
185
|
+
return TokenCategoryHierarchyMapper.valid(include=include, exclude=exclude)
|
186
|
+
|
187
|
+
@classmethod
|
188
|
+
def leaves(cls, target: TokenCategory) -> Set[TokenCategory]:
|
189
|
+
"""
|
190
|
+
Get the leaves of the subtree of the target category.
|
191
|
+
|
192
|
+
Args:
|
193
|
+
target (TokenCategory): The target category.
|
194
|
+
|
195
|
+
Returns (List[TokenCategory]): The list of leaf categories of the target category.
|
196
|
+
"""
|
197
|
+
return TokenCategoryHierarchyMapper.leaves(target=target)
|
198
|
+
|
199
|
+
@classmethod
|
200
|
+
def nodes(cls, target: TokenCategory) -> Set[TokenCategory]:
|
201
|
+
"""
|
202
|
+
Get the nodes of the subtree of the target category.
|
203
|
+
|
204
|
+
Args:
|
205
|
+
target (TokenCategory): The target category.
|
206
|
+
|
207
|
+
Returns (List[TokenCategory]): The list of node categories of the target category.
|
208
|
+
"""
|
209
|
+
return TokenCategoryHierarchyMapper.nodes(parent=target)
|
210
|
+
|
211
|
+
@classmethod
|
212
|
+
def match(cls,
|
213
|
+
target: TokenCategory, *,
|
214
|
+
include: Optional[Set[TokenCategory]] = None,
|
215
|
+
exclude: Optional[Set[TokenCategory]] = None) -> bool:
|
216
|
+
"""
|
217
|
+
Check if the target category matches the include and exclude sets.
|
218
|
+
|
219
|
+
Args:
|
220
|
+
target (TokenCategory): The target category.
|
221
|
+
include (Optional[Set[TokenCategory]]): The set of categories to include. Defaults to None. \
|
222
|
+
If None, all categories are included.
|
223
|
+
exclude (Optional[Set[TokenCategory]]): The set of categories to exclude. Defaults to None. \
|
224
|
+
If None, no categories are excluded.
|
225
|
+
|
226
|
+
Returns (bool): True if the target category matches the include and exclude sets, False otherwise.
|
227
|
+
"""
|
228
|
+
return TokenCategoryHierarchyMapper.match(category=target, include=include, exclude=exclude)
|
229
|
+
|
230
|
+
def __str__(self):
|
231
|
+
"""
|
232
|
+
Get the string representation of the category.
|
233
|
+
|
234
|
+
Returns (str): The string representation of the category.
|
235
|
+
"""
|
236
|
+
return self.name
|
237
|
+
|
238
|
+
|
239
|
+
NON_CORE_CATEGORIES = {
|
240
|
+
TokenCategory.STRUCTURAL,
|
241
|
+
TokenCategory.SIGNATURES,
|
242
|
+
TokenCategory.EMPTY,
|
243
|
+
TokenCategory.IMAGE_ANNOTATIONS,
|
244
|
+
TokenCategory.BARLINES,
|
245
|
+
TokenCategory.COMMENTS,
|
246
|
+
}
|
247
|
+
|
248
|
+
BEKERN_CATEGORIES = {
|
249
|
+
TokenCategory.STRUCTURAL,
|
250
|
+
TokenCategory.CORE,
|
251
|
+
TokenCategory.SIGNATURES,
|
252
|
+
TokenCategory.BARLINES,
|
253
|
+
TokenCategory.IMAGE_ANNOTATIONS,
|
254
|
+
}
|
255
|
+
|
256
|
+
|
257
|
+
class TokenCategoryHierarchyMapper:
|
258
|
+
"""
|
259
|
+
Mapping of the TokenCategory hierarchy.
|
260
|
+
|
261
|
+
This class is used to define the hierarchy of the TokenCategory. Useful related methods are provided.
|
262
|
+
"""
|
263
|
+
"""
|
264
|
+
The hierarchy of the TokenCategory is a recursive dictionary that defines the parent-child relationships \
|
265
|
+
between the categories. It's a tree.
|
266
|
+
"""
|
267
|
+
_hierarchy_typing = Dict[TokenCategory, '_hierarchy_typing']
|
268
|
+
hierarchy: _hierarchy_typing = {
|
269
|
+
TokenCategory.STRUCTURAL: {
|
270
|
+
TokenCategory.HEADER: {}, # each leave must be an empty dictionary
|
271
|
+
TokenCategory.SPINE_OPERATION: {},
|
272
|
+
},
|
273
|
+
TokenCategory.CORE: {
|
274
|
+
TokenCategory.NOTE_REST: {
|
275
|
+
TokenCategory.DURATION: {},
|
276
|
+
TokenCategory.NOTE: {
|
277
|
+
TokenCategory.PITCH: {},
|
278
|
+
TokenCategory.DECORATION: {},
|
279
|
+
TokenCategory.ALTERATION: {},
|
280
|
+
},
|
281
|
+
TokenCategory.REST: {},
|
282
|
+
},
|
283
|
+
TokenCategory.CHORD: {},
|
284
|
+
TokenCategory.EMPTY: {},
|
285
|
+
TokenCategory.ERROR: {},
|
286
|
+
},
|
287
|
+
TokenCategory.SIGNATURES: {
|
288
|
+
TokenCategory.CLEF: {},
|
289
|
+
TokenCategory.TIME_SIGNATURE: {},
|
290
|
+
TokenCategory.METER_SYMBOL: {},
|
291
|
+
TokenCategory.KEY_SIGNATURE: {},
|
292
|
+
TokenCategory.KEY_TOKEN: {},
|
293
|
+
},
|
294
|
+
TokenCategory.ENGRAVED_SYMBOLS: {},
|
295
|
+
TokenCategory.OTHER_CONTEXTUAL: {},
|
296
|
+
TokenCategory.BARLINES: {},
|
297
|
+
TokenCategory.COMMENTS: {
|
298
|
+
TokenCategory.FIELD_COMMENTS: {},
|
299
|
+
TokenCategory.LINE_COMMENTS: {},
|
300
|
+
},
|
301
|
+
TokenCategory.DYNAMICS: {},
|
302
|
+
TokenCategory.HARMONY: {},
|
303
|
+
TokenCategory.FINGERING: {},
|
304
|
+
TokenCategory.LYRICS: {},
|
305
|
+
TokenCategory.INSTRUMENTS: {},
|
306
|
+
TokenCategory.IMAGE_ANNOTATIONS: {
|
307
|
+
TokenCategory.BOUNDING_BOXES: {},
|
308
|
+
TokenCategory.LINE_BREAK: {},
|
309
|
+
},
|
310
|
+
TokenCategory.OTHER: {},
|
311
|
+
TokenCategory.MHXM: {},
|
312
|
+
TokenCategory.ROOT: {},
|
313
|
+
}
|
314
|
+
|
315
|
+
@classmethod
|
316
|
+
def _is_child(cls, parent: TokenCategory, child: TokenCategory, *, tree: '_hierarchy_typing') -> bool:
|
317
|
+
"""
|
318
|
+
Recursively check if `child` is in the subtree of `parent`.
|
319
|
+
|
320
|
+
Args:
|
321
|
+
parent (TokenCategory): The parent category.
|
322
|
+
child (TokenCategory): The category to check.
|
323
|
+
tree (_hierarchy_typing): The subtree to check.
|
324
|
+
|
325
|
+
Returns:
|
326
|
+
bool: True if `child` is a descendant of `parent`, False otherwise.
|
327
|
+
"""
|
328
|
+
# Base case: the parent is empty.
|
329
|
+
if len(tree.keys()) == 0:
|
330
|
+
return False
|
331
|
+
|
332
|
+
# Recursive case: explore the direct children of the parent.
|
333
|
+
return any(
|
334
|
+
direct_child == child or cls._is_child(direct_child, child, tree=tree[parent])
|
335
|
+
for direct_child in tree.get(parent, {})
|
336
|
+
)
|
337
|
+
# Vectorized version of the following code:
|
338
|
+
#direct_children = tree.get(parent, dict())
|
339
|
+
#for direct_child in direct_children.keys():
|
340
|
+
# if direct_child == child or cls._is_child(direct_child, child, tree=tree[parent]):
|
341
|
+
# return True
|
342
|
+
|
343
|
+
@classmethod
|
344
|
+
def is_child(cls, parent: TokenCategory, child: TokenCategory) -> bool:
|
345
|
+
"""
|
346
|
+
Recursively check if `child` is in the subtree of `parent`. If `parent` is the same as `child`, return True.
|
347
|
+
|
348
|
+
Args:
|
349
|
+
parent (TokenCategory): The parent category.
|
350
|
+
child (TokenCategory): The category to check.
|
351
|
+
|
352
|
+
Returns:
|
353
|
+
bool: True if `child` is a descendant of `parent`, False otherwise.
|
354
|
+
"""
|
355
|
+
if parent == child:
|
356
|
+
return True
|
357
|
+
return cls._is_child(parent, child, tree=cls.hierarchy)
|
358
|
+
|
359
|
+
@classmethod
|
360
|
+
def children(cls, parent: TokenCategory) -> Set[TokenCategory]:
|
361
|
+
"""
|
362
|
+
Get the direct children of the parent category.
|
363
|
+
|
364
|
+
Args:
|
365
|
+
parent (TokenCategory): The parent category.
|
366
|
+
|
367
|
+
Returns:
|
368
|
+
Set[TokenCategory]: The list of children categories of the parent category.
|
369
|
+
"""
|
370
|
+
return set(cls.hierarchy.get(parent, {}).keys())
|
371
|
+
|
372
|
+
@classmethod
|
373
|
+
def _nodes(cls, tree: _hierarchy_typing) -> Set[TokenCategory]:
|
374
|
+
"""
|
375
|
+
Recursively get all nodes in the given hierarchy tree.
|
376
|
+
"""
|
377
|
+
nodes = set(tree.keys())
|
378
|
+
for child in tree.values():
|
379
|
+
nodes.update(cls._nodes(child))
|
380
|
+
return nodes
|
381
|
+
|
382
|
+
@classmethod
|
383
|
+
def _find_subtree(cls, tree: '_hierarchy_typing', parent: TokenCategory) -> Optional['_hierarchy_typing']:
|
384
|
+
"""
|
385
|
+
Recursively find the subtree for the given parent category.
|
386
|
+
"""
|
387
|
+
if parent in tree:
|
388
|
+
return tree[parent] # Return subtree if parent is found at this level
|
389
|
+
for child, sub_tree in tree.items():
|
390
|
+
result = cls._find_subtree(sub_tree, parent)
|
391
|
+
if result is not None:
|
392
|
+
return result
|
393
|
+
return None # Return None if parent is not found. It won't happer never
|
394
|
+
|
395
|
+
|
396
|
+
@classmethod
|
397
|
+
def nodes(cls, parent: TokenCategory) -> Set[TokenCategory]:
|
398
|
+
"""
|
399
|
+
Get the all nodes of the subtree of the parent category.
|
400
|
+
|
401
|
+
Args:
|
402
|
+
parent (TokenCategory): The parent category.
|
403
|
+
|
404
|
+
Returns:
|
405
|
+
List[TokenCategory]: The list of nodes of the subtree of the parent category.
|
406
|
+
"""
|
407
|
+
subtree = cls._find_subtree(cls.hierarchy, parent)
|
408
|
+
return cls._nodes(subtree) if subtree is not None else set()
|
409
|
+
|
410
|
+
@classmethod
|
411
|
+
def valid(cls,
|
412
|
+
include: Optional[Set[TokenCategory]] = None,
|
413
|
+
exclude: Optional[Set[TokenCategory]] = None) -> Set[TokenCategory]:
|
414
|
+
"""
|
415
|
+
Get the valid categories based on the include and exclude sets.
|
416
|
+
|
417
|
+
Args:
|
418
|
+
include (Optional[Set[TokenCategory]]): The set of categories to include. Defaults to None. \
|
419
|
+
If None, all categories are included.
|
420
|
+
exclude (Optional[Set[TokenCategory]]): The set of categories to exclude. Defaults to None. \
|
421
|
+
If None, no categories are excluded.
|
422
|
+
|
423
|
+
Returns (Set[TokenCategory]): The list of valid categories based on the include and exclude sets.
|
424
|
+
"""
|
425
|
+
include = cls._validate_include(include)
|
426
|
+
exclude = cls._validate_exclude(exclude)
|
427
|
+
|
428
|
+
included_nodes = set.union(*[(cls.nodes(cat) | {cat}) for cat in include]) if len(include) > 0 else include
|
429
|
+
excluded_nodes = set.union(*[(cls.nodes(cat) | {cat}) for cat in exclude]) if len(exclude) > 0 else exclude
|
430
|
+
return included_nodes - excluded_nodes
|
431
|
+
|
432
|
+
@classmethod
|
433
|
+
def _leaves(cls, tree: '_hierarchy_typing') -> Set[TokenCategory]:
|
434
|
+
"""
|
435
|
+
Recursively get all leaves (nodes without children) in the hierarchy tree.
|
436
|
+
"""
|
437
|
+
if not tree:
|
438
|
+
return set()
|
439
|
+
leaves = {node for node, children in tree.items() if not children}
|
440
|
+
for node, children in tree.items():
|
441
|
+
leaves.update(cls._leaves(children))
|
442
|
+
return leaves
|
443
|
+
|
444
|
+
@classmethod
|
445
|
+
def leaves(cls, target: TokenCategory) -> Set[TokenCategory]:
|
446
|
+
"""
|
447
|
+
Get the leaves of the subtree of the target category.
|
448
|
+
|
449
|
+
Args:
|
450
|
+
target (TokenCategory): The target category.
|
451
|
+
|
452
|
+
Returns (List[TokenCategory]): The list of leaf categories of the target category.
|
453
|
+
"""
|
454
|
+
tree = cls._find_subtree(cls.hierarchy, target)
|
455
|
+
return cls._leaves(tree)
|
456
|
+
|
457
|
+
|
458
|
+
@classmethod
|
459
|
+
def _match(cls, category: TokenCategory, *,
|
460
|
+
include: Set[TokenCategory],
|
461
|
+
exclude: Set[TokenCategory]) -> bool:
|
462
|
+
"""
|
463
|
+
Check if a category matches include/exclude criteria.
|
464
|
+
"""
|
465
|
+
# Include the category itself along with its descendants.
|
466
|
+
target_nodes = cls.nodes(category) | {category}
|
467
|
+
|
468
|
+
valid_categories = cls.valid(include=include, exclude=exclude)
|
469
|
+
|
470
|
+
# Check if any node in the target set is in the valid categories.
|
471
|
+
return len(target_nodes & valid_categories) > 0
|
472
|
+
|
473
|
+
@classmethod
|
474
|
+
def _validate_include(cls, include: Optional[Set[TokenCategory]]) -> Set[TokenCategory]:
|
475
|
+
"""
|
476
|
+
Validate the include set.
|
477
|
+
"""
|
478
|
+
if include is None:
|
479
|
+
return cls.all()
|
480
|
+
if isinstance(include, (list, tuple)):
|
481
|
+
include = set(include)
|
482
|
+
elif not isinstance(include, set):
|
483
|
+
include = {include}
|
484
|
+
if not all(isinstance(cat, TokenCategory) for cat in include):
|
485
|
+
raise ValueError('Invalid category: include and exclude must be a set of TokenCategory.')
|
486
|
+
return include
|
487
|
+
|
488
|
+
@classmethod
|
489
|
+
def _validate_exclude(cls, exclude: Optional[Set[TokenCategory]]) -> Set[TokenCategory]:
|
490
|
+
"""
|
491
|
+
Validate the exclude set.
|
492
|
+
"""
|
493
|
+
if exclude is None:
|
494
|
+
return set()
|
495
|
+
if isinstance(exclude, (list, tuple)):
|
496
|
+
exclude = set(exclude)
|
497
|
+
elif not isinstance(exclude, set):
|
498
|
+
exclude = {exclude}
|
499
|
+
if not all(isinstance(cat, TokenCategory) for cat in exclude):
|
500
|
+
raise ValueError(f'Invalid category: category must be a {TokenCategory.__name__}.')
|
501
|
+
return exclude
|
502
|
+
|
503
|
+
|
504
|
+
@classmethod
|
505
|
+
def match(cls, category: TokenCategory, *,
|
506
|
+
include: Optional[Set[TokenCategory]] = None,
|
507
|
+
exclude: Optional[Set[TokenCategory]] = None) -> bool:
|
508
|
+
"""
|
509
|
+
Check if the category matches the include and exclude sets.
|
510
|
+
If include is None, all categories are included. \
|
511
|
+
If exclude is None, no categories are excluded.
|
512
|
+
|
513
|
+
Args:
|
514
|
+
category (TokenCategory): The category to check.
|
515
|
+
include (Optional[Set[TokenCategory]]): The set of categories to include. Defaults to None. \
|
516
|
+
If None, all categories are included.
|
517
|
+
exclude (Optional[Set[TokenCategory]]): The set of categories to exclude. Defaults to None. \
|
518
|
+
If None, no categories are excluded.
|
519
|
+
|
520
|
+
Returns (bool): True if the category matches the include and exclude sets, False otherwise.
|
521
|
+
|
522
|
+
Examples:
|
523
|
+
>>> TokenCategoryHierarchyMapper.match(TokenCategory.NOTE, include={TokenCategory.NOTE_REST})
|
524
|
+
True
|
525
|
+
>>> TokenCategoryHierarchyMapper.match(TokenCategory.NOTE, include={TokenCategory.NOTE_REST}, exclude={TokenCategory.REST})
|
526
|
+
True
|
527
|
+
>>> TokenCategoryHierarchyMapper.match(TokenCategory.NOTE, include={TokenCategory.NOTE_REST}, exclude={TokenCategory.NOTE})
|
528
|
+
False
|
529
|
+
>>> TokenCategoryHierarchyMapper.match(TokenCategory.NOTE, include={TokenCategory.CORE}, exclude={TokenCategory.DURATION})
|
530
|
+
True
|
531
|
+
>>> TokenCategoryHierarchyMapper.match(TokenCategory.DURATION, include={TokenCategory.CORE}, exclude={TokenCategory.DURATION})
|
532
|
+
False
|
533
|
+
"""
|
534
|
+
include = cls._validate_include(include)
|
535
|
+
exclude = cls._validate_exclude(exclude)
|
536
|
+
|
537
|
+
return cls._match(category, include=include, exclude=exclude)
|
538
|
+
|
539
|
+
@classmethod
|
540
|
+
def all(cls) -> Set[TokenCategory]:
|
541
|
+
"""
|
542
|
+
Get all categories in the hierarchy.
|
543
|
+
|
544
|
+
Returns:
|
545
|
+
Set[TokenCategory]: The set of all categories in the hierarchy.
|
546
|
+
"""
|
547
|
+
return cls._nodes(cls.hierarchy)
|
548
|
+
|
549
|
+
@classmethod
|
550
|
+
def tree(cls) -> str:
|
551
|
+
"""
|
552
|
+
Return a string representation of the category hierarchy,
|
553
|
+
formatted similar to the output of the Unix 'tree' command.
|
554
|
+
|
555
|
+
Example output:
|
556
|
+
.
|
557
|
+
├── STRUCTURAL
|
558
|
+
├── CORE
|
559
|
+
│ ├── NOTE_REST
|
560
|
+
│ │ ├── DURATION
|
561
|
+
│ │ ├── NOTE
|
562
|
+
│ │ │ ├── PITCH
|
563
|
+
│ │ │ └── DECORATION
|
564
|
+
│ │ └── REST
|
565
|
+
│ ├── CHORD
|
566
|
+
│ └── EMPTY
|
567
|
+
├── SIGNATURES
|
568
|
+
│ ├── CLEF
|
569
|
+
│ ├── TIME_SIGNATURE
|
570
|
+
│ ├── METER_SYMBOL
|
571
|
+
│ └── KEY_SIGNATURE
|
572
|
+
├── ENGRAVED_SYMBOLS
|
573
|
+
├── OTHER_CONTEXTUAL
|
574
|
+
├── BARLINES
|
575
|
+
├── COMMENTS
|
576
|
+
│ ├── FIELD_COMMENTS
|
577
|
+
│ └── LINE_COMMENTS
|
578
|
+
├── DYNAMICS
|
579
|
+
├── HARMONY
|
580
|
+
...
|
581
|
+
"""
|
582
|
+
def build_tree(tree: Dict[TokenCategory, '_hierarchy_typing'], prefix: str = "") -> [str]:
|
583
|
+
lines_buffer = []
|
584
|
+
items = list(tree.items())
|
585
|
+
count = len(items)
|
586
|
+
for index, (category, subtree) in enumerate(items):
|
587
|
+
connector = "└── " if index == count - 1 else "├── "
|
588
|
+
lines_buffer.append(prefix + connector + str(category))
|
589
|
+
extension = " " if index == count - 1 else "│ "
|
590
|
+
lines_buffer.extend(build_tree(subtree, prefix + extension))
|
591
|
+
return lines_buffer
|
592
|
+
|
593
|
+
lines = ["."]
|
594
|
+
lines.extend(build_tree(cls.hierarchy))
|
595
|
+
return "\n".join(lines)
|
596
|
+
|
597
|
+
|
598
|
+
class PitchRest:
|
599
|
+
"""
|
600
|
+
Represents a name or a rest in a note.
|
601
|
+
|
602
|
+
The name is represented using the International Standard Organization (ISO) name notation.
|
603
|
+
The first line below the staff is the C4 in G clef. The above C is C5, the below C is C3, etc.
|
604
|
+
|
605
|
+
The Humdrum Kern format uses the following name representation:
|
606
|
+
'c' = C4
|
607
|
+
'cc' = C5
|
608
|
+
'ccc' = C6
|
609
|
+
'cccc' = C7
|
610
|
+
|
611
|
+
'C' = C3
|
612
|
+
'CC' = C2
|
613
|
+
'CCC' = C1
|
614
|
+
|
615
|
+
The rests are represented by the letter 'r'. The rests do not have name.
|
616
|
+
|
617
|
+
This class do not limit the name ranges.
|
618
|
+
|
619
|
+
|
620
|
+
In the following example, the name is represented by the letter 'c'. The name of 'c' is C4, 'cc' is C5, 'ccc' is C6.
|
621
|
+
```
|
622
|
+
**kern
|
623
|
+
*clefG2
|
624
|
+
2c // C4
|
625
|
+
2cc // C5
|
626
|
+
2ccc // C6
|
627
|
+
2C // C3
|
628
|
+
2CC // C2
|
629
|
+
2CCC // C1
|
630
|
+
*-
|
631
|
+
```
|
632
|
+
"""
|
633
|
+
C4_PITCH_LOWERCASE = 'c'
|
634
|
+
C4_OCATAVE = 4
|
635
|
+
C3_PITCH_UPPERCASE = 'C'
|
636
|
+
C3_OCATAVE = 3
|
637
|
+
REST_CHARACTER = 'r'
|
638
|
+
|
639
|
+
VALID_PITCHES = 'abcdefg' + 'ABCDEFG' + REST_CHARACTER
|
640
|
+
|
641
|
+
def __init__(self, raw_pitch: str):
|
642
|
+
"""
|
643
|
+
Create a new PitchRest object.
|
644
|
+
|
645
|
+
Args:
|
646
|
+
raw_pitch (str): name representation in Humdrum Kern format
|
647
|
+
|
648
|
+
Examples:
|
649
|
+
>>> pitch_rest = PitchRest('c')
|
650
|
+
>>> pitch_rest = PitchRest('r')
|
651
|
+
>>> pitch_rest = PitchRest('DDD')
|
652
|
+
"""
|
653
|
+
if raw_pitch is None or len(raw_pitch) == 0:
|
654
|
+
raise ValueError(f'Empty name: name can not be None or empty. But {raw_pitch} was provided.')
|
655
|
+
|
656
|
+
self.encoding = raw_pitch
|
657
|
+
self.pitch, self.octave = self.__parse_pitch_octave()
|
658
|
+
|
659
|
+
def __parse_pitch_octave(self) -> (str, int):
|
660
|
+
if self.encoding == PitchRest.REST_CHARACTER:
|
661
|
+
return PitchRest.REST_CHARACTER, None
|
662
|
+
|
663
|
+
if self.encoding.islower():
|
664
|
+
min_octave = PitchRest.C4_OCATAVE
|
665
|
+
octave = min_octave + (len(self.encoding) - 1)
|
666
|
+
pitch = self.encoding[0].lower()
|
667
|
+
return pitch, octave
|
668
|
+
|
669
|
+
if self.encoding.isupper():
|
670
|
+
max_octave = PitchRest.C3_OCATAVE
|
671
|
+
octave = max_octave - (len(self.encoding) - 1)
|
672
|
+
pitch = self.encoding[0].lower()
|
673
|
+
return pitch, octave
|
674
|
+
|
675
|
+
raise ValueError(f'Invalid name: name {self.encoding} is not a valid name representation.')
|
676
|
+
|
677
|
+
def is_rest(self) -> bool:
|
678
|
+
"""
|
679
|
+
Check if the name is a rest.
|
680
|
+
|
681
|
+
Returns:
|
682
|
+
bool: True if the name is a rest, False otherwise.
|
683
|
+
|
684
|
+
Examples:
|
685
|
+
>>> pitch_rest = PitchRest('c')
|
686
|
+
>>> pitch_rest.is_rest()
|
687
|
+
False
|
688
|
+
>>> pitch_rest = PitchRest('r')
|
689
|
+
>>> pitch_rest.is_rest()
|
690
|
+
True
|
691
|
+
"""
|
692
|
+
return self.octave is None
|
693
|
+
|
694
|
+
@staticmethod
|
695
|
+
def pitch_comparator(pitch_a: str, pitch_b: str) -> int:
|
696
|
+
"""
|
697
|
+
Compare two pitches of the same octave.
|
698
|
+
|
699
|
+
The lower name is 'a'. So 'a' < 'b' < 'c' < 'd' < 'e' < 'f' < 'g'
|
700
|
+
|
701
|
+
Args:
|
702
|
+
pitch_a: One name of 'abcdefg'
|
703
|
+
pitch_b: Another name of 'abcdefg'
|
704
|
+
|
705
|
+
Returns:
|
706
|
+
-1 if pitch1 is lower than pitch2
|
707
|
+
0 if pitch1 is equal to pitch2
|
708
|
+
1 if pitch1 is higher than pitch2
|
709
|
+
|
710
|
+
Examples:
|
711
|
+
>>> PitchRest.pitch_comparator('c', 'c')
|
712
|
+
0
|
713
|
+
>>> PitchRest.pitch_comparator('c', 'd')
|
714
|
+
-1
|
715
|
+
>>> PitchRest.pitch_comparator('d', 'c')
|
716
|
+
1
|
717
|
+
"""
|
718
|
+
if pitch_a < pitch_b:
|
719
|
+
return -1
|
720
|
+
if pitch_a > pitch_b:
|
721
|
+
return 1
|
722
|
+
return 0
|
723
|
+
|
724
|
+
def __str__(self):
|
725
|
+
return f'{self.encoding}'
|
726
|
+
|
727
|
+
def __repr__(self):
|
728
|
+
return f'[PitchRest: {self.encoding}, name={self.pitch}, octave={self.octave}]'
|
729
|
+
|
730
|
+
def __eq__(self, other: 'PitchRest') -> bool:
|
731
|
+
"""
|
732
|
+
Compare two pitches and rests.
|
733
|
+
|
734
|
+
Args:
|
735
|
+
other (PitchRest): The other name to compare
|
736
|
+
|
737
|
+
Returns (bool):
|
738
|
+
True if the pitches are equal, False otherwise
|
739
|
+
|
740
|
+
Examples:
|
741
|
+
>>> pitch_rest = PitchRest('c')
|
742
|
+
>>> pitch_rest2 = PitchRest('c')
|
743
|
+
>>> pitch_rest == pitch_rest2
|
744
|
+
True
|
745
|
+
>>> pitch_rest = PitchRest('c')
|
746
|
+
>>> pitch_rest2 = PitchRest('ccc')
|
747
|
+
>>> pitch_rest == pitch_rest2
|
748
|
+
False
|
749
|
+
>>> pitch_rest = PitchRest('c')
|
750
|
+
>>> pitch_rest2 = PitchRest('r')
|
751
|
+
>>> pitch_rest == pitch_rest2
|
752
|
+
False
|
753
|
+
>>> pitch_rest = PitchRest('r')
|
754
|
+
>>> pitch_rest2 = PitchRest('r')
|
755
|
+
>>> pitch_rest == pitch_rest2
|
756
|
+
True
|
757
|
+
|
758
|
+
"""
|
759
|
+
if not isinstance(other, PitchRest):
|
760
|
+
return False
|
761
|
+
if self.is_rest() and other.is_rest():
|
762
|
+
return True
|
763
|
+
if self.is_rest() or other.is_rest():
|
764
|
+
return False
|
765
|
+
return self.pitch == other.pitch and self.octave == other.octave
|
766
|
+
|
767
|
+
def __ne__(self, other: 'PitchRest') -> bool:
|
768
|
+
"""
|
769
|
+
Compare two pitches and rests.
|
770
|
+
Args:
|
771
|
+
other (PitchRest): The other name to compare
|
772
|
+
|
773
|
+
Returns (bool):
|
774
|
+
True if the pitches are different, False otherwise
|
775
|
+
|
776
|
+
Examples:
|
777
|
+
>>> pitch_rest = PitchRest('c')
|
778
|
+
>>> pitch_rest2 = PitchRest('c')
|
779
|
+
>>> pitch_rest != pitch_rest2
|
780
|
+
False
|
781
|
+
>>> pitch_rest = PitchRest('c')
|
782
|
+
>>> pitch_rest2 = PitchRest('ccc')
|
783
|
+
>>> pitch_rest != pitch_rest2
|
784
|
+
True
|
785
|
+
>>> pitch_rest = PitchRest('c')
|
786
|
+
>>> pitch_rest2 = PitchRest('r')
|
787
|
+
>>> pitch_rest != pitch_rest2
|
788
|
+
True
|
789
|
+
>>> pitch_rest = PitchRest('r')
|
790
|
+
>>> pitch_rest2 = PitchRest('r')
|
791
|
+
>>> pitch_rest != pitch_rest2
|
792
|
+
False
|
793
|
+
"""
|
794
|
+
return not self.__eq__(other)
|
795
|
+
|
796
|
+
def __gt__(self, other: 'PitchRest') -> bool:
|
797
|
+
"""
|
798
|
+
Compare two pitches.
|
799
|
+
|
800
|
+
If any of the pitches is a rest, the comparison raise an exception.
|
801
|
+
|
802
|
+
Args:
|
803
|
+
other (PitchRest): The other name to compare
|
804
|
+
|
805
|
+
Returns (bool): True if this name is higher than the other, False otherwise
|
806
|
+
|
807
|
+
Examples:
|
808
|
+
>>> pitch_rest = PitchRest('c')
|
809
|
+
>>> pitch_rest2 = PitchRest('d')
|
810
|
+
>>> pitch_rest > pitch_rest2
|
811
|
+
False
|
812
|
+
>>> pitch_rest = PitchRest('c')
|
813
|
+
>>> pitch_rest2 = PitchRest('c')
|
814
|
+
>>> pitch_rest > pitch_rest2
|
815
|
+
False
|
816
|
+
>>> pitch_rest = PitchRest('c')
|
817
|
+
>>> pitch_rest2 = PitchRest('b')
|
818
|
+
>>> pitch_rest > pitch_rest2
|
819
|
+
True
|
820
|
+
>>> pitch_rest = PitchRest('r')
|
821
|
+
>>> pitch_rest2 = PitchRest('c')
|
822
|
+
>>> pitch_rest > pitch_rest2
|
823
|
+
Traceback (most recent call last):
|
824
|
+
...
|
825
|
+
ValueError: ...
|
826
|
+
>>> pitch_rest = PitchRest('r')
|
827
|
+
>>> pitch_rest2 = PitchRest('r')
|
828
|
+
>>> pitch_rest > pitch_rest2
|
829
|
+
Traceback (most recent call last):
|
830
|
+
ValueError: ...
|
831
|
+
|
832
|
+
|
833
|
+
"""
|
834
|
+
if self.is_rest() or other.is_rest():
|
835
|
+
raise ValueError(f'Invalid comparison: > operator can not be used to compare name of a rest.\n\
|
836
|
+
self={repr(self)} > other={repr(other)}')
|
837
|
+
|
838
|
+
if self.octave > other.octave:
|
839
|
+
return True
|
840
|
+
if self.octave == other.octave:
|
841
|
+
return PitchRest.pitch_comparator(self.pitch, other.pitch) > 0
|
842
|
+
return False
|
843
|
+
|
844
|
+
def __lt__(self, other: 'PitchRest') -> bool:
|
845
|
+
"""
|
846
|
+
Compare two pitches.
|
847
|
+
|
848
|
+
If any of the pitches is a rest, the comparison raise an exception.
|
849
|
+
|
850
|
+
Args:
|
851
|
+
other: The other name to compare
|
852
|
+
|
853
|
+
Returns:
|
854
|
+
True if this name is lower than the other, False otherwise
|
855
|
+
|
856
|
+
Examples:
|
857
|
+
>>> pitch_rest = PitchRest('c')
|
858
|
+
>>> pitch_rest2 = PitchRest('d')
|
859
|
+
>>> pitch_rest < pitch_rest2
|
860
|
+
True
|
861
|
+
>>> pitch_rest = PitchRest('c')
|
862
|
+
>>> pitch_rest2 = PitchRest('c')
|
863
|
+
>>> pitch_rest < pitch_rest2
|
864
|
+
False
|
865
|
+
>>> pitch_rest = PitchRest('c')
|
866
|
+
>>> pitch_rest2 = PitchRest('b')
|
867
|
+
>>> pitch_rest < pitch_rest2
|
868
|
+
False
|
869
|
+
>>> pitch_rest = PitchRest('r')
|
870
|
+
>>> pitch_rest2 = PitchRest('c')
|
871
|
+
>>> pitch_rest < pitch_rest2
|
872
|
+
Traceback (most recent call last):
|
873
|
+
...
|
874
|
+
ValueError: ...
|
875
|
+
>>> pitch_rest = PitchRest('r')
|
876
|
+
>>> pitch_rest2 = PitchRest('r')
|
877
|
+
>>> pitch_rest < pitch_rest2
|
878
|
+
Traceback (most recent call last):
|
879
|
+
...
|
880
|
+
ValueError: ...
|
881
|
+
|
882
|
+
"""
|
883
|
+
if self.is_rest() or other.is_rest():
|
884
|
+
raise ValueError(f'Invalid comparison: < operator can not be used to compare name of a rest.\n\
|
885
|
+
self={repr(self)} < other={repr(other)}')
|
886
|
+
|
887
|
+
if self.octave < other.octave:
|
888
|
+
return True
|
889
|
+
if self.octave == other.octave:
|
890
|
+
return PitchRest.pitch_comparator(self.pitch, other.pitch) < 0
|
891
|
+
return False
|
892
|
+
|
893
|
+
def __ge__(self, other: 'PitchRest') -> bool:
|
894
|
+
"""
|
895
|
+
Compare two pitches. If any of the PitchRest is a rest, the comparison raise an exception.
|
896
|
+
Args:
|
897
|
+
other (PitchRest): The other name to compare
|
898
|
+
|
899
|
+
Returns (bool):
|
900
|
+
True if this name is higher or equal than the other, False otherwise
|
901
|
+
|
902
|
+
Examples:
|
903
|
+
>>> pitch_rest = PitchRest('c')
|
904
|
+
>>> pitch_rest2 = PitchRest('d')
|
905
|
+
>>> pitch_rest >= pitch_rest2
|
906
|
+
False
|
907
|
+
>>> pitch_rest = PitchRest('c')
|
908
|
+
>>> pitch_rest2 = PitchRest('c')
|
909
|
+
>>> pitch_rest >= pitch_rest2
|
910
|
+
True
|
911
|
+
>>> pitch_rest = PitchRest('c')
|
912
|
+
>>> pitch_rest2 = PitchRest('b')
|
913
|
+
>>> pitch_rest >= pitch_rest2
|
914
|
+
True
|
915
|
+
>>> pitch_rest = PitchRest('r')
|
916
|
+
>>> pitch_rest2 = PitchRest('c')
|
917
|
+
>>> pitch_rest >= pitch_rest2
|
918
|
+
Traceback (most recent call last):
|
919
|
+
...
|
920
|
+
ValueError: ...
|
921
|
+
>>> pitch_rest = PitchRest('r')
|
922
|
+
>>> pitch_rest2 = PitchRest('r')
|
923
|
+
>>> pitch_rest >= pitch_rest2
|
924
|
+
Traceback (most recent call last):
|
925
|
+
...
|
926
|
+
ValueError: ...
|
927
|
+
|
928
|
+
|
929
|
+
"""
|
930
|
+
return self.__gt__(other) or self.__eq__(other)
|
931
|
+
|
932
|
+
def __le__(self, other: 'PitchRest') -> bool:
|
933
|
+
"""
|
934
|
+
Compare two pitches. If any of the PitchRest is a rest, the comparison raise an exception.
|
935
|
+
Args:
|
936
|
+
other (PitchRest): The other name to compare
|
937
|
+
|
938
|
+
Returns (bool): True if this name is lower or equal than the other, False otherwise
|
939
|
+
|
940
|
+
Examples:
|
941
|
+
>>> pitch_rest = PitchRest('c')
|
942
|
+
>>> pitch_rest2 = PitchRest('d')
|
943
|
+
>>> pitch_rest <= pitch_rest2
|
944
|
+
True
|
945
|
+
>>> pitch_rest = PitchRest('c')
|
946
|
+
>>> pitch_rest2 = PitchRest('c')
|
947
|
+
>>> pitch_rest <= pitch_rest2
|
948
|
+
True
|
949
|
+
>>> pitch_rest = PitchRest('c')
|
950
|
+
>>> pitch_rest2 = PitchRest('b')
|
951
|
+
>>> pitch_rest <= pitch_rest2
|
952
|
+
False
|
953
|
+
>>> pitch_rest = PitchRest('r')
|
954
|
+
>>> pitch_rest2 = PitchRest('c')
|
955
|
+
>>> pitch_rest <= pitch_rest2
|
956
|
+
Traceback (most recent call last):
|
957
|
+
...
|
958
|
+
ValueError: ...
|
959
|
+
>>> pitch_rest = PitchRest('r')
|
960
|
+
>>> pitch_rest2 = PitchRest('r')
|
961
|
+
>>> pitch_rest <= pitch_rest2
|
962
|
+
Traceback (most recent call last):
|
963
|
+
...
|
964
|
+
ValueError: ...
|
965
|
+
|
966
|
+
"""
|
967
|
+
return self.__lt__(other) or self.__eq__(other)
|
968
|
+
|
969
|
+
|
970
|
+
class Duration(ABC):
|
971
|
+
"""
|
972
|
+
Represents the duration of a note or a rest.
|
973
|
+
|
974
|
+
The duration is represented using the Humdrum Kern format.
|
975
|
+
The duration is a number that represents the number of units of the duration.
|
976
|
+
|
977
|
+
The duration of a whole note is 1, half note is 2, quarter note is 4, eighth note is 8, etc.
|
978
|
+
|
979
|
+
The duration of a note is represented by a number. The duration of a rest is also represented by a number.
|
980
|
+
|
981
|
+
This class do not limit the duration ranges.
|
982
|
+
|
983
|
+
In the following example, the duration is represented by the number '2'.
|
984
|
+
```
|
985
|
+
**kern
|
986
|
+
*clefG2
|
987
|
+
2c // whole note
|
988
|
+
4c // half note
|
989
|
+
8c // quarter note
|
990
|
+
16c // eighth note
|
991
|
+
*-
|
992
|
+
```
|
993
|
+
"""
|
994
|
+
|
995
|
+
def __init__(self, raw_duration):
|
996
|
+
self.encoding = str(raw_duration)
|
997
|
+
|
998
|
+
@abstractmethod
|
999
|
+
def modify(self, ratio: int):
|
1000
|
+
pass
|
1001
|
+
|
1002
|
+
@abstractmethod
|
1003
|
+
def __deepcopy__(self, memo=None):
|
1004
|
+
pass
|
1005
|
+
|
1006
|
+
@abstractmethod
|
1007
|
+
def __eq__(self, other):
|
1008
|
+
pass
|
1009
|
+
|
1010
|
+
@abstractmethod
|
1011
|
+
def __ne__(self, other):
|
1012
|
+
pass
|
1013
|
+
|
1014
|
+
@abstractmethod
|
1015
|
+
def __gt__(self, other):
|
1016
|
+
pass
|
1017
|
+
|
1018
|
+
@abstractmethod
|
1019
|
+
def __lt__(self, other):
|
1020
|
+
pass
|
1021
|
+
|
1022
|
+
@abstractmethod
|
1023
|
+
def __ge__(self, other):
|
1024
|
+
pass
|
1025
|
+
|
1026
|
+
@abstractmethod
|
1027
|
+
def __le__(self, other):
|
1028
|
+
pass
|
1029
|
+
|
1030
|
+
@abstractmethod
|
1031
|
+
def __str__(self):
|
1032
|
+
pass
|
1033
|
+
|
1034
|
+
|
1035
|
+
class DurationFactory:
|
1036
|
+
@staticmethod
|
1037
|
+
def create_duration(duration: str) -> Duration:
|
1038
|
+
return DurationClassical(int(duration))
|
1039
|
+
|
1040
|
+
|
1041
|
+
class DurationMensural(Duration):
|
1042
|
+
"""
|
1043
|
+
Represents the duration in mensural notation of a note or a rest.
|
1044
|
+
"""
|
1045
|
+
|
1046
|
+
def __init__(self, duration):
|
1047
|
+
super().__init__(duration)
|
1048
|
+
self.duration = duration
|
1049
|
+
|
1050
|
+
def __eq__(self, other):
|
1051
|
+
raise NotImplementedError()
|
1052
|
+
|
1053
|
+
def modify(self, ratio: int):
|
1054
|
+
raise NotImplementedError()
|
1055
|
+
|
1056
|
+
def __deepcopy__(self, memo=None):
|
1057
|
+
raise NotImplementedError()
|
1058
|
+
|
1059
|
+
def __gt__(self, other):
|
1060
|
+
raise NotImplementedError()
|
1061
|
+
|
1062
|
+
def __lt__(self, other):
|
1063
|
+
raise NotImplementedError()
|
1064
|
+
|
1065
|
+
def __le__(self, other):
|
1066
|
+
raise NotImplementedError()
|
1067
|
+
|
1068
|
+
def __str__(self):
|
1069
|
+
raise NotImplementedError()
|
1070
|
+
|
1071
|
+
def __ge__(self, other):
|
1072
|
+
raise NotImplementedError()
|
1073
|
+
|
1074
|
+
def __ne__(self, other):
|
1075
|
+
raise NotImplementedError()
|
1076
|
+
|
1077
|
+
|
1078
|
+
class DurationClassical(Duration):
|
1079
|
+
"""
|
1080
|
+
Represents the duration in classical notation of a note or a rest.
|
1081
|
+
"""
|
1082
|
+
|
1083
|
+
def __init__(self, duration: int):
|
1084
|
+
"""
|
1085
|
+
Create a new Duration object.
|
1086
|
+
|
1087
|
+
Args:
|
1088
|
+
duration (str): duration representation in Humdrum Kern format
|
1089
|
+
|
1090
|
+
Examples:
|
1091
|
+
>>> duration = DurationClassical(2)
|
1092
|
+
True
|
1093
|
+
>>> duration = DurationClassical(4)
|
1094
|
+
True
|
1095
|
+
>>> duration = DurationClassical(32)
|
1096
|
+
True
|
1097
|
+
>>> duration = DurationClassical(1)
|
1098
|
+
True
|
1099
|
+
>>> duration = DurationClassical(0)
|
1100
|
+
False
|
1101
|
+
>>> duration = DurationClassical(-2)
|
1102
|
+
False
|
1103
|
+
>>> duration = DurationClassical(3)
|
1104
|
+
False
|
1105
|
+
>>> duration = DurationClassical(7)
|
1106
|
+
False
|
1107
|
+
"""
|
1108
|
+
super().__init__(duration)
|
1109
|
+
if not DurationClassical.__is_valid_duration(duration):
|
1110
|
+
raise ValueError(f'Bad duration: {duration} was provided.')
|
1111
|
+
|
1112
|
+
self.duration = int(duration)
|
1113
|
+
|
1114
|
+
def modify(self, ratio: int):
|
1115
|
+
"""
|
1116
|
+
Modify the duration of a note or a rest of the current object.
|
1117
|
+
|
1118
|
+
Args:
|
1119
|
+
ratio (int): The factor to modify the duration. The factor must be greater than 0.
|
1120
|
+
|
1121
|
+
Returns (DurationClassical): The new duration object with the modified duration.
|
1122
|
+
|
1123
|
+
Examples:
|
1124
|
+
>>> duration = DurationClassical(2)
|
1125
|
+
>>> new_duration = duration.modify(2)
|
1126
|
+
>>> new_duration.duration
|
1127
|
+
4
|
1128
|
+
>>> duration = DurationClassical(2)
|
1129
|
+
>>> new_duration = duration.modify(0)
|
1130
|
+
Traceback (most recent call last):
|
1131
|
+
...
|
1132
|
+
ValueError: Invalid factor provided: 0. The factor must be greater than 0.
|
1133
|
+
>>> duration = DurationClassical(2)
|
1134
|
+
>>> new_duration = duration.modify(-2)
|
1135
|
+
Traceback (most recent call last):
|
1136
|
+
...
|
1137
|
+
ValueError: Invalid factor provided: -2. The factor must be greater than 0.
|
1138
|
+
"""
|
1139
|
+
if not isinstance(ratio, int):
|
1140
|
+
raise ValueError(f'Invalid factor provided: {ratio}. The factor must be an integer.')
|
1141
|
+
if ratio <= 0:
|
1142
|
+
raise ValueError(f'Invalid factor provided: {ratio}. The factor must be greater than 0.')
|
1143
|
+
|
1144
|
+
return copy.deepcopy(DurationClassical(self.duration * ratio))
|
1145
|
+
|
1146
|
+
def __deepcopy__(self, memo=None):
|
1147
|
+
if memo is None:
|
1148
|
+
memo = {}
|
1149
|
+
|
1150
|
+
new_instance = DurationClassical(self.duration)
|
1151
|
+
new_instance.duration = self.duration
|
1152
|
+
return new_instance
|
1153
|
+
|
1154
|
+
def __str__(self):
|
1155
|
+
return f'{self.duration}'
|
1156
|
+
|
1157
|
+
def __eq__(self, other: 'DurationClassical') -> bool:
|
1158
|
+
"""
|
1159
|
+
Compare two durations.
|
1160
|
+
|
1161
|
+
Args:
|
1162
|
+
other (DurationClassical): The other duration to compare
|
1163
|
+
|
1164
|
+
Returns (bool): True if the durations are equal, False otherwise
|
1165
|
+
|
1166
|
+
|
1167
|
+
Examples:
|
1168
|
+
>>> duration = DurationClassical(2)
|
1169
|
+
>>> duration2 = DurationClassical(2)
|
1170
|
+
>>> duration == duration2
|
1171
|
+
True
|
1172
|
+
>>> duration = DurationClassical(2)
|
1173
|
+
>>> duration2 = DurationClassical(4)
|
1174
|
+
>>> duration == duration2
|
1175
|
+
False
|
1176
|
+
"""
|
1177
|
+
if not isinstance(other, DurationClassical):
|
1178
|
+
return False
|
1179
|
+
return self.duration == other.duration
|
1180
|
+
|
1181
|
+
def __ne__(self, other: 'DurationClassical') -> bool:
|
1182
|
+
"""
|
1183
|
+
Compare two durations.
|
1184
|
+
|
1185
|
+
Args:
|
1186
|
+
other (DurationClassical): The other duration to compare
|
1187
|
+
|
1188
|
+
Returns (bool):
|
1189
|
+
True if the durations are different, False otherwise
|
1190
|
+
|
1191
|
+
Examples:
|
1192
|
+
>>> duration = DurationClassical(2)
|
1193
|
+
>>> duration2 = DurationClassical(2)
|
1194
|
+
>>> duration != duration2
|
1195
|
+
False
|
1196
|
+
>>> duration = DurationClassical(2)
|
1197
|
+
>>> duration2 = DurationClassical(4)
|
1198
|
+
>>> duration != duration2
|
1199
|
+
True
|
1200
|
+
"""
|
1201
|
+
return not self.__eq__(other)
|
1202
|
+
|
1203
|
+
def __gt__(self, other: 'DurationClassical') -> bool:
|
1204
|
+
"""
|
1205
|
+
Compare two durations.
|
1206
|
+
|
1207
|
+
Args:
|
1208
|
+
other: The other duration to compare
|
1209
|
+
|
1210
|
+
Returns (bool):
|
1211
|
+
True if this duration is higher than the other, False otherwise
|
1212
|
+
|
1213
|
+
Examples:
|
1214
|
+
>>> duration = DurationClassical(2)
|
1215
|
+
>>> duration2 = DurationClassical(4)
|
1216
|
+
>>> duration > duration2
|
1217
|
+
False
|
1218
|
+
>>> duration = DurationClassical(4)
|
1219
|
+
>>> duration2 = DurationClassical(2)
|
1220
|
+
>>> duration > duration2
|
1221
|
+
True
|
1222
|
+
>>> duration = DurationClassical(4)
|
1223
|
+
>>> duration2 = DurationClassical(4)
|
1224
|
+
>>> duration > duration2
|
1225
|
+
False
|
1226
|
+
"""
|
1227
|
+
if not isinstance(other, DurationClassical):
|
1228
|
+
raise ValueError(f'Invalid comparison: > operator can not be used to compare duration with {type(other)}')
|
1229
|
+
return self.duration > other.duration
|
1230
|
+
|
1231
|
+
def __lt__(self, other: 'DurationClassical') -> bool:
|
1232
|
+
"""
|
1233
|
+
Compare two durations.
|
1234
|
+
|
1235
|
+
Args:
|
1236
|
+
other (DurationClassical): The other duration to compare
|
1237
|
+
|
1238
|
+
Returns (bool):
|
1239
|
+
True if this duration is lower than the other, False otherwise
|
1240
|
+
|
1241
|
+
Examples:
|
1242
|
+
>>> duration = DurationClassical(2)
|
1243
|
+
>>> duration2 = DurationClassical(4)
|
1244
|
+
>>> duration < duration2
|
1245
|
+
True
|
1246
|
+
>>> duration = DurationClassical(4)
|
1247
|
+
>>> duration2 = DurationClassical(2)
|
1248
|
+
>>> duration < duration2
|
1249
|
+
False
|
1250
|
+
>>> duration = DurationClassical(4)
|
1251
|
+
>>> duration2 = DurationClassical(4)
|
1252
|
+
>>> duration < duration2
|
1253
|
+
False
|
1254
|
+
"""
|
1255
|
+
if not isinstance(other, DurationClassical):
|
1256
|
+
raise ValueError(f'Invalid comparison: < operator can not be used to compare duration with {type(other)}')
|
1257
|
+
return self.duration < other.duration
|
1258
|
+
|
1259
|
+
def __ge__(self, other: 'DurationClassical') -> bool:
|
1260
|
+
"""
|
1261
|
+
Compare two durations.
|
1262
|
+
|
1263
|
+
Args:
|
1264
|
+
other (DurationClassical): The other duration to compare
|
1265
|
+
|
1266
|
+
Returns (bool):
|
1267
|
+
True if this duration is higher or equal than the other, False otherwise
|
1268
|
+
|
1269
|
+
Examples:
|
1270
|
+
>>> duration = DurationClassical(2)
|
1271
|
+
>>> duration2 = DurationClassical(4)
|
1272
|
+
>>> duration >= duration2
|
1273
|
+
False
|
1274
|
+
>>> duration = DurationClassical(4)
|
1275
|
+
>>> duration2 = DurationClassical(2)
|
1276
|
+
>>> duration >= duration2
|
1277
|
+
True
|
1278
|
+
>>> duration = DurationClassical(4)
|
1279
|
+
>>> duration2 = DurationClassical(4)
|
1280
|
+
>>> duration >= duration2
|
1281
|
+
True
|
1282
|
+
"""
|
1283
|
+
return self.__gt__(other) or self.__eq__(other)
|
1284
|
+
|
1285
|
+
def __le__(self, other: 'DurationClassical') -> bool:
|
1286
|
+
"""
|
1287
|
+
Compare two durations.
|
1288
|
+
|
1289
|
+
Args:
|
1290
|
+
other (DurationClassical): The other duration to compare
|
1291
|
+
|
1292
|
+
Returns:
|
1293
|
+
True if this duration is lower or equal than the other, False otherwise
|
1294
|
+
|
1295
|
+
Examples:
|
1296
|
+
>>> duration = DurationClassical(2)
|
1297
|
+
>>> duration2 = DurationClassical(4)
|
1298
|
+
>>> duration <= duration2
|
1299
|
+
True
|
1300
|
+
>>> duration = DurationClassical(4)
|
1301
|
+
>>> duration2 = DurationClassical(2)
|
1302
|
+
>>> duration <= duration2
|
1303
|
+
False
|
1304
|
+
>>> duration = DurationClassical(4)
|
1305
|
+
>>> duration2 = DurationClassical(4)
|
1306
|
+
>>> duration <= duration2
|
1307
|
+
True
|
1308
|
+
"""
|
1309
|
+
return self.__lt__(other) or self.__eq__(other)
|
1310
|
+
|
1311
|
+
@classmethod
|
1312
|
+
def __is_valid_duration(cls, duration: int) -> bool:
|
1313
|
+
try:
|
1314
|
+
duration = int(duration)
|
1315
|
+
if duration is None or duration <= 0:
|
1316
|
+
return False
|
1317
|
+
|
1318
|
+
return duration > 0 and (duration % 2 == 0 or duration == 1)
|
1319
|
+
except ValueError:
|
1320
|
+
return False
|
1321
|
+
|
1322
|
+
|
1323
|
+
class Subtoken:
|
1324
|
+
"""
|
1325
|
+
Subtoken class. Thhe subtokens are the smallest units of categories. ComplexToken objects are composed of subtokens.
|
1326
|
+
|
1327
|
+
Attributes:
|
1328
|
+
encoding: The complete unprocessed encoding
|
1329
|
+
category: The subtoken category, one of SubTokenCategory
|
1330
|
+
"""
|
1331
|
+
DECORATION = None
|
1332
|
+
|
1333
|
+
def __init__(self, encoding: str, category: TokenCategory):
|
1334
|
+
"""
|
1335
|
+
Subtoken constructor
|
1336
|
+
|
1337
|
+
Args:
|
1338
|
+
encoding (str): The complete unprocessed encoding
|
1339
|
+
category (TokenCategory): The subtoken category. \
|
1340
|
+
It should be a child of the main 'TokenCategory' in the hierarchy.
|
1341
|
+
|
1342
|
+
"""
|
1343
|
+
self.encoding = encoding
|
1344
|
+
self.category = category
|
1345
|
+
|
1346
|
+
def __str__(self):
|
1347
|
+
"""
|
1348
|
+
Returns the string representation of the subtoken.
|
1349
|
+
|
1350
|
+
Returns (str): The string representation of the subtoken.
|
1351
|
+
"""
|
1352
|
+
return self.encoding
|
1353
|
+
|
1354
|
+
def __eq__(self, other):
|
1355
|
+
"""
|
1356
|
+
Compare two subtokens.
|
1357
|
+
|
1358
|
+
Args:
|
1359
|
+
other (Subtoken): The other subtoken to compare.
|
1360
|
+
Returns (bool): True if the subtokens are equal, False otherwise.
|
1361
|
+
"""
|
1362
|
+
if not isinstance(other, Subtoken):
|
1363
|
+
return False
|
1364
|
+
return self.encoding == other.encoding and self.category == other.category
|
1365
|
+
|
1366
|
+
def __ne__(self, other):
|
1367
|
+
"""
|
1368
|
+
Compare two subtokens.
|
1369
|
+
|
1370
|
+
Args:
|
1371
|
+
other (Subtoken): The other subtoken to compare.
|
1372
|
+
Returns (bool): True if the subtokens are different, False otherwise.
|
1373
|
+
"""
|
1374
|
+
return not self.__eq__(other)
|
1375
|
+
|
1376
|
+
def __hash__(self):
|
1377
|
+
"""
|
1378
|
+
Returns the hash of the subtoken.
|
1379
|
+
|
1380
|
+
Returns (int): The hash of the subtoken.
|
1381
|
+
"""
|
1382
|
+
return hash((self.encoding, self.category))
|
1383
|
+
|
1384
|
+
class AbstractToken(ABC):
|
1385
|
+
"""
|
1386
|
+
An abstract base class representing a token.
|
1387
|
+
|
1388
|
+
This class serves as a blueprint for creating various types of tokens, which are
|
1389
|
+
categorized based on their TokenCategory.
|
1390
|
+
|
1391
|
+
Attributes:
|
1392
|
+
encoding (str): The original representation of the token.
|
1393
|
+
category (TokenCategory): The category of the token.
|
1394
|
+
hidden (bool): A flag indicating whether the token is hidden. Defaults to False.
|
1395
|
+
"""
|
1396
|
+
|
1397
|
+
def __init__(self, encoding: str, category: TokenCategory):
|
1398
|
+
"""
|
1399
|
+
AbstractToken constructor
|
1400
|
+
|
1401
|
+
Args:
|
1402
|
+
encoding (str): The original representation of the token.
|
1403
|
+
category (TokenCategory): The category of the token.
|
1404
|
+
"""
|
1405
|
+
self.encoding = encoding
|
1406
|
+
self.category = category
|
1407
|
+
self.hidden = False
|
1408
|
+
|
1409
|
+
@abstractmethod
|
1410
|
+
def export(self, **kwargs) -> str:
|
1411
|
+
"""
|
1412
|
+
Exports the token.
|
1413
|
+
|
1414
|
+
Keyword Arguments:
|
1415
|
+
filter_categories (Optional[Callable[[TokenCategory], bool]]): A function that takes a TokenCategory and returns a boolean
|
1416
|
+
indicating whether the token should be included in the export. If provided, only tokens for which the
|
1417
|
+
function returns True will be exported. Defaults to None. If None, all tokens will be exported.
|
1418
|
+
|
1419
|
+
Returns:
|
1420
|
+
str: The encoded token representation, potentially filtered if a filter_categories function is provided.
|
1421
|
+
|
1422
|
+
Examples:
|
1423
|
+
>>> token = AbstractToken('*clefF4', TokenCategory.SIGNATURES)
|
1424
|
+
>>> token.export()
|
1425
|
+
'*clefF4'
|
1426
|
+
>>> token.export(filter_categories=lambda cat: cat in {TokenCategory.SIGNATURES, TokenCategory.SIGNATURES.DURATION})
|
1427
|
+
'*clefF4'
|
1428
|
+
"""
|
1429
|
+
pass
|
1430
|
+
|
1431
|
+
|
1432
|
+
def __str__(self):
|
1433
|
+
"""
|
1434
|
+
Returns the string representation of the token.
|
1435
|
+
|
1436
|
+
Returns (str): The string representation of the token without processing.
|
1437
|
+
"""
|
1438
|
+
return self.export()
|
1439
|
+
|
1440
|
+
def __eq__(self, other):
|
1441
|
+
"""
|
1442
|
+
Compare two tokens.
|
1443
|
+
|
1444
|
+
Args:
|
1445
|
+
other (AbstractToken): The other token to compare.
|
1446
|
+
Returns (bool): True if the tokens are equal, False otherwise.
|
1447
|
+
"""
|
1448
|
+
if not isinstance(other, AbstractToken):
|
1449
|
+
return False
|
1450
|
+
return self.encoding == other.encoding and self.category == other.category
|
1451
|
+
|
1452
|
+
def __ne__(self, other):
|
1453
|
+
"""
|
1454
|
+
Compare two tokens.
|
1455
|
+
|
1456
|
+
Args:
|
1457
|
+
other (AbstractToken): The other token to compare.
|
1458
|
+
Returns (bool): True if the tokens are different, False otherwise.
|
1459
|
+
"""
|
1460
|
+
return not self.__eq__(other)
|
1461
|
+
|
1462
|
+
def __hash__(self):
|
1463
|
+
"""
|
1464
|
+
Returns the hash of the token.
|
1465
|
+
|
1466
|
+
Returns (int): The hash of the token.
|
1467
|
+
"""
|
1468
|
+
return hash((self.export(), self.category))
|
1469
|
+
|
1470
|
+
|
1471
|
+
class Token(AbstractToken, ABC):
|
1472
|
+
"""
|
1473
|
+
Abstract Token class.
|
1474
|
+
"""
|
1475
|
+
|
1476
|
+
def __init__(self, encoding, category):
|
1477
|
+
super().__init__(encoding, category)
|
1478
|
+
|
1479
|
+
|
1480
|
+
class SimpleToken(Token):
|
1481
|
+
"""
|
1482
|
+
SimpleToken class.
|
1483
|
+
"""
|
1484
|
+
|
1485
|
+
def __init__(self, encoding, category):
|
1486
|
+
super().__init__(encoding, category)
|
1487
|
+
|
1488
|
+
def export(self, **kwargs) -> str:
|
1489
|
+
"""
|
1490
|
+
Exports the token.
|
1491
|
+
|
1492
|
+
Args:
|
1493
|
+
**kwargs: 'filter_categories' (Optional[Callable[[TokenCategory], bool]]): It is ignored in this class.
|
1494
|
+
|
1495
|
+
Returns (str): The encoded token representation.
|
1496
|
+
"""
|
1497
|
+
return self.encoding
|
1498
|
+
|
1499
|
+
|
1500
|
+
class ErrorToken(SimpleToken):
|
1501
|
+
"""
|
1502
|
+
Used to wrap tokens that have not been parsed.
|
1503
|
+
"""
|
1504
|
+
|
1505
|
+
def __init__(
|
1506
|
+
self,
|
1507
|
+
encoding: str,
|
1508
|
+
line: int,
|
1509
|
+
error: str
|
1510
|
+
):
|
1511
|
+
"""
|
1512
|
+
ErrorToken constructor
|
1513
|
+
|
1514
|
+
Args:
|
1515
|
+
encoding (str): The original representation of the token.
|
1516
|
+
line (int): The line number of the token in the score.
|
1517
|
+
error (str): The error message thrown by the parser.
|
1518
|
+
"""
|
1519
|
+
super().__init__(encoding, TokenCategory.ERROR)
|
1520
|
+
self.error = error
|
1521
|
+
self.line = line
|
1522
|
+
|
1523
|
+
def export(self, **kwargs) -> str:
|
1524
|
+
"""
|
1525
|
+
Exports the error token.
|
1526
|
+
|
1527
|
+
Returns (str): A string representation of the error token.
|
1528
|
+
"""
|
1529
|
+
# return ERROR_TOKEN
|
1530
|
+
return self.encoding # TODO: add a constant for the error token
|
1531
|
+
|
1532
|
+
def __str__(self):
|
1533
|
+
"""
|
1534
|
+
Information about the error token.
|
1535
|
+
|
1536
|
+
Returns (str) The information about the error token.
|
1537
|
+
"""
|
1538
|
+
return f'Error token found at line {self.line} with encoding "{self.encoding}". Description: {self.error}'
|
1539
|
+
|
1540
|
+
|
1541
|
+
class MetacommentToken(SimpleToken):
|
1542
|
+
"""
|
1543
|
+
MetacommentToken class stores the metacomments of the score.
|
1544
|
+
Usually these are comments starting with `!!`.
|
1545
|
+
|
1546
|
+
"""
|
1547
|
+
|
1548
|
+
def __init__(self, encoding: str):
|
1549
|
+
"""
|
1550
|
+
Constructor for the MetacommentToken class.
|
1551
|
+
|
1552
|
+
Args:
|
1553
|
+
encoding (str): The original representation of the token.
|
1554
|
+
"""
|
1555
|
+
super().__init__(encoding, TokenCategory.LINE_COMMENTS)
|
1556
|
+
|
1557
|
+
|
1558
|
+
class InstrumentToken(SimpleToken):
|
1559
|
+
"""
|
1560
|
+
InstrumentToken class stores the instruments of the score.
|
1561
|
+
|
1562
|
+
These tokens usually look like `*I"Organo`.
|
1563
|
+
"""
|
1564
|
+
|
1565
|
+
def __init__(self, encoding: str):
|
1566
|
+
"""
|
1567
|
+
Constructor for the InstrumentToken
|
1568
|
+
|
1569
|
+
Args:
|
1570
|
+
encoding:
|
1571
|
+
"""
|
1572
|
+
super().__init__(encoding, TokenCategory.INSTRUMENTS)
|
1573
|
+
|
1574
|
+
|
1575
|
+
class FieldCommentToken(SimpleToken):
|
1576
|
+
"""
|
1577
|
+
FieldCommentToken class stores the metacomments of the score.
|
1578
|
+
Usually these are comments starting with `!!!`.
|
1579
|
+
|
1580
|
+
"""
|
1581
|
+
|
1582
|
+
def __init__(self, encoding):
|
1583
|
+
super().__init__(encoding, TokenCategory.FIELD_COMMENTS)
|
1584
|
+
|
1585
|
+
|
1586
|
+
class HeaderToken(SimpleToken):
|
1587
|
+
"""
|
1588
|
+
HeaderTokens class.
|
1589
|
+
"""
|
1590
|
+
|
1591
|
+
def __init__(self, encoding, spine_id: int):
|
1592
|
+
"""
|
1593
|
+
Constructor for the HeaderToken class.
|
1594
|
+
|
1595
|
+
Args:
|
1596
|
+
encoding (str): The original representation of the token.
|
1597
|
+
spine_id (int): The spine id of the token. The spine id is used to identify the token in the score.\
|
1598
|
+
The spine_id starts from 0 and increases by 1 for each new spine like the following example:
|
1599
|
+
**kern **kern **kern **dyn **text
|
1600
|
+
0 1 2 3 4
|
1601
|
+
"""
|
1602
|
+
super().__init__(encoding, TokenCategory.HEADER)
|
1603
|
+
self.spine_id = spine_id
|
1604
|
+
|
1605
|
+
def export(self, **kwargs) -> str:
|
1606
|
+
return self.encoding
|
1607
|
+
|
1608
|
+
|
1609
|
+
class SpineOperationToken(SimpleToken):
|
1610
|
+
"""
|
1611
|
+
SpineOperationToken class.
|
1612
|
+
|
1613
|
+
This token represents different operations in the Humdrum kern encoding.
|
1614
|
+
These are the available operations:
|
1615
|
+
- `*-`: spine-path terminator.
|
1616
|
+
- `*`: null interpretation.
|
1617
|
+
- `*+`: add spines.
|
1618
|
+
- `*^`: split spines.
|
1619
|
+
- `*x`: exchange spines.
|
1620
|
+
|
1621
|
+
Attributes:
|
1622
|
+
cancelled_at_stage (int): The stage at which the operation was cancelled. Defaults to None.
|
1623
|
+
"""
|
1624
|
+
|
1625
|
+
def __init__(self, encoding):
|
1626
|
+
super().__init__(encoding, TokenCategory.SPINE_OPERATION)
|
1627
|
+
self.cancelled_at_stage = None
|
1628
|
+
|
1629
|
+
def is_cancelled_at(self, stage) -> bool:
|
1630
|
+
"""
|
1631
|
+
Checks if the operation was cancelled at the given stage.
|
1632
|
+
|
1633
|
+
Args:
|
1634
|
+
stage (int): The stage at which the operation was cancelled.
|
1635
|
+
|
1636
|
+
Returns:
|
1637
|
+
bool: True if the operation was cancelled at the given stage, False otherwise.
|
1638
|
+
"""
|
1639
|
+
if self.cancelled_at_stage is None:
|
1640
|
+
return False
|
1641
|
+
else:
|
1642
|
+
return self.cancelled_at_stage < stage
|
1643
|
+
|
1644
|
+
|
1645
|
+
class BarToken(SimpleToken):
|
1646
|
+
"""
|
1647
|
+
BarToken class.
|
1648
|
+
"""
|
1649
|
+
|
1650
|
+
def __init__(self, encoding):
|
1651
|
+
super().__init__(encoding, TokenCategory.BARLINES)
|
1652
|
+
|
1653
|
+
|
1654
|
+
class SignatureToken(SimpleToken):
|
1655
|
+
"""
|
1656
|
+
SignatureToken class for all signature tokens. It will be overridden by more specific classes.
|
1657
|
+
"""
|
1658
|
+
|
1659
|
+
def __init__(self, encoding, category=TokenCategory.SIGNATURES):
|
1660
|
+
super().__init__(encoding, category)
|
1661
|
+
|
1662
|
+
|
1663
|
+
class ClefToken(SignatureToken):
|
1664
|
+
"""
|
1665
|
+
ClefToken class.
|
1666
|
+
"""
|
1667
|
+
|
1668
|
+
def __init__(self, encoding):
|
1669
|
+
super().__init__(encoding, TokenCategory.CLEF)
|
1670
|
+
|
1671
|
+
|
1672
|
+
class TimeSignatureToken(SignatureToken):
|
1673
|
+
"""
|
1674
|
+
TimeSignatureToken class.
|
1675
|
+
"""
|
1676
|
+
|
1677
|
+
def __init__(self, encoding):
|
1678
|
+
super().__init__(encoding, TokenCategory.TIME_SIGNATURE)
|
1679
|
+
|
1680
|
+
|
1681
|
+
class MeterSymbolToken(SignatureToken):
|
1682
|
+
"""
|
1683
|
+
MeterSymbolToken class.
|
1684
|
+
"""
|
1685
|
+
|
1686
|
+
def __init__(self, encoding):
|
1687
|
+
super().__init__(encoding, TokenCategory.METER_SYMBOL)
|
1688
|
+
|
1689
|
+
|
1690
|
+
class KeySignatureToken(SignatureToken):
|
1691
|
+
"""
|
1692
|
+
KeySignatureToken class.
|
1693
|
+
"""
|
1694
|
+
|
1695
|
+
def __init__(self, encoding):
|
1696
|
+
super().__init__(encoding, TokenCategory.KEY_SIGNATURE)
|
1697
|
+
|
1698
|
+
|
1699
|
+
class KeyToken(SignatureToken):
|
1700
|
+
"""
|
1701
|
+
KeyToken class.
|
1702
|
+
"""
|
1703
|
+
|
1704
|
+
def __init__(self, encoding):
|
1705
|
+
super().__init__(encoding, TokenCategory.KEY_TOKEN)
|
1706
|
+
|
1707
|
+
|
1708
|
+
class ComplexToken(Token, ABC):
|
1709
|
+
"""
|
1710
|
+
Abstract ComplexToken class. This abstract class ensures that the subclasses implement the export method using\
|
1711
|
+
the 'filter_categories' parameter to filter the subtokens.
|
1712
|
+
|
1713
|
+
Passing the argument 'filter_categories' by **kwargs don't break the compatibility with parent classes.
|
1714
|
+
|
1715
|
+
Here we're trying to get the Liskov substitution principle done...
|
1716
|
+
"""
|
1717
|
+
def __init__(self, encoding: str, category: TokenCategory):
|
1718
|
+
"""
|
1719
|
+
Constructor for the ComplexToken
|
1720
|
+
|
1721
|
+
Args:
|
1722
|
+
encoding (str): The original representation of the token.
|
1723
|
+
category (TokenCategory) : The category of the token.
|
1724
|
+
"""
|
1725
|
+
super().__init__(encoding, category)
|
1726
|
+
|
1727
|
+
@abstractmethod
|
1728
|
+
def export(self, **kwargs) -> str:
|
1729
|
+
"""
|
1730
|
+
Exports the token.
|
1731
|
+
|
1732
|
+
Keyword Arguments:
|
1733
|
+
filter_categories (Optional[Callable[[TokenCategory], bool]]): A function that takes a TokenCategory and returns a boolean
|
1734
|
+
indicating whether the token should be included in the export. If provided, only tokens for which the
|
1735
|
+
function returns True will be exported. Defaults to None. If None, all tokens will be exported.
|
1736
|
+
|
1737
|
+
Returns (str): The exported token.
|
1738
|
+
"""
|
1739
|
+
pass
|
1740
|
+
|
1741
|
+
|
1742
|
+
class CompoundToken(ComplexToken):
|
1743
|
+
def __init__(self, encoding: str, category: TokenCategory, subtokens: List[Subtoken]):
|
1744
|
+
"""
|
1745
|
+
Args:
|
1746
|
+
encoding (str): The complete unprocessed encoding
|
1747
|
+
category (TokenCategory): The token category, one of 'TokenCategory'
|
1748
|
+
subtokens (List[Subtoken]): The individual elements of the token. Also of type 'TokenCategory' but \
|
1749
|
+
in the hierarchy they must be children of the current token.
|
1750
|
+
"""
|
1751
|
+
super().__init__(encoding, category)
|
1752
|
+
|
1753
|
+
for subtoken in subtokens:
|
1754
|
+
if not isinstance(subtoken, Subtoken):
|
1755
|
+
raise ValueError(f'All subtokens must be instances of Subtoken. Found {type(subtoken)}')
|
1756
|
+
|
1757
|
+
self.subtokens = subtokens
|
1758
|
+
|
1759
|
+
def export(self, **kwargs) -> str:
|
1760
|
+
"""
|
1761
|
+
Exports the token.
|
1762
|
+
|
1763
|
+
Keyword Arguments:
|
1764
|
+
filter_categories (Optional[Callable[[TokenCategory], bool]]): A function that takes a TokenCategory and returns a boolean
|
1765
|
+
indicating whether the token should be included in the export. If provided, only tokens for which the
|
1766
|
+
function returns True will be exported. Defaults to None. If None, all tokens will be exported.
|
1767
|
+
|
1768
|
+
Returns (str): The exported token.
|
1769
|
+
"""
|
1770
|
+
filter_categories_fn = kwargs.get('filter_categories', None)
|
1771
|
+
parts = []
|
1772
|
+
for subtoken in self.subtokens:
|
1773
|
+
# Only export the subtoken if it passes the filter_categories (if provided)
|
1774
|
+
if filter_categories_fn is None or filter_categories_fn(subtoken.category):
|
1775
|
+
# parts.append(subtoken.export(**kwargs)) in the future when SubTokens will be Tokens
|
1776
|
+
parts.append(subtoken.encoding)
|
1777
|
+
return TOKEN_SEPARATOR.join(parts) if len(parts) > 0 else EMPTY_TOKEN
|
1778
|
+
|
1779
|
+
|
1780
|
+
class NoteRestToken(ComplexToken):
|
1781
|
+
"""
|
1782
|
+
NoteRestToken class.
|
1783
|
+
|
1784
|
+
Attributes:
|
1785
|
+
pitch_duration_subtokens (list): The subtokens for the pitch and duration
|
1786
|
+
decoration_subtokens (list): The subtokens for the decorations
|
1787
|
+
"""
|
1788
|
+
|
1789
|
+
def __init__(
|
1790
|
+
self,
|
1791
|
+
encoding: str,
|
1792
|
+
pitch_duration_subtokens: List[Subtoken],
|
1793
|
+
decoration_subtokens: List[Subtoken]
|
1794
|
+
):
|
1795
|
+
"""
|
1796
|
+
NoteRestToken constructor.
|
1797
|
+
|
1798
|
+
Args:
|
1799
|
+
encoding (str): The complete unprocessed encoding
|
1800
|
+
pitch_duration_subtokens (List[Subtoken])y: The subtokens for the pitch and duration
|
1801
|
+
decoration_subtokens (List[Subtoken]): The subtokens for the decorations. Individual elements of the token, of type Subtoken
|
1802
|
+
"""
|
1803
|
+
super().__init__(encoding, TokenCategory.NOTE_REST)
|
1804
|
+
if not pitch_duration_subtokens or len(pitch_duration_subtokens) == 0:
|
1805
|
+
raise ValueError('Empty name-duration subtokens')
|
1806
|
+
|
1807
|
+
for subtoken in pitch_duration_subtokens:
|
1808
|
+
if not isinstance(subtoken, Subtoken):
|
1809
|
+
raise ValueError(f'All pitch-duration subtokens must be instances of Subtoken. Found {type(subtoken)}')
|
1810
|
+
for subtoken in decoration_subtokens:
|
1811
|
+
if not isinstance(subtoken, Subtoken):
|
1812
|
+
raise ValueError(f'All decoration subtokens must be instances of Subtoken. Found {type(subtoken)}')
|
1813
|
+
|
1814
|
+
self.pitch_duration_subtokens = pitch_duration_subtokens
|
1815
|
+
self.decoration_subtokens = decoration_subtokens
|
1816
|
+
|
1817
|
+
def export(self, **kwargs) -> str:
|
1818
|
+
"""
|
1819
|
+
Exports the token.
|
1820
|
+
|
1821
|
+
Keyword Arguments:
|
1822
|
+
filter_categories (Optional[Callable[[TokenCategory], bool]]): A function that takes a TokenCategory and returns a boolean
|
1823
|
+
indicating whether the token should be included in the export. If provided, only tokens for which the
|
1824
|
+
function returns True will be exported. Defaults to None. If None, all tokens will be exported.
|
1825
|
+
|
1826
|
+
Returns (str): The exported token.
|
1827
|
+
|
1828
|
+
"""
|
1829
|
+
filter_categories_fn = kwargs.get('filter_categories', None)
|
1830
|
+
|
1831
|
+
# Filter subcategories
|
1832
|
+
pitch_duration_tokens = {
|
1833
|
+
subtoken for subtoken in self.pitch_duration_subtokens
|
1834
|
+
if filter_categories_fn is None or filter_categories_fn(subtoken.category)
|
1835
|
+
}
|
1836
|
+
decoration_tokens = {
|
1837
|
+
subtoken for subtoken in self.decoration_subtokens
|
1838
|
+
if filter_categories_fn is None or filter_categories_fn(subtoken.category)
|
1839
|
+
}
|
1840
|
+
pitch_duration_tokens_sorted = sorted(pitch_duration_tokens, key=lambda t: (t.category.value, t.encoding))
|
1841
|
+
decoration_tokens_sorted = sorted(decoration_tokens, key=lambda t: (t.category.value, t.encoding))
|
1842
|
+
|
1843
|
+
# Join the sorted subtokens
|
1844
|
+
pitch_duration_part = TOKEN_SEPARATOR.join([subtoken.encoding for subtoken in pitch_duration_tokens_sorted])
|
1845
|
+
decoration_part = DECORATION_SEPARATOR.join([subtoken.encoding for subtoken in decoration_tokens_sorted])
|
1846
|
+
|
1847
|
+
result = pitch_duration_part
|
1848
|
+
if len(decoration_part):
|
1849
|
+
result += DECORATION_SEPARATOR + decoration_part
|
1850
|
+
|
1851
|
+
return result if len(result) > 0 else EMPTY_TOKEN
|
1852
|
+
|
1853
|
+
|
1854
|
+
class ChordToken(SimpleToken):
|
1855
|
+
"""
|
1856
|
+
ChordToken class.
|
1857
|
+
|
1858
|
+
It contains a list of compound tokens
|
1859
|
+
"""
|
1860
|
+
|
1861
|
+
def __init__(self,
|
1862
|
+
encoding: str,
|
1863
|
+
category: TokenCategory,
|
1864
|
+
notes_tokens: Sequence[Token]
|
1865
|
+
):
|
1866
|
+
"""
|
1867
|
+
ChordToken constructor.
|
1868
|
+
|
1869
|
+
Args:
|
1870
|
+
encoding (str): The complete unprocessed encoding
|
1871
|
+
category (TokenCategory): The token category, one of TokenCategory
|
1872
|
+
notes_tokens (Sequence[Token]): The subtokens for the notes. Individual elements of the token, of type token
|
1873
|
+
"""
|
1874
|
+
super().__init__(encoding, category)
|
1875
|
+
self.notes_tokens = notes_tokens
|
1876
|
+
|
1877
|
+
def export(self, **kwargs) -> str:
|
1878
|
+
result = ''
|
1879
|
+
for note_token in self.notes_tokens:
|
1880
|
+
if len(result) > 0:
|
1881
|
+
result += ' '
|
1882
|
+
|
1883
|
+
result += note_token.export()
|
1884
|
+
|
1885
|
+
return result
|
1886
|
+
|
1887
|
+
|
1888
|
+
class BoundingBox:
|
1889
|
+
"""
|
1890
|
+
BoundingBox class.
|
1891
|
+
|
1892
|
+
It contains the coordinates of the score bounding box. Useful for full-page tasks.
|
1893
|
+
|
1894
|
+
Attributes:
|
1895
|
+
from_x (int): The x coordinate of the top left corner
|
1896
|
+
from_y (int): The y coordinate of the top left corner
|
1897
|
+
to_x (int): The x coordinate of the bottom right corner
|
1898
|
+
to_y (int): The y coordinate of the bottom right corner
|
1899
|
+
"""
|
1900
|
+
|
1901
|
+
def __init__(self, x, y, w, h):
|
1902
|
+
"""
|
1903
|
+
BoundingBox constructor.
|
1904
|
+
|
1905
|
+
Args:
|
1906
|
+
x (int): The x coordinate of the top left corner
|
1907
|
+
y (int): The y coordinate of the top left corner
|
1908
|
+
w (int): The width
|
1909
|
+
h (int): The height
|
1910
|
+
"""
|
1911
|
+
self.from_x = x
|
1912
|
+
self.from_y = y
|
1913
|
+
self.to_x = x + w
|
1914
|
+
self.to_y = y + h
|
1915
|
+
|
1916
|
+
def w(self) -> int:
|
1917
|
+
"""
|
1918
|
+
Returns the width of the box
|
1919
|
+
|
1920
|
+
Returns:
|
1921
|
+
int: The width of the box
|
1922
|
+
"""
|
1923
|
+
return self.to_x - self.from_x
|
1924
|
+
|
1925
|
+
def h(self) -> int:
|
1926
|
+
"""
|
1927
|
+
Returns the height of the box
|
1928
|
+
|
1929
|
+
Returns:
|
1930
|
+
int: The height of the box
|
1931
|
+
return self.to_y - self.from_y
|
1932
|
+
"""
|
1933
|
+
return self.to_y - self.from_y
|
1934
|
+
|
1935
|
+
def extend(self, bounding_box) -> None:
|
1936
|
+
"""
|
1937
|
+
Extends the bounding box. Modify the current object.
|
1938
|
+
|
1939
|
+
Args:
|
1940
|
+
bounding_box (BoundingBox): The bounding box to extend
|
1941
|
+
|
1942
|
+
Returns:
|
1943
|
+
None
|
1944
|
+
"""
|
1945
|
+
self.from_x = min(self.from_x, bounding_box.from_x)
|
1946
|
+
self.from_y = min(self.from_y, bounding_box.from_y)
|
1947
|
+
self.to_x = max(self.to_x, bounding_box.to_x)
|
1948
|
+
self.to_y = max(self.to_y, bounding_box.to_y)
|
1949
|
+
|
1950
|
+
def __str__(self) -> str:
|
1951
|
+
"""
|
1952
|
+
Returns a string representation of the bounding box
|
1953
|
+
|
1954
|
+
Returns (str): The string representation of the bounding box
|
1955
|
+
"""
|
1956
|
+
return f'(x={self.from_x}, y={self.from_y}, w={self.w()}, h={self.h()})'
|
1957
|
+
|
1958
|
+
def xywh(self) -> str:
|
1959
|
+
"""
|
1960
|
+
Returns a string representation of the bounding box.
|
1961
|
+
|
1962
|
+
Returns:
|
1963
|
+
str: The string representation of the bounding box
|
1964
|
+
"""
|
1965
|
+
return f'{self.from_x},{self.from_y},{self.w()},{self.h()}'
|
1966
|
+
|
1967
|
+
|
1968
|
+
class BoundingBoxToken(Token):
|
1969
|
+
"""
|
1970
|
+
BoundingBoxToken class.
|
1971
|
+
|
1972
|
+
It contains the coordinates of the score bounding box. Useful for full-page tasks.
|
1973
|
+
|
1974
|
+
Attributes:
|
1975
|
+
encoding (str): The complete unprocessed encoding
|
1976
|
+
page_number (int): The page number
|
1977
|
+
bounding_box (BoundingBox): The bounding box
|
1978
|
+
"""
|
1979
|
+
|
1980
|
+
def __init__(
|
1981
|
+
self,
|
1982
|
+
encoding: str,
|
1983
|
+
page_number: int,
|
1984
|
+
bounding_box: BoundingBox
|
1985
|
+
):
|
1986
|
+
"""
|
1987
|
+
BoundingBoxToken constructor.
|
1988
|
+
|
1989
|
+
Args:
|
1990
|
+
encoding (str): The complete unprocessed encoding
|
1991
|
+
page_number (int): The page number
|
1992
|
+
bounding_box (BoundingBox): The bounding box
|
1993
|
+
"""
|
1994
|
+
super().__init__(encoding, TokenCategory.BOUNDING_BOXES)
|
1995
|
+
self.page_number = page_number
|
1996
|
+
self.bounding_box = bounding_box
|
1997
|
+
|
1998
|
+
def export(self, **kwargs) -> str:
|
1999
|
+
return self.encoding
|
2000
|
+
|
2001
|
+
|
2002
|
+
class MHXMToken(Token):
|
2003
|
+
"""
|
2004
|
+
MHXMToken class.
|
2005
|
+
"""
|
2006
|
+
def __init__(self, encoding):
|
2007
|
+
super().__init__(encoding, TokenCategory.MHXM)
|
2008
|
+
|
2009
|
+
# TODO: Implement constructor
|
2010
|
+
def export(self, **kwargs) -> str:
|
2011
|
+
return self.encoding
|