• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

0plus1 / sottovoce / #4

10 Dec 2025 01:17PM UTC coverage: 72.956% (+0.4%) from 72.589%
#4

push

coveralls-python

0plus1
Add speech synthesis

89 of 133 new or added lines in 7 files covered. (66.92%)

6 existing lines in 2 files now uncovered.

232 of 318 relevant lines covered (72.96%)

0.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

28.57
/src/tts_engine.py
1
from __future__ import annotations
1✔
2

3
from pathlib import Path
1✔
4
from typing import Optional
1✔
5

6
import numpy
1✔
7
import sounddevice as sd  # type: ignore[import-not-found]
1✔
8
from piper import PiperVoice, SynthesisConfig  # type: ignore[import-not-found]
1✔
9

10
from src.config import Settings
1✔
11

12

13
class TtsEngine:
1✔
14
    """Thin wrapper around Piper TTS."""
15

16
    def __init__(self, settings: Settings):
1✔
NEW
17
        self.enabled = settings.tts_enabled
×
NEW
18
        self.voice_path = settings.tts_voice_path
×
NEW
19
        self.use_cuda = settings.tts_use_cuda
×
NEW
20
        self.length_scale = settings.tts_length_scale
×
NEW
21
        self.noise_scale = settings.tts_noise_scale
×
NEW
22
        self.noise_w_scale = settings.tts_noise_w_scale
×
NEW
23
        self.volume = settings.tts_volume
×
NEW
24
        self._voice: Optional[PiperVoice] = None
×
25

26
    def _ensure_voice(self) -> None:
1✔
NEW
27
        if self._voice is None:
×
NEW
28
            if not self.voice_path:
×
NEW
29
                raise RuntimeError("TTS voice path is not set. Set TTS_VOICE_PATH to a Piper voice .onnx.")
×
NEW
30
            self._voice = PiperVoice.load(self.voice_path, use_cuda=self.use_cuda)
×
31

32
    def _synth_config(self):
1✔
NEW
33
        assert self._voice is not None
×
NEW
34
        return SynthesisConfig(
×
35
            volume=self.volume,
36
            length_scale=self.length_scale,
37
            noise_scale=self.noise_scale,
38
            noise_w_scale=self.noise_w_scale,
39
        )
40

41
    def synthesize(self, text: str, output_path: Path) -> Path:
1✔
NEW
42
        if not self.enabled:
×
NEW
43
            raise RuntimeError("TTS is disabled")
×
NEW
44
        self._ensure_voice()
×
NEW
45
        assert self._voice is not None
×
NEW
46
        syn_config = self._synth_config()
×
47

48
        # Play back to the user (no disk write)
NEW
49
        chunks = list(self._voice.synthesize(text, syn_config=syn_config))
×
NEW
50
        if chunks:
×
NEW
51
            sample_rate = chunks[0].sample_rate
×
NEW
52
            channels = chunks[0].sample_channels
×
NEW
53
            audio_bytes = b"".join(chunk.audio_int16_bytes for chunk in chunks)
×
NEW
54
            audio = numpy.frombuffer(audio_bytes, dtype=numpy.int16)
×
NEW
55
            if channels > 1:
×
NEW
56
                audio = audio.reshape(-1, channels)
×
NEW
57
            sd.play(audio, sample_rate)
×
NEW
58
            sd.wait()
×
59

NEW
60
        return output_path
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc