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