Skip to content

Commit 1ca4988

Browse files
authored
Merge pull request #10 from NeuroDevCo/ruff-ty
Use ruff and ty
2 parents 5344be9 + b5aed32 commit 1ca4988

5 files changed

Lines changed: 74 additions & 21 deletions

File tree

.github/workflows/publish.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: "Publish"
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
run:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
id-token: write
13+
contents: read
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v6
17+
- name: Install uv
18+
uses: astral-sh/setup-uv@v7
19+
- name: "Set up Python"
20+
uses: actions/setup-python@v6
21+
with:
22+
python-version-file: ".python-version"
23+
- name: Build
24+
run: uv build
25+
- name: Smoke test (wheel)
26+
run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
27+
- name: Smoke test (source distribution)
28+
run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
29+
- name: Publish
30+
run: uv publish

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,6 @@ voices/
168168
/**/*.tar
169169
/.ruff_cache
170170
/.benchmarks
171+
*.wav
172+
*.pho
173+
src/mbrola.egg-info/

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "mbrola"
3-
version = "0.2.1"
3+
version = "0.2.2"
44
description = 'A Python front-end for the MBROLA speech synthesizer'
55
readme = "README.md"
66
requires-python = ">=3.12"
@@ -11,6 +11,7 @@ authors = [
1111
]
1212
classifiers = [
1313
"Development Status :: 4 - Beta",
14+
"Operating System :: POSIX :: Linux",
1415
"Programming Language :: Python",
1516
"Programming Language :: Python :: 3.8",
1617
"Programming Language :: Python :: 3.9",
@@ -20,6 +21,7 @@ classifiers = [
2021
"Programming Language :: Python :: Implementation :: CPython",
2122
"Programming Language :: Python :: Implementation :: PyPy",
2223
]
24+
2325
dependencies = [
2426
"pytest>=8.3.5",
2527
]

src/mbrola.egg-info/PKG-INFO

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Project-URL: Documentation, https://github.com/gongcastro/pymbrola#readme
88
Project-URL: Issues, https://github.com/gongcastro/pymbrola/issues
99
Project-URL: Source, https://github.com/gongcastro/pymbrola
1010
Classifier: Development Status :: 4 - Beta
11+
Classifier: Operating System :: POSIX :: Linux
1112
Classifier: Programming Language :: Python
1213
Classifier: Programming Language :: Python :: 3.8
1314
Classifier: Programming Language :: Python :: 3.9

src/mbrola.py

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
References:
55
Dutoit, T., Pagel, V., Pierret, N., Bataille, F., & Van der Vrecken, O. (1996, October). The MBROLA project: Towards a set of high quality speech synthesizers free of use for non commercial purposes. In Proceeding of Fourth International Conference on Spoken Language Processing. ICSLP'96 (Vol. 3, pp. 1393-1396). IEEE. https://doi.org/10.1109/ICSLP.1996.607874
6-
""" # pylint: disable=line-too-long
6+
"""
77

88
import os
99
from pathlib import Path
@@ -32,7 +32,7 @@ def validate_word(
3232
3333
Returns:
3434
str: validated word.
35-
""" # pylint: disable=line-too-long
35+
"""
3636
if len(word) > max_chars:
3737
raise ValueError(f"`word` exceeds maximum characters ({max_chars})")
3838
if re.search(invalid_chars, word):
@@ -58,7 +58,7 @@ def validate_durations(
5858
5959
Returns:
6060
list[int]: Phoneme durations.
61-
""" # pylint: disable=line-too-long
61+
"""
6262
raise TypeError(
6363
f"`durations` must be int or list, but {type(durations)} was provided"
6464
)
@@ -153,24 +153,24 @@ class MBROLA:
153153
Attributes:
154154
word (str): label for the mbrola sound.
155155
phon (Sequence[str]): list of phonemes.
156-
durations (list[int] | int, optional): phoneme duration in milliseconds. Defaults to 100. If an integer is provided, all phonemes in ``phon`` are assumed to be the same length. If a list is provided, each element in the list indicates the duration of each phoneme.
157-
pitch (list[int] | int, optional): pitch in Hertz (Hz). Defaults to 200. If an integer is provided, the pitch contour of each phoneme is assumed to be constant at the indicated value. If a list of integers or strings is provided, each element in the list indicates the value at which the pitch contour of each phoneme is kept constant. If a list of lists (of integers or strings), each value in each element describes the pitch contour for each phoneme.
158-
outer_silences (tuple[int, int], optional): duration in milliseconds of the silence interval to be inserted at onset and offset. Defaults to (1, 1).
156+
durations (Sequence[int] | int, optional): phoneme duration in milliseconds. Defaults to 100. If an integer is provided, all phonemes in ``phon`` are assumed to be the same length. If a list is provided, each element in the list indicates the duration of each phoneme.
157+
pitch (int | Sequence[int] | Sequence[int | Sequence[int]], optional): pitch in Hertz (Hz). Defaults to 200. If an integer is provided, the pitch contour of each phoneme is assumed to be constant at the indicated value. If a list of integers or strings is provided, each element in the list indicates the value at which the pitch contour of each phoneme is kept constant. If a list of lists (of integers or strings), each value in each element describes the pitch contour for each phoneme.
158+
outer_silences (Sequence[int, int], optional): duration in milliseconds of the silence interval to be inserted at onset and offset. Defaults to (1, 1).
159159
Examples:
160160
>>> house = mb.MBROLA(
161161
word = "house",
162162
phonemes = ["h", "a", "U", "s"],
163163
durations = "100",
164164
pitch = [200, [200, 50, 200], 200, 100]
165165
)
166-
""" # pylint: disable=line-too-long
166+
"""
167167

168168
def __init__(
169169
self,
170170
word: str,
171171
phon: str | Sequence[str],
172172
durations: int | Sequence[int] = 100,
173-
pitch: int | Sequence[int] = 200,
173+
pitch: int | Sequence[int] | Sequence[int | Sequence[int]] = 200,
174174
outer_silences: Sequence[int] = (1, 1),
175175
):
176176
self.word = validate_word(word)
@@ -197,13 +197,13 @@ def __add__(self, other, sep="_"):
197197
self.phon = self.phon + other.phon
198198
self.pho = self.pho + other
199199

200-
def export_pho(self, file: str) -> None:
200+
def export_pho(self, file: str | Path) -> None:
201201
"""Save PHO file.
202202
203203
Args:
204204
file (str): Path of the output PHO file.
205205
"""
206-
with open(f"{file}", "w+", encoding="utf-8") as f:
206+
with Path(file).open("w+", encoding="utf-8") as f:
207207
f.write("\n".join(self.pho))
208208

209209
def make_sound(
@@ -224,19 +224,23 @@ def make_sound(
224224
remove_pho (bool, optional): Should the intermediate PHO file be deleted after the sound is created? Defaults to True.
225225
"""
226226
pho = Path("tmp.pho")
227-
with open(pho, mode="w", encoding="utf-8") as f:
227+
228+
with Path(pho).open(mode="w", encoding="utf-8") as f:
228229
f.write("\n".join(self.pho))
229-
cmd_str = f"{mbrola_cmd()} -f {f0_ratio} -t {dur_ratio} /usr/share/mbrola/{voice}/{voice} {pho} {Path(file)}"
230+
231+
cmd_str = f"{mbrola_cmd()} -f {f0_ratio} -t {dur_ratio} /usr/share/mbrola/{voice}/{voice} {pho} {str(Path(file))}"
232+
230233
try:
231234
sp.check_output(cmd_str, shell=True)
232235
except sp.CalledProcessError as e:
233236
print(f"Error when making sound for {file}: {e}")
237+
234238
f.close()
235239
if remove_pho:
236240
pho.unlink()
237241

238242

239-
def make_pho(x) -> list[str]:
243+
def make_pho(x: MBROLA) -> list[str]:
240244
"""Generate PHO file.
241245
242246
A PHO (.pho) file contains the phonological information of the speech sound in a format that MBROLA can read. See more examples in the MBROLA documentation (https://github.com/numediart/MBROLA).
@@ -246,16 +250,21 @@ def make_pho(x) -> list[str]:
246250
247251
Raises:
248252
TypeError: if ``x`` is not a MBROLA object.
253+
249254
Returns:
250255
list[str]: Lines in the PHO file.
251256
"""
252257
if not isinstance(x, MBROLA):
253258
raise TypeError("`x` must be an instance of MBROLA class")
259+
254260
pho = [f"; {x.word}", f"_ {x.outer_silences[0]}"]
261+
255262
for ph, d, p in zip(x.phon, x.durations, x.pitch):
256263
p_seq = " ".join([str(pi) for pi in p])
257264
pho.append(" ".join(map(str, [ph, d, p_seq])))
265+
258266
pho.append(f"_ {x.outer_silences[1]}")
267+
259268
return pho
260269

261270

@@ -267,21 +276,24 @@ class PlatformException(Exception):
267276
"""
268277

269278
def __init__(self):
270-
self.message = f"MBROLA is only available on {platform.system()} using the Windows Subsystem for Linux (WSL).\nPlease, follow the instructions in the WSL site: https://learn.microsoft.com/en-us/windows/wsl/install." # pylint: disable=line-too-long
279+
self.message = f"MBROLA is only available on {platform.system()} using the Windows Subsystem for Linux (WSL).\nPlease, follow the instructions in the WSL site: https://learn.microsoft.com/en-us/windows/wsl/install."
271280
super().__init__(self.message)
272281

273282

274283
@cache
275284
def mbrola_cmd():
276285
"""
277286
Get MBROLA command for system command line.
278-
""" # pylint: disable=line-too-long
287+
"""
279288
try:
280289
if is_wsl() or os.name == "posix":
281290
return "mbrola"
291+
282292
if os.name == "nt" and wsl_available():
283293
return "wsl mbrola"
284-
raise PlatformException
294+
295+
raise PlatformException()
296+
285297
except PlatformException:
286298
return None
287299

@@ -292,18 +304,23 @@ def is_wsl(version: str = platform.uname().release) -> bool:
292304
293305
Returns:
294306
bool: returns ``True`` if Python is running in WSL, otherwise ``False``.
295-
""" # pylint: disable=line-too-long
307+
"""
296308
return version.endswith("microsoft-standard-WSL2")
297309

298310

299311
@cache
300-
def wsl_available() -> int:
312+
def wsl_available() -> bool | int:
313+
"""
314+
Check if Windows Subsystem for Linux (WSL is available).
315+
316+
Returns:
317+
bool | int: ``True` if Windows Subsystem for Linux (WLS) is available from Windows, otherwise ``False``
301318
"""
302-
Returns ``True` if Windows Subsystem for Linux (WLS) is available from Windows, otherwise ``False``
303-
""" # pylint: disable=line-too-long
304319
if os.name != "nt" or not shutil.which("wsl"):
305320
return False
321+
306322
cmd = partial(sp.check_output, timeout=5, encoding="UTF-8", text=True)
323+
307324
try:
308325
return is_wsl(cmd(["wsl", "uname", "-r"]).strip())
309326
except sp.SubprocessError:

0 commit comments

Comments
 (0)