drugi commit
This commit is contained in:
+188
@@ -0,0 +1,188 @@
|
|||||||
|
# SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
# Prerequisites
|
||||||
|
*.d
|
||||||
|
|
||||||
|
# Compiled Object files
|
||||||
|
*.slo
|
||||||
|
*.lo
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
|
||||||
|
# Precompiled Headers
|
||||||
|
*.gch
|
||||||
|
*.pch
|
||||||
|
|
||||||
|
# Compiled Dynamic libraries
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
|
||||||
|
# Fortran module files
|
||||||
|
*.mod
|
||||||
|
*.smod
|
||||||
|
|
||||||
|
# Compiled Static libraries
|
||||||
|
*.lai
|
||||||
|
*.la
|
||||||
|
*.a
|
||||||
|
*.lib
|
||||||
|
|
||||||
|
# Executables
|
||||||
|
*.exe
|
||||||
|
*.out
|
||||||
|
*.app
|
||||||
|
|
||||||
|
# Models
|
||||||
|
*.pt
|
||||||
|
*.savedmodel
|
||||||
|
install/
|
||||||
|
|
||||||
|
# Ignore backup files.
|
||||||
|
*~
|
||||||
|
# Ignore Vim swap files.
|
||||||
|
.*.swp
|
||||||
|
# Ignore files generated by IDEs.
|
||||||
|
/.classpath
|
||||||
|
/.factorypath
|
||||||
|
/.idea/
|
||||||
|
/.ijwb/
|
||||||
|
/.project
|
||||||
|
/.settings
|
||||||
|
/.vscode/
|
||||||
|
# Ignore outputs generated during Bazel bootstrapping.
|
||||||
|
/output/
|
||||||
|
# Ignore jekyll build output.
|
||||||
|
/production
|
||||||
|
/.sass-cache
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
tests/integration/asr/outputs
|
||||||
|
tests/integration/nlp/outputs
|
||||||
|
tests/integration/tts/outputs
|
||||||
|
|
||||||
|
riva/client/proto/*_pb2.py
|
||||||
|
riva/client/proto/*_pb2_grpc.py
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
touch README.md
|
||||||
|
git init
|
||||||
|
git checkout -b main
|
||||||
|
git add README.md
|
||||||
|
git commit -m "first commit"
|
||||||
|
git remote add origin https://gitea.sic.pl/ms/nvidia-chatterbox.git
|
||||||
|
git push -u origin main
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
python3 python-clients/scripts/tts/talk.py --server 0.0.0.0:50051 \
|
||||||
|
--language-code pl-PL \
|
||||||
|
--voice Chatterbox-Multilingual.pl-PL.Male \
|
||||||
|
--text "Jesień to czas, kiedy przyroda zwalnia tempo. Dni stają się coraz krótsze, a liście na drzewach zmieniają swoje kolory na żółte, czerwone i brązowe. W chłodniejsze wieczory przyjemnie jest usiąść w fotelu z kubkiem gorącej herbaty i dobrą książką. To także idealny moment na długie spacery po parku i zbieranie kolorowych kasztanów." \
|
||||||
|
--stream \
|
||||||
|
--output output_polish.wav
|
||||||
|
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Convert a long text file to MP3 using the existing Riva TTS client.
|
||||||
|
|
||||||
|
The script splits input text into smaller chunks, synthesizes each chunk with
|
||||||
|
`python-clients/scripts/tts/talk.py`, joins all WAV chunks, and finally converts
|
||||||
|
the result to MP3 using ffmpeg.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import wave
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Convert a long TXT file to MP3 using NVIDIA Riva TTS.",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
)
|
||||||
|
parser.add_argument("--input", type=Path, required=True, help="Path to input .txt file.")
|
||||||
|
parser.add_argument("--output", type=Path, required=True, help="Path to output .mp3 file.")
|
||||||
|
parser.add_argument("--server", default="0.0.0.0:50051", help="Riva server address.")
|
||||||
|
parser.add_argument("--language-code", default="pl-PL", help="Language code for TTS.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--voice",
|
||||||
|
default="Chatterbox-Multilingual.pl-PL.Male",
|
||||||
|
help="Riva voice name.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--talk-script",
|
||||||
|
type=Path,
|
||||||
|
default=Path("python-clients/scripts/tts/talk.py"),
|
||||||
|
help="Path to existing talk.py script.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-chars",
|
||||||
|
type=int,
|
||||||
|
default=700,
|
||||||
|
help="Maximum characters per TTS chunk.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--sample-rate-hz",
|
||||||
|
type=int,
|
||||||
|
default=22050,
|
||||||
|
help="Output sample rate used by talk.py.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--keep-wav",
|
||||||
|
action="store_true",
|
||||||
|
help="Keep final merged WAV next to MP3.",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_whitespace(text: str) -> str:
|
||||||
|
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
||||||
|
lines = [line.strip() for line in text.split("\n")]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def split_sentence_if_needed(sentence: str, max_chars: int) -> List[str]:
|
||||||
|
if len(sentence) <= max_chars:
|
||||||
|
return [sentence]
|
||||||
|
|
||||||
|
words = sentence.split()
|
||||||
|
parts: List[str] = []
|
||||||
|
current = ""
|
||||||
|
for word in words:
|
||||||
|
candidate = f"{current} {word}".strip() if current else word
|
||||||
|
if len(candidate) <= max_chars:
|
||||||
|
current = candidate
|
||||||
|
else:
|
||||||
|
if current:
|
||||||
|
parts.append(current)
|
||||||
|
# Single very long token fallback.
|
||||||
|
if len(word) > max_chars:
|
||||||
|
for i in range(0, len(word), max_chars):
|
||||||
|
parts.append(word[i : i + max_chars])
|
||||||
|
current = ""
|
||||||
|
else:
|
||||||
|
current = word
|
||||||
|
if current:
|
||||||
|
parts.append(current)
|
||||||
|
return parts
|
||||||
|
|
||||||
|
|
||||||
|
def chunk_text(text: str, max_chars: int) -> List[str]:
|
||||||
|
paragraphs = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]
|
||||||
|
sentence_splitter = re.compile(r"(?<=[.!?…])\s+")
|
||||||
|
|
||||||
|
chunks: List[str] = []
|
||||||
|
current = ""
|
||||||
|
|
||||||
|
for paragraph in paragraphs:
|
||||||
|
sentences = [s.strip() for s in sentence_splitter.split(paragraph) if s.strip()]
|
||||||
|
expanded_sentences: List[str] = []
|
||||||
|
for s in sentences:
|
||||||
|
expanded_sentences.extend(split_sentence_if_needed(s, max_chars))
|
||||||
|
|
||||||
|
for sentence in expanded_sentences:
|
||||||
|
candidate = f"{current} {sentence}".strip() if current else sentence
|
||||||
|
if len(candidate) <= max_chars:
|
||||||
|
current = candidate
|
||||||
|
else:
|
||||||
|
if current:
|
||||||
|
chunks.append(current)
|
||||||
|
current = sentence
|
||||||
|
|
||||||
|
if current:
|
||||||
|
chunks.append(current)
|
||||||
|
current = ""
|
||||||
|
|
||||||
|
if current:
|
||||||
|
chunks.append(current)
|
||||||
|
|
||||||
|
if not chunks:
|
||||||
|
raise ValueError("Input text is empty after preprocessing.")
|
||||||
|
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
|
||||||
|
def synthesize_chunk(
|
||||||
|
talk_script: Path,
|
||||||
|
server: str,
|
||||||
|
language_code: str,
|
||||||
|
voice: str,
|
||||||
|
sample_rate_hz: int,
|
||||||
|
text: str,
|
||||||
|
output_wav: Path,
|
||||||
|
) -> None:
|
||||||
|
cmd = [
|
||||||
|
sys.executable,
|
||||||
|
str(talk_script),
|
||||||
|
"--server",
|
||||||
|
server,
|
||||||
|
"--language-code",
|
||||||
|
language_code,
|
||||||
|
"--voice",
|
||||||
|
voice,
|
||||||
|
"--text",
|
||||||
|
text,
|
||||||
|
"--stream",
|
||||||
|
"--sample-rate-hz",
|
||||||
|
str(sample_rate_hz),
|
||||||
|
"--output",
|
||||||
|
str(output_wav),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
"TTS synthesis failed.\n"
|
||||||
|
f"Command: {' '.join(cmd)}\n"
|
||||||
|
f"stdout:\n{result.stdout}\n"
|
||||||
|
f"stderr:\n{result.stderr}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def merge_wav_files(wav_files: List[Path], output_wav: Path) -> None:
|
||||||
|
if not wav_files:
|
||||||
|
raise ValueError("No WAV files to merge.")
|
||||||
|
|
||||||
|
with wave.open(str(wav_files[0]), "rb") as first:
|
||||||
|
params = first.getparams()
|
||||||
|
frames = [first.readframes(first.getnframes())]
|
||||||
|
|
||||||
|
for wav_path in wav_files[1:]:
|
||||||
|
with wave.open(str(wav_path), "rb") as wf:
|
||||||
|
if (
|
||||||
|
wf.getnchannels() != params.nchannels
|
||||||
|
or wf.getsampwidth() != params.sampwidth
|
||||||
|
or wf.getframerate() != params.framerate
|
||||||
|
or wf.getcomptype() != params.comptype
|
||||||
|
):
|
||||||
|
raise RuntimeError(f"Incompatible WAV params in {wav_path}")
|
||||||
|
frames.append(wf.readframes(wf.getnframes()))
|
||||||
|
|
||||||
|
with wave.open(str(output_wav), "wb") as out:
|
||||||
|
out.setparams(params)
|
||||||
|
for chunk in frames:
|
||||||
|
out.writeframes(chunk)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_wav_to_mp3(input_wav: Path, output_mp3: Path) -> None:
|
||||||
|
ffmpeg = shutil.which("ffmpeg")
|
||||||
|
if ffmpeg is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"ffmpeg not found in PATH. Install ffmpeg to generate MP3 output."
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
ffmpeg,
|
||||||
|
"-y",
|
||||||
|
"-i",
|
||||||
|
str(input_wav),
|
||||||
|
"-codec:a",
|
||||||
|
"libmp3lame",
|
||||||
|
"-q:a",
|
||||||
|
"2",
|
||||||
|
str(output_mp3),
|
||||||
|
]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
"MP3 conversion failed.\n"
|
||||||
|
f"Command: {' '.join(cmd)}\n"
|
||||||
|
f"stdout:\n{result.stdout}\n"
|
||||||
|
f"stderr:\n{result.stderr}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
if args.max_chars < 50:
|
||||||
|
raise ValueError("--max-chars should be at least 50.")
|
||||||
|
|
||||||
|
input_path = args.input.expanduser().resolve()
|
||||||
|
output_mp3 = args.output.expanduser().resolve()
|
||||||
|
talk_script = args.talk_script.expanduser().resolve()
|
||||||
|
|
||||||
|
if not input_path.is_file():
|
||||||
|
raise FileNotFoundError(f"Input file not found: {input_path}")
|
||||||
|
if not talk_script.is_file():
|
||||||
|
raise FileNotFoundError(f"talk.py not found: {talk_script}")
|
||||||
|
|
||||||
|
output_mp3.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
merged_wav = output_mp3.with_suffix(".wav")
|
||||||
|
|
||||||
|
text = input_path.read_text(encoding="utf-8")
|
||||||
|
text = normalize_whitespace(text)
|
||||||
|
chunks = chunk_text(text, args.max_chars)
|
||||||
|
|
||||||
|
print(f"Input split into {len(chunks)} chunks (max {args.max_chars} chars each).")
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="tts_chunks_") as tmp_dir:
|
||||||
|
tmp_path = Path(tmp_dir)
|
||||||
|
wav_paths: List[Path] = []
|
||||||
|
|
||||||
|
for idx, chunk in enumerate(chunks, start=1):
|
||||||
|
chunk_wav = tmp_path / f"chunk_{idx:05d}.wav"
|
||||||
|
print(f"[{idx}/{len(chunks)}] Synthesizing chunk...")
|
||||||
|
synthesize_chunk(
|
||||||
|
talk_script=talk_script,
|
||||||
|
server=args.server,
|
||||||
|
language_code=args.language_code,
|
||||||
|
voice=args.voice,
|
||||||
|
sample_rate_hz=args.sample_rate_hz,
|
||||||
|
text=chunk,
|
||||||
|
output_wav=chunk_wav,
|
||||||
|
)
|
||||||
|
wav_paths.append(chunk_wav)
|
||||||
|
|
||||||
|
print("Merging WAV chunks...")
|
||||||
|
merge_wav_files(wav_paths, merged_wav)
|
||||||
|
|
||||||
|
print("Converting WAV to MP3...")
|
||||||
|
convert_wav_to_mp3(merged_wav, output_mp3)
|
||||||
|
|
||||||
|
if not args.keep_wav and merged_wav.exists():
|
||||||
|
merged_wav.unlink()
|
||||||
|
|
||||||
|
print(f"Done: {output_mp3}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except Exception as exc: # pragma: no cover
|
||||||
|
print(f"Error: {exc}", file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
Reference in New Issue
Block a user