mirror of
https://github.com/slackr31337/wyoming-piper-gpu.git
synced 2026-10-20 01:24:23 +02:00
Updates to enable CUDA in piper
This commit is contained in:
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"CUDA",
|
||||
"onnxruntime",
|
||||
"setuptools",
|
||||
"zxvf"
|
||||
]
|
||||
}
|
||||
+22
-8
@@ -6,41 +6,55 @@ WORKDIR /usr/src
|
||||
ARG TARGETARCH=amd64
|
||||
ARG TARGETVARIANT=
|
||||
ARG PIPER_LIB_VERSION=1.4.0
|
||||
ARG PIPER_RELEASE=v1.2.0
|
||||
ARG WYOMING_PIPER_VERSION='1.5.0'
|
||||
ARG PIPER_RELEASE='1.2.0'
|
||||
|
||||
RUN \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
wget \
|
||||
curl \
|
||||
python3 \
|
||||
python3-pip \
|
||||
\
|
||||
&& pip3 install --no-cache-dir -U \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN \
|
||||
pip3 install --no-cache-dir -U \
|
||||
setuptools \
|
||||
wheel \
|
||||
\
|
||||
&& pip3 install --no-cache-dir torch \
|
||||
\
|
||||
&& pip3 install --no-cache-dir \
|
||||
"wyoming-piper==${PIPER_LIB_VERSION}" \
|
||||
&& wget https://github.com/rhasspy/piper-phonemize/releases/download/v1.1.0/piper_phonemize-1.1.0-cp39-cp39-manylinux_2_28_x86_64.whl \
|
||||
\
|
||||
&& curl -L -s \
|
||||
"https://github.com/rhasspy/piper/releases/download/${PIPER_RELEASE}/piper_${TARGETARCH}${TARGETVARIANT}.tar.gz"|tar -zxvf - -C /usr/share \
|
||||
&& mv piper_phonemize-1.1.0-cp310-cp310-manylinux_2_28_x86_64.whl piper_phonemize-1.1.0-py3-none-any.whl \
|
||||
\
|
||||
&& pip3 install --no-cache-dir --force-reinstall --no-deps \
|
||||
"piper-tts==${PIPER_RELEASE}" \
|
||||
\
|
||||
&& pip3 install --no-cache-dir --force-reinstall --no-deps \
|
||||
piper_phonemize-1.1.0-py3-none-any.whl \
|
||||
\
|
||||
&& pip3 install --no-cache-dir \
|
||||
onnxruntime-gpu \
|
||||
"wyoming-piper @ https://github.com/rhasspy/wyoming-piper/archive/refs/tags/v${WYOMING_PIPER_VERSION}.tar.gz" \
|
||||
\
|
||||
&& rm -r piper_phonemize-1.1.0-py3-none-any.whl \
|
||||
\
|
||||
&& apt-get purge -y --auto-remove \
|
||||
build-essential \
|
||||
python3-dev \
|
||||
\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Patch process.py to pass --cuda argument to piper
|
||||
# Patch to enable CUDA in piper
|
||||
COPY patch/process.py /usr/local/lib/python3.10/dist-packages/wyoming_piper/
|
||||
COPY patch/__main__.py /usr/local/lib/python3.10/dist-packages/wyoming_piper/
|
||||
|
||||
WORKDIR /
|
||||
COPY run.sh ./
|
||||
|
||||
EXPOSE 10200
|
||||
|
||||
ENTRYPOINT ["bash", "/run.sh"]
|
||||
ENTRYPOINT ["bash", "-c", "exec run.sh \"${@}\"", "--"]
|
||||
@@ -1,5 +1,5 @@
|
||||
# wyoming-piper-gpu
|
||||
Wyoming Piper docker container with GPU support for Home-Assistant
|
||||
Wyoming Piper docker container with Nvidia GPU support for Home-Assistant
|
||||
|
||||
https://github.com/rhasspy/wyoming-piper
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Set
|
||||
|
||||
from wyoming.info import Attribution, Info, TtsProgram, TtsVoice, TtsVoiceSpeaker
|
||||
from wyoming.server import AsyncServer
|
||||
|
||||
from . import __version__
|
||||
from .download import find_voice, get_voices
|
||||
from .handler import PiperEventHandler
|
||||
from .process import PiperProcessManager
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--piper",
|
||||
required=True,
|
||||
help="Path to piper executable",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--voice",
|
||||
required=True,
|
||||
help="Default Piper voice to use (e.g., en_US-lessac-medium)",
|
||||
)
|
||||
parser.add_argument("--uri", default="stdio://", help="unix:// or tcp://")
|
||||
parser.add_argument(
|
||||
"--data-dir",
|
||||
required=True,
|
||||
action="append",
|
||||
help="Data directory to check for downloaded models",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--download-dir",
|
||||
help="Directory to download voices into (default: first data dir)",
|
||||
)
|
||||
#
|
||||
parser.add_argument(
|
||||
"--speaker", type=str, help="Name or id of speaker for default voice"
|
||||
)
|
||||
parser.add_argument("--noise-scale", type=float, help="Generator noise")
|
||||
parser.add_argument("--length-scale", type=float, help="Phoneme length")
|
||||
parser.add_argument("--noise-w", type=float, help="Phoneme width noise")
|
||||
#
|
||||
parser.add_argument(
|
||||
"--auto-punctuation", default=".?!", help="Automatically add punctuation"
|
||||
)
|
||||
parser.add_argument("--samples-per-chunk", type=int, default=1024)
|
||||
parser.add_argument(
|
||||
"--max-piper-procs",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Maximum number of piper process to run simultaneously (default: 1)",
|
||||
)
|
||||
#
|
||||
parser.add_argument(
|
||||
"--update-voices",
|
||||
action="store_true",
|
||||
help="Download latest voices.json during startup",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cuda",
|
||||
action="store_true",
|
||||
help="Use GPU"
|
||||
)
|
||||
#
|
||||
parser.add_argument("--debug", action="store_true", help="Log DEBUG messages")
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version=__version__,
|
||||
help="Print version and exit",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.download_dir:
|
||||
# Default to first data directory
|
||||
args.download_dir = args.data_dir[0]
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
|
||||
_LOGGER.debug(args)
|
||||
|
||||
# Load voice info
|
||||
voices_info = get_voices(args.download_dir, update_voices=args.update_voices)
|
||||
|
||||
# Resolve aliases for backwards compatibility with old voice names
|
||||
aliases_info: Dict[str, Any] = {}
|
||||
for voice_info in voices_info.values():
|
||||
for voice_alias in voice_info.get("aliases", []):
|
||||
aliases_info[voice_alias] = {"_is_alias": True, **voice_info}
|
||||
|
||||
voices_info.update(aliases_info)
|
||||
voices = [
|
||||
TtsVoice(
|
||||
name=voice_name,
|
||||
description=get_description(voice_info),
|
||||
attribution=Attribution(
|
||||
name="rhasspy", url="https://github.com/rhasspy/piper"
|
||||
),
|
||||
installed=True,
|
||||
version=None,
|
||||
languages=[
|
||||
voice_info.get("language", {}).get(
|
||||
"code",
|
||||
voice_info.get("espeak", {}).get("voice", voice_name.split("_")[0]),
|
||||
)
|
||||
],
|
||||
speakers=[
|
||||
TtsVoiceSpeaker(name=speaker_name)
|
||||
for speaker_name in voice_info["speaker_id_map"]
|
||||
]
|
||||
if voice_info.get("speaker_id_map")
|
||||
else None,
|
||||
)
|
||||
for voice_name, voice_info in voices_info.items()
|
||||
if not voice_info.get("_is_alias", False)
|
||||
]
|
||||
|
||||
custom_voice_names: Set[str] = set()
|
||||
if args.voice not in voices_info:
|
||||
custom_voice_names.add(args.voice)
|
||||
|
||||
for data_dir in args.data_dir:
|
||||
data_dir = Path(data_dir)
|
||||
if not data_dir.is_dir():
|
||||
continue
|
||||
|
||||
for onnx_path in data_dir.glob("*.onnx"):
|
||||
custom_voice_name = onnx_path.stem
|
||||
if custom_voice_name not in voices_info:
|
||||
custom_voice_names.add(custom_voice_name)
|
||||
|
||||
for custom_voice_name in custom_voice_names:
|
||||
# Add custom voice info
|
||||
custom_voice_path, custom_config_path = find_voice(
|
||||
custom_voice_name, args.data_dir
|
||||
)
|
||||
with open(custom_config_path, "r", encoding="utf-8") as custom_config_file:
|
||||
custom_config = json.load(custom_config_file)
|
||||
custom_name = custom_config.get("dataset", custom_voice_path.stem)
|
||||
custom_quality = custom_config.get("audio", {}).get("quality")
|
||||
if custom_quality:
|
||||
description = f"{custom_name} ({custom_quality})"
|
||||
else:
|
||||
description = custom_name
|
||||
|
||||
lang_code = custom_config.get("language", {}).get("code")
|
||||
if not lang_code:
|
||||
lang_code = custom_config.get("espeak", {}).get("voice")
|
||||
if not lang_code:
|
||||
lang_code = custom_voice_path.stem.split("_")[0]
|
||||
|
||||
voices.append(
|
||||
TtsVoice(
|
||||
name=custom_name,
|
||||
description=description,
|
||||
version=None,
|
||||
attribution=Attribution(name="", url=""),
|
||||
installed=True,
|
||||
languages=[lang_code],
|
||||
)
|
||||
)
|
||||
|
||||
wyoming_info = Info(
|
||||
tts=[
|
||||
TtsProgram(
|
||||
name="piper",
|
||||
description="A fast, local, neural text to speech engine",
|
||||
attribution=Attribution(
|
||||
name="rhasspy", url="https://github.com/rhasspy/piper"
|
||||
),
|
||||
installed=True,
|
||||
voices=sorted(voices, key=lambda v: v.name),
|
||||
version=__version__,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
process_manager = PiperProcessManager(args, voices_info)
|
||||
|
||||
# Make sure default voice is loaded.
|
||||
# Other voices will be loaded on-demand.
|
||||
await process_manager.get_process()
|
||||
|
||||
# Start server
|
||||
server = AsyncServer.from_uri(args.uri)
|
||||
|
||||
_LOGGER.info("Ready")
|
||||
await server.run(
|
||||
partial(
|
||||
PiperEventHandler,
|
||||
wyoming_info,
|
||||
args,
|
||||
process_manager,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_description(voice_info: Dict[str, Any]):
|
||||
"""Get a human readable description for a voice."""
|
||||
name = voice_info["name"]
|
||||
name = " ".join(name.split("_"))
|
||||
quality = voice_info["quality"]
|
||||
|
||||
return f"{name} ({quality})"
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def run():
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
+3
-2
@@ -131,7 +131,6 @@ class PiperProcessManager:
|
||||
"--output_dir",
|
||||
str(wav_dir.name),
|
||||
"--json-input", # piper 1.1+
|
||||
"--cuda",
|
||||
]
|
||||
|
||||
if voice_speaker is not None:
|
||||
@@ -149,6 +148,8 @@ class PiperProcessManager:
|
||||
if self.args.noise_w:
|
||||
piper_args.extend(["--noise-w", str(self.args.noise_w)])
|
||||
|
||||
if self.args.cuda:
|
||||
piper_args.extend(["--cuda"])
|
||||
|
||||
_LOGGER.debug(
|
||||
"Starting piper process: %s args=%s", self.args.piper, piper_args
|
||||
@@ -170,4 +171,4 @@ class PiperProcessManager:
|
||||
# Update used
|
||||
piper_proc.last_used = time.monotonic_ns()
|
||||
|
||||
return piper_proc
|
||||
return piper_proc
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
diff --git a/wyoming_piper/__main__.py b/wyoming_piper/__main__.py
|
||||
index 08b20f1..f88312b 100755
|
||||
--- a/wyoming_piper/__main__.py
|
||||
+++ b/wyoming_piper/__main__.py
|
||||
@@ -77,6 +77,15 @@ async def main() -> None:
|
||||
version=__version__,
|
||||
help="Print version and exit",
|
||||
)
|
||||
+ parser.add_argument(
|
||||
+ "--cuda",
|
||||
+ action="store_true",
|
||||
+ help="Use GPU"
|
||||
+ )
|
||||
+ parser.add_argument(
|
||||
+ "--espeak-data-dir",
|
||||
+ help="Path to espeak-ng data directory"
|
||||
+ )
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.download_dir:
|
||||
diff --git a/wyoming_piper/process.py b/wyoming_piper/process.py
|
||||
index a7e90a7..98b0712 100644
|
||||
--- a/wyoming_piper/process.py
|
||||
+++ b/wyoming_piper/process.py
|
||||
@@ -148,6 +148,14 @@ class PiperProcessManager:
|
||||
if self.args.noise_w:
|
||||
piper_args.extend(["--noise-w", str(self.args.noise_w)])
|
||||
|
||||
+ if self.args.espeak_data_dir:
|
||||
+ piper_args.extend(["--espeak_data", str(self.args.espeak_data_dir)])
|
||||
+
|
||||
+ if self.args.cuda:
|
||||
+ piper_args.extend(["--debug"])
|
||||
+ piper_args.extend(["--cuda"])
|
||||
+ piper_args.extend(["--use-cuda"])
|
||||
+
|
||||
_LOGGER.debug(
|
||||
"Starting piper process: %s args=%s", self.args.piper, piper_args
|
||||
)
|
||||
Reference in New Issue
Block a user