sonusai 0.16.1__py3-none-any.whl → 0.17.2__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.
- sonusai/audiofe.py +33 -27
- sonusai/calc_metric_spenh.py +206 -213
- sonusai/doc/doc.py +1 -1
- sonusai/mixture/__init__.py +2 -0
- sonusai/mixture/audio.py +12 -0
- sonusai/mixture/datatypes.py +11 -3
- sonusai/mixture/mixdb.py +101 -0
- sonusai/mixture/soundfile_audio.py +39 -0
- sonusai/mixture/speaker_metadata.py +35 -0
- sonusai/mixture/torchaudio_audio.py +22 -0
- sonusai/mkmanifest.py +1 -1
- sonusai/onnx_predict.py +129 -171
- sonusai/queries/queries.py +1 -1
- sonusai/speech/__init__.py +3 -0
- sonusai/speech/l2arctic.py +116 -0
- sonusai/speech/librispeech.py +99 -0
- sonusai/speech/mcgill.py +70 -0
- sonusai/speech/textgrid.py +100 -0
- sonusai/speech/timit.py +135 -0
- sonusai/speech/types.py +12 -0
- sonusai/speech/vctk.py +52 -0
- sonusai/speech/voxceleb2.py +86 -0
- sonusai/utils/__init__.py +2 -1
- sonusai/utils/asr_manifest_functions/__init__.py +0 -1
- sonusai/utils/asr_manifest_functions/data.py +0 -8
- sonusai/utils/asr_manifest_functions/librispeech.py +1 -1
- sonusai/utils/asr_manifest_functions/mcgill_speech.py +1 -1
- sonusai/utils/asr_manifest_functions/vctk_noisy_speech.py +1 -1
- sonusai/utils/braced_glob.py +7 -3
- sonusai/utils/onnx_utils.py +142 -49
- sonusai/utils/path_info.py +7 -0
- {sonusai-0.16.1.dist-info → sonusai-0.17.2.dist-info}/METADATA +2 -1
- {sonusai-0.16.1.dist-info → sonusai-0.17.2.dist-info}/RECORD +35 -24
- {sonusai-0.16.1.dist-info → sonusai-0.17.2.dist-info}/WHEEL +0 -0
- {sonusai-0.16.1.dist-info → sonusai-0.17.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,100 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from praatio import textgrid
|
4
|
+
from praatio.utilities.constants import Interval
|
5
|
+
|
6
|
+
from .types import TimeAlignedType
|
7
|
+
|
8
|
+
|
9
|
+
def _get_duration(name: str) -> float:
|
10
|
+
from pydub import AudioSegment
|
11
|
+
|
12
|
+
from sonusai import SonusAIError
|
13
|
+
|
14
|
+
try:
|
15
|
+
return AudioSegment.from_file(name).duration_seconds
|
16
|
+
except Exception as e:
|
17
|
+
raise SonusAIError(f'Error reading {name}: {e}')
|
18
|
+
|
19
|
+
|
20
|
+
def create_textgrid(prompt: Path,
|
21
|
+
speaker_id: str,
|
22
|
+
speaker: dict,
|
23
|
+
output_dir: Path,
|
24
|
+
text: TimeAlignedType = None,
|
25
|
+
words: list[TimeAlignedType] = None,
|
26
|
+
phonemes: list[TimeAlignedType] = None) -> None:
|
27
|
+
if text is not None or words is not None or phonemes is not None:
|
28
|
+
min_t, max_t = _get_min_max({'phonemes': phonemes,
|
29
|
+
'text': [text],
|
30
|
+
'words': words})
|
31
|
+
else:
|
32
|
+
min_t = 0
|
33
|
+
max_t = _get_duration(str(prompt))
|
34
|
+
|
35
|
+
tg = textgrid.Textgrid()
|
36
|
+
|
37
|
+
tg.addTier(textgrid.IntervalTier('speaker_id', [Interval(min_t, max_t, speaker_id)], min_t, max_t))
|
38
|
+
for tier in speaker.keys():
|
39
|
+
tg.addTier(textgrid.IntervalTier(tier, [Interval(min_t, max_t, str(speaker[tier]))], min_t, max_t))
|
40
|
+
|
41
|
+
if text is not None:
|
42
|
+
entries = [Interval(text.start, text.end, text.text)]
|
43
|
+
text_tier = textgrid.IntervalTier('text', entries, min_t, max_t)
|
44
|
+
tg.addTier(text_tier)
|
45
|
+
|
46
|
+
if words is not None:
|
47
|
+
entries = []
|
48
|
+
for word in words:
|
49
|
+
entries.append(Interval(word.start, word.end, word.text))
|
50
|
+
words_tier = textgrid.IntervalTier('words', entries, min_t, max_t)
|
51
|
+
tg.addTier(words_tier)
|
52
|
+
|
53
|
+
if phonemes is not None:
|
54
|
+
entries = []
|
55
|
+
for phoneme in phonemes:
|
56
|
+
entries.append(Interval(phoneme.start, phoneme.end, phoneme.text))
|
57
|
+
phonemes_tier = textgrid.IntervalTier('phonemes', entries, min_t, max_t)
|
58
|
+
tg.addTier(phonemes_tier)
|
59
|
+
|
60
|
+
output_filename = str(output_dir / prompt.stem) + '.TextGrid'
|
61
|
+
tg.save(output_filename, format='long_textgrid', includeBlankSpaces=True)
|
62
|
+
|
63
|
+
|
64
|
+
def _get_min_max(tiers: dict[str, list[TimeAlignedType]]) -> tuple[float, float]:
|
65
|
+
starts = []
|
66
|
+
ends = []
|
67
|
+
for tier in tiers.values():
|
68
|
+
if tier is not None:
|
69
|
+
starts.append(tier[0].start)
|
70
|
+
ends.append(tier[-1].end)
|
71
|
+
|
72
|
+
return min(starts), max(ends)
|
73
|
+
|
74
|
+
|
75
|
+
def annotate_textgrid(tiers: dict[str, list[TimeAlignedType]], prompt: Path, output_dir: Path) -> None:
|
76
|
+
import os
|
77
|
+
|
78
|
+
if tiers is None:
|
79
|
+
return
|
80
|
+
|
81
|
+
file = Path(output_dir / prompt.stem).with_suffix('.TextGrid')
|
82
|
+
if not os.path.exists(file):
|
83
|
+
tg = textgrid.Textgrid()
|
84
|
+
min_t, max_t = _get_min_max(tiers)
|
85
|
+
else:
|
86
|
+
tg = textgrid.openTextgrid(str(file), includeEmptyIntervals=False)
|
87
|
+
min_t = tg.minTimestamp
|
88
|
+
max_t = tg.maxTimestamp
|
89
|
+
|
90
|
+
for tier in tiers.keys():
|
91
|
+
entries = []
|
92
|
+
for entry in tiers[tier]:
|
93
|
+
entries.append(Interval(entry.start, entry.end, entry.text))
|
94
|
+
if tier == 'phones':
|
95
|
+
name = 'annotation_phonemes'
|
96
|
+
else:
|
97
|
+
name = 'annotation_' + tier
|
98
|
+
tg.addTier(textgrid.IntervalTier(name, entries, min_t, max_t))
|
99
|
+
|
100
|
+
tg.save(str(file), format='long_textgrid', includeBlankSpaces=True)
|
sonusai/speech/timit.py
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
import os
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from .types import TimeAlignedType
|
6
|
+
|
7
|
+
|
8
|
+
def load_text(audio: str | os.PathLike[str]) -> Optional[TimeAlignedType]:
|
9
|
+
"""Load time-aligned text data given a TIMIT audio file.
|
10
|
+
|
11
|
+
:param audio: Path to the TIMIT audio file.
|
12
|
+
:return: A TimeAlignedType object.
|
13
|
+
"""
|
14
|
+
import string
|
15
|
+
|
16
|
+
from sonusai.mixture import get_sample_rate
|
17
|
+
|
18
|
+
file = Path(audio).with_suffix('.TXT')
|
19
|
+
if not os.path.exists(file):
|
20
|
+
return None
|
21
|
+
|
22
|
+
sample_rate = get_sample_rate(str(audio))
|
23
|
+
|
24
|
+
with open(file, mode='r', encoding='utf-8') as f:
|
25
|
+
line = f.read()
|
26
|
+
|
27
|
+
fields = line.strip().split()
|
28
|
+
start = int(fields[0]) / sample_rate
|
29
|
+
end = int(fields[1]) / sample_rate
|
30
|
+
text = ' '.join(fields[2:]).lower().translate(str.maketrans('', '', string.punctuation))
|
31
|
+
|
32
|
+
return TimeAlignedType(start, end, text)
|
33
|
+
|
34
|
+
|
35
|
+
def load_words(audio: str | os.PathLike[str]) -> Optional[list[TimeAlignedType]]:
|
36
|
+
"""Load time-aligned word data given a TIMIT audio file.
|
37
|
+
|
38
|
+
:param audio: Path to the TIMIT audio file.
|
39
|
+
:return: A list of TimeAlignedType objects.
|
40
|
+
"""
|
41
|
+
|
42
|
+
return _load_ta(audio, 'words')
|
43
|
+
|
44
|
+
|
45
|
+
def load_phonemes(audio: str | os.PathLike[str]) -> Optional[list[TimeAlignedType]]:
|
46
|
+
"""Load time-aligned phonemes data given a TIMIT audio file.
|
47
|
+
|
48
|
+
:param audio: Path to the TIMIT audio file.
|
49
|
+
:return: A list of TimeAlignedType objects.
|
50
|
+
"""
|
51
|
+
|
52
|
+
return _load_ta(audio, 'phonemes')
|
53
|
+
|
54
|
+
|
55
|
+
def _load_ta(audio: str | os.PathLike[str], tier: str) -> Optional[list[TimeAlignedType]]:
|
56
|
+
from sonusai.mixture import get_sample_rate
|
57
|
+
|
58
|
+
if tier == 'words':
|
59
|
+
file = Path(audio).with_suffix('.WRD')
|
60
|
+
elif tier == 'phonemes':
|
61
|
+
file = Path(audio).with_suffix('.PHN')
|
62
|
+
else:
|
63
|
+
raise ValueError(f'Unknown tier: {tier}')
|
64
|
+
|
65
|
+
if not os.path.exists(file):
|
66
|
+
return None
|
67
|
+
|
68
|
+
sample_rate = get_sample_rate(str(audio))
|
69
|
+
|
70
|
+
entries: list[TimeAlignedType] = []
|
71
|
+
first = True
|
72
|
+
with open(file, mode='r', encoding='utf-8') as f:
|
73
|
+
for line in f.readlines():
|
74
|
+
fields = line.strip().split()
|
75
|
+
start = int(fields[0]) / sample_rate
|
76
|
+
end = int(fields[1]) / sample_rate
|
77
|
+
text = ' '.join(fields[2:])
|
78
|
+
|
79
|
+
if first:
|
80
|
+
first = False
|
81
|
+
else:
|
82
|
+
if start < entries[-1].end:
|
83
|
+
start = entries[-1].end - (entries[- 1].end - start) // 2
|
84
|
+
entries[-1] = TimeAlignedType(text=entries[-1].text, start=entries[-1].start, end=start)
|
85
|
+
|
86
|
+
if end <= start:
|
87
|
+
end = start + 1 / sample_rate
|
88
|
+
|
89
|
+
entries.append(TimeAlignedType(text=text, start=start, end=end))
|
90
|
+
|
91
|
+
return entries
|
92
|
+
|
93
|
+
|
94
|
+
def _years_between(record, born):
|
95
|
+
try:
|
96
|
+
rec_fields = [int(x) for x in record.split('/')]
|
97
|
+
brn_fields = [int(x) for x in born.split('/')]
|
98
|
+
return rec_fields[2] - brn_fields[2] - ((rec_fields[1], rec_fields[0]) < (brn_fields[1], brn_fields[0]))
|
99
|
+
except ValueError:
|
100
|
+
return '??'
|
101
|
+
|
102
|
+
|
103
|
+
def _decode_dialect(d: str) -> str:
|
104
|
+
if d in ['DR1', '1']:
|
105
|
+
return 'New England'
|
106
|
+
if d in ['DR2', '2']:
|
107
|
+
return 'Northern'
|
108
|
+
if d in ['DR3', '3']:
|
109
|
+
return 'North Midland'
|
110
|
+
if d in ['DR4', '4']:
|
111
|
+
return 'South Midland'
|
112
|
+
if d in ['DR5', '5']:
|
113
|
+
return 'Southern'
|
114
|
+
if d in ['DR6', '6']:
|
115
|
+
return 'New York City'
|
116
|
+
if d in ['DR7', '7']:
|
117
|
+
return 'Western'
|
118
|
+
if d in ['DR8', '8']:
|
119
|
+
return 'Army Brat'
|
120
|
+
|
121
|
+
raise ValueError(f'Unrecognized dialect: {d}')
|
122
|
+
|
123
|
+
|
124
|
+
def load_speakers(input_dir: Path) -> dict:
|
125
|
+
speakers = {}
|
126
|
+
with open(input_dir / 'SPKRINFO.TXT') as file:
|
127
|
+
for line in file:
|
128
|
+
if not line.startswith(';'):
|
129
|
+
fields = line.strip().split()
|
130
|
+
speaker_id = fields[0]
|
131
|
+
gender = fields[1]
|
132
|
+
dialect = _decode_dialect(fields[2])
|
133
|
+
age = _years_between(fields[4], fields[5])
|
134
|
+
speakers[speaker_id] = {'gender': gender, 'dialect': dialect, 'age': age}
|
135
|
+
return speakers
|
sonusai/speech/types.py
ADDED
sonusai/speech/vctk.py
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
import os
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from .types import TimeAlignedType
|
6
|
+
|
7
|
+
|
8
|
+
def _get_duration(name: str) -> float:
|
9
|
+
import soundfile
|
10
|
+
|
11
|
+
from sonusai import SonusAIError
|
12
|
+
|
13
|
+
try:
|
14
|
+
return soundfile.info(name).duration
|
15
|
+
except Exception as e:
|
16
|
+
raise SonusAIError(f'Error reading {name}: {e}')
|
17
|
+
|
18
|
+
|
19
|
+
def load_text(audio: str | os.PathLike[str]) -> Optional[TimeAlignedType]:
|
20
|
+
"""Load time-aligned text data given a VCTK audio file.
|
21
|
+
|
22
|
+
:param audio: Path to the VCTK audio file.
|
23
|
+
:return: A TimeAlignedType object.
|
24
|
+
"""
|
25
|
+
import string
|
26
|
+
|
27
|
+
file = Path(audio).parents[2] / 'txt' / Path(audio).parent.name / (Path(audio).stem[:-5] + '.txt')
|
28
|
+
if not os.path.exists(file):
|
29
|
+
return None
|
30
|
+
|
31
|
+
with open(file, mode='r', encoding='utf-8') as f:
|
32
|
+
line = f.read()
|
33
|
+
|
34
|
+
start = 0
|
35
|
+
end = _get_duration(str(audio))
|
36
|
+
text = line.strip().lower().translate(str.maketrans('', '', string.punctuation))
|
37
|
+
|
38
|
+
return TimeAlignedType(start, end, text)
|
39
|
+
|
40
|
+
|
41
|
+
def load_speakers(input_dir: Path) -> dict:
|
42
|
+
speakers = {}
|
43
|
+
with open(input_dir / 'speaker-info.txt') as file:
|
44
|
+
for line in file:
|
45
|
+
if not line.startswith('ID'):
|
46
|
+
fields = line.strip().split('(', 1)[0].split()
|
47
|
+
speaker_id = fields[0]
|
48
|
+
age = fields[1]
|
49
|
+
gender = fields[2]
|
50
|
+
dialect = ' '.join([field for field in fields[3:]])
|
51
|
+
speakers[speaker_id] = {'gender': gender, 'dialect': dialect, 'age': age}
|
52
|
+
return speakers
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import os
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
|
6
|
+
@dataclass(frozen=True)
|
7
|
+
class Segment:
|
8
|
+
person: str
|
9
|
+
video: str
|
10
|
+
id: str
|
11
|
+
start: float
|
12
|
+
stop: float
|
13
|
+
|
14
|
+
|
15
|
+
def load_speakers(input_dir: Path) -> dict:
|
16
|
+
import csv
|
17
|
+
|
18
|
+
speakers = {}
|
19
|
+
first = True
|
20
|
+
with open(input_dir / 'vox2_meta_cleansed.csv', newline='') as file:
|
21
|
+
data = csv.reader(file)
|
22
|
+
for row in data:
|
23
|
+
if first:
|
24
|
+
first = False
|
25
|
+
else:
|
26
|
+
speakers[row[0].strip()] = {'gender': row[2].strip(), 'category': row[3].strip()}
|
27
|
+
return speakers
|
28
|
+
|
29
|
+
|
30
|
+
def load_segment(path: str | os.PathLike[str]) -> Segment:
|
31
|
+
path = Path(path)
|
32
|
+
|
33
|
+
with path.open('r') as file:
|
34
|
+
segment = file.read().strip()
|
35
|
+
|
36
|
+
header, frames = segment.split('\n\n')
|
37
|
+
header_dict = _parse_header(header)
|
38
|
+
start, stop = _get_segment_boundaries(frames)
|
39
|
+
|
40
|
+
return Segment(person=header_dict['Identity'],
|
41
|
+
video=header_dict['Reference'],
|
42
|
+
id=path.stem,
|
43
|
+
start=start,
|
44
|
+
stop=stop)
|
45
|
+
|
46
|
+
|
47
|
+
def _parse_header(header: str) -> dict:
|
48
|
+
def _parse_line(line: str) -> tuple[str, str]:
|
49
|
+
"""Parse a line of header text into a dictionary.
|
50
|
+
|
51
|
+
Header text has the following format:
|
52
|
+
|
53
|
+
Identity : \tid00017
|
54
|
+
Reference : \t7t6lfzvVaTM
|
55
|
+
Offset : \t1
|
56
|
+
FV Conf : \t16.647\t(1)
|
57
|
+
ASD Conf : \t4.465
|
58
|
+
|
59
|
+
"""
|
60
|
+
k, v = line.split('\t', maxsplit=1)
|
61
|
+
k = k[:-2].strip()
|
62
|
+
v = v.strip()
|
63
|
+
return k, v
|
64
|
+
|
65
|
+
return dict(_parse_line(line) for line in header.split('\n'))
|
66
|
+
|
67
|
+
|
68
|
+
def _get_segment_boundaries(frames: str) -> tuple[float, float]:
|
69
|
+
"""Get the start and stop points of the segment.
|
70
|
+
|
71
|
+
Frames text has the following format:
|
72
|
+
|
73
|
+
FRAME X Y W H
|
74
|
+
000245 0.392 0.223 0.253 0.451
|
75
|
+
...
|
76
|
+
000470 0.359 0.207 0.260 0.463
|
77
|
+
|
78
|
+
"""
|
79
|
+
|
80
|
+
def _get_frame_seconds(line: str) -> float:
|
81
|
+
frame = int(line.split('\t')[0])
|
82
|
+
# YouTube is 25 FPS
|
83
|
+
return frame / 25
|
84
|
+
|
85
|
+
lines = frames.split('\n')
|
86
|
+
return _get_frame_seconds(lines[1]), _get_frame_seconds(lines[-1])
|
sonusai/utils/__init__.py
CHANGED
@@ -27,11 +27,12 @@ from .max_text_width import max_text_width
|
|
27
27
|
from .model_utils import import_module
|
28
28
|
from .numeric_conversion import float_to_int16
|
29
29
|
from .numeric_conversion import int16_to_float
|
30
|
-
from .onnx_utils import SonusAIMetaData
|
31
30
|
from .onnx_utils import add_sonusai_metadata
|
32
31
|
from .onnx_utils import get_sonusai_metadata
|
32
|
+
from .onnx_utils import load_ort_session
|
33
33
|
from .parallel import pp_imap
|
34
34
|
from .parallel import pp_tqdm_imap
|
35
|
+
from .path_info import PathInfo
|
35
36
|
from .print_mixture_details import print_class_count
|
36
37
|
from .print_mixture_details import print_mixture_details
|
37
38
|
from .ranges import consolidate_range
|
sonusai/utils/braced_glob.py
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
from typing import Generator
|
2
|
+
from typing import LiteralString
|
2
3
|
from typing import Optional
|
3
4
|
from typing import Set
|
4
5
|
|
5
6
|
|
6
|
-
def expand_braces(text: str, seen: Optional[Set[str]] = None) -> Generator[str, None, None]:
|
7
|
+
def expand_braces(text: LiteralString | str | bytes, seen: Optional[Set[str]] = None) -> Generator[str, None, None]:
|
7
8
|
"""Brace-expansion pre-processing for glob.
|
8
9
|
|
9
10
|
Expand all the braces, then run glob on each of the results.
|
@@ -16,6 +17,9 @@ def expand_braces(text: str, seen: Optional[Set[str]] = None) -> Generator[str,
|
|
16
17
|
if seen is None:
|
17
18
|
seen = set()
|
18
19
|
|
20
|
+
if not isinstance(text, str):
|
21
|
+
text = str(text)
|
22
|
+
|
19
23
|
spans = [m.span() for m in re.finditer(r'\{[^{}]*}', text)][::-1]
|
20
24
|
alts = [text[start + 1: stop - 1].split(',') for start, stop in spans]
|
21
25
|
|
@@ -31,7 +35,7 @@ def expand_braces(text: str, seen: Optional[Set[str]] = None) -> Generator[str,
|
|
31
35
|
yield from expand_braces(''.join(replaced), seen)
|
32
36
|
|
33
37
|
|
34
|
-
def braced_glob(pathname: str, recursive: bool = False) -> list[str]:
|
38
|
+
def braced_glob(pathname: LiteralString | str | bytes, recursive: bool = False) -> list[str]:
|
35
39
|
from glob import glob
|
36
40
|
|
37
41
|
result = []
|
@@ -41,7 +45,7 @@ def braced_glob(pathname: str, recursive: bool = False) -> list[str]:
|
|
41
45
|
return result
|
42
46
|
|
43
47
|
|
44
|
-
def braced_iglob(pathname: str, recursive: bool = False) -> Generator[str, None, None]:
|
48
|
+
def braced_iglob(pathname: LiteralString | str | bytes, recursive: bool = False) -> Generator[str, None, None]:
|
45
49
|
from glob import iglob
|
46
50
|
|
47
51
|
for expanded_path in expand_braces(pathname):
|