Generowanie mowy z tekstu lokalnie z użyciem pipera

Generowanie mowy z tekstu (TTS) nie musi wymagać usług chmurowych ani stałego połączenia z internetem. W tym artykule pokażę, jak uruchomić lokalne przetwarzanie głosu przy użyciu narzędzia piper oraz jak dodatkowo obrobić dźwięk za pomocą ffmpeg i sox, aby uzyskać lepszą jakość i elastyczność.

Piper

Piper to lekki, otwartoźródłowy silnik Text-to-Speech (TTS) oparty na modelach eSpeak NG i VITS, zaprojektowany do działania lokalnego, bez potrzeby korzystania z chmury. Obsługuje wiele języków (w tym polski), pozwala wybierać różne głosy i generować mowę w czasie rzeczywistym lub do pliku. Dzięki niewielkim wymaganiom sprzętowym działa nawet na urządzeniach typu Raspberry Pi, a przy odpowiednim GPU potrafi generować mowę bardzo szybko. Idealny do projektów, w których liczy się prywatność i pełna kontrola nad danymi.

Pipera pobierzemy z githuba i zainstalujmy z użyciem pip.

git clone https://github.com/OHF-Voice/piper1-gpl
cd piper1-gpl
pip install -e .

Aby móc z niego korzystać potrzebujemy głosów z konfiguracją. W oficjalnym repozytorium są same głosy, ale bez odpowiedniego pliku json niestety nie podziałamy. Głosy znajdziemy na Hugging Face.

Również możemy je ściągnąć, albo pobrać korzystając z pip i huggingface_hub.

pip install -U huggingface_hub
hf download rhasspy/piper-voices/pl --local-dir voices --repo-type dataset --include "*/model.onnx" --include "*/model.onnx.json" --include "*/espeak-ng-data/*"

Wrzućmy je do katalogu z piperem. Możemy teraz generować tekst na podstawie głosu. Odpalamy cmd i wpisujemy.

piper -m pl_PL-gosia-medium.onnx -f voice.wav -t "Cześć, polecam stronę eskim.pl"

W parametrze -m podajemy link do głosu, -f nazwę pliku, która powstanie, a w -t tekst, który ma być mówiony.

Może być konieczne doinstalowanie biblioteki onnxruntime i Visual Studio Build Tools z modułem „Desktop development with C++”.

pip install onnxruntime

ffmpeg

FFmpeg — to zestaw narzędzi wiersza poleceń do konwersji, obróbki i analizy audio/wideo. W praktyce dostajesz trzy binarki: ffmpeg (przetwarzanie), ffprobe (metadane) i ffplay (podgląd). Do pracy z TTS z Pipera sprawdza się do: konwersji WAV→MP3/OGG, resamplingu (np. 22.05 kHz, mono), normalizacji głośności, przycinania/ciszy, fade-in/out oraz łączenia klipów.

Instalację ffmpeg możemy przeprowadzić przez Chocolatey lub pobrać z oficjalnej strony i zainstalować. Do pobrania Chocloatey wpisujemy:

@"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "[System.Net.ServicePointManager]::SecurityProtocol = 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"

lub przez PowerShell (z włączeniem polityki uruchamiania):

Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

Następnie instalujemy ffmpeg

choco install ffmpeg

Po co nam ffmpeg?

Piper na samym początku generuje niechciane dźwięki. To tzw. rozgrzewka systemu głosowego. W przypadku języka polskiego są to słowa „ta te”. Można je oczywiście obciąć w programie później, ale można też z użyciem np. ffmpeg.

ffmpeg -y -i voice.wav -af atrim=start=0.18,asetpts=N/SR/TB,afade=t=in:ss=0:d=0.01 voice-cut.wav

ffmpeg pozwala także np. na konwersję na format mp3

ffmpeg asetpts=N/SR/TB,afade=t=in:ss=0:d=0.01

Poza tym ma również mnóstwo filtrów do normalizacji, poprawiania dźwięku, albo łączenia wielu plików dźwiękowych w jeden większy.

Można też wygenerować np. plik dźwiękowy, który da nam ciszę przez określony czas np. 0.2 sekundy.

ffmpeg -y -f lavfi -t 0.2 -i anullsrc=r=48000:cl=mono silence.wav  

Modyfikacja dźwięku z sox

SoX (Sound eXchange) to narzędzie wiersza poleceń do obróbki audio: konwersji formatów, resamplingu, przycinania, łączenia, zmiany tempa bez zmiany tonu (WSOLA), pitchu, filtrów (high/low-pass, EQ) i prostych efektów. Idealne jako szybki „procesor” TTS przed publikacją lub przed dalszą obróbką w FFmpeg.

Przy użyciu SOX możemy obniżyć lub podnieść ton głosu, spowolnić go czy też np. zniekształcić.

sox OUTPUT_FILE OUTPUT_FILE_DEEP pitch -400 speed 0.9 reverb 10 10 100 overdrive 10 0

Generowanie dużego bloku tekstu

Można utworzyć długi tekst przy użyciu samego pipera, ale jeżeli chcemy mieć większe spacje lub zmianę tonacji dźwięku to należy użyć wspomnianych narzędzi.

Do tego celu można utworzyć wiele plików dźwiękowych i połączyć je razem (oddzielając ciszą). Wcześniej jednak musimy usunąć generowany dźwięk „ta te”, który jest na początku każdego stworzonego pliku dźwiękowego.

Do wygenerowania pliku z ciszą użyjemy ffmpeg.

ffmpeg -y -f lavfi -t 0.2 -i anullsrc=r=48000:cl=mono silence.wav  

Do łączenia plików itp. możemy użyć powershell-a, albo np. pythona.

Poniższy tworzy i łączy wiele plików dźwiękowych oddzielając jest wygenerowaną ciszą (plik silence.wav). Dodatkowo przed każdym tekstem dodawana jest kropka ze spacją, aby łatwiej przewidzieć długość dodatkowego głosu „ta te” i móc go wyciąć.

Tekst do przetworzenia umieszczamy w pliku text.txt, a ciszę uzyskujemy dodając znaki entera – im więcej tym dłuższa.

import subprocess
import os
import shutil

# --- Ustawienia ---
MODEL = "pl_PL-mc_speech-medium.onnx"
SILENCE_FILE = "silence.wav"  # przygotowane wcześniej (np. 1 sekunda ciszy)
OUTPUT_FILE = "output-raw.wav"
OUTPUT_FILE_DEEP = "output.wav"
TEXT_FILE = "text.txt"  # źródłowy plik z tekstem
# -------------------

with open(TEXT_FILE, "r", encoding="utf-8") as f:
    TEXT = f.read()

tmp_dir = "_tmp_audio"
os.makedirs(tmp_dir, exist_ok=True)
parts = []

lines = TEXT.split("\n")
for line in lines:
    if line.strip():
        wav_path = os.path.join(tmp_dir, f"part_{len(parts)}.wav")
        raw_path = os.path.join(tmp_dir, "raw.wav")
        
        safe_text = ". " + line  # lekki „rozruch”
        # 1. Piper TTS
        subprocess.run([
            "piper", "-m", MODEL, "-f", raw_path, "-t", safe_text
        ], check=True)

        # 2. Przycięcie początku ffmpeg
        subprocess.run([
            "ffmpeg", "-y", "-i", raw_path,
            "-af", "atrim=start=0.6,asetpts=N/SR/TB,afade=t=in:ss=0:d=0.01",
            wav_path
        ], check=True)

        parts.append(wav_path)

    else:
        # Pauza
        parts.append(SILENCE_FILE)

# --- Tworzenie listy do concat ---
list_file = os.path.join(tmp_dir, "list.txt")
with open(list_file, "w", encoding="utf-8") as f:
    for p in parts:
        f.write(f"file '{os.path.abspath(p)}'\n")

# --- Łączenie ---
subprocess.run([
    "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file,
    "-c:a", "pcm_s16le", OUTPUT_FILE
], check=True)

subprocess.run([
    "sox", OUTPUT_FILE, OUTPUT_FILE_DEEP, "pitch", "-400", "speed", "0.9", "reverb", "10", "10", "100", "overdrive", "20", "0"
], check=True)

# --- Sprzątanie ---
shutil.rmtree(tmp_dir)

print(f"Gotowe: {OUTPUT_FILE}")