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.
@@ -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)
@@ -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
@@ -0,0 +1,12 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class TimeAlignedType:
6
+ start: float
7
+ end: float
8
+ text: str
9
+
10
+ @property
11
+ def duration(self) -> float:
12
+ return self.end - self.start
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
@@ -1,4 +1,3 @@
1
- from .data import PathInfo
2
1
  from .data import TranscriptData
3
2
  from .librispeech import collect_librispeech_transcripts
4
3
  from .librispeech import get_librispeech_manifest_entry
@@ -1,9 +1 @@
1
- from dataclasses import dataclass
2
-
3
1
  TranscriptData = dict[str, str]
4
-
5
-
6
- @dataclass(frozen=True)
7
- class PathInfo:
8
- abs_path: str
9
- audio_filepath: str
@@ -1,4 +1,4 @@
1
- from sonusai.utils.asr_manifest_functions import PathInfo
1
+ from sonusai.utils import PathInfo
2
2
  from sonusai.utils.asr_manifest_functions import TranscriptData
3
3
 
4
4
 
@@ -1,4 +1,4 @@
1
- from sonusai.utils.asr_manifest_functions import PathInfo
1
+ from sonusai.utils import PathInfo
2
2
 
3
3
 
4
4
  def get_mcgill_speech_manifest_entry(entry: PathInfo, transcript_data: list[str]) -> dict:
@@ -1,4 +1,4 @@
1
- from sonusai.utils.asr_manifest_functions import PathInfo
1
+ from sonusai.utils import PathInfo
2
2
  from sonusai.utils.asr_manifest_functions import TranscriptData
3
3
 
4
4
 
@@ -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):