Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ MaxEmptyLinesToKeep: 2
SortIncludes: true
CompactNamespaces: false
ReflowComments: true
FixNamespaceComments: false

DerivePointerAlignment: false
PointerAlignment: Left
Expand Down
13 changes: 7 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,37 +19,38 @@ repos:
hooks:
- id: check-added-large-files
args: ['--maxkb=512']
exclude: '^(test_files/|src/tts/tokenizer/mandarin_data/)'
exclude: '^(test_files/|src/tts/tokenizer/mandarin_data/|native/tts-normalizer/testdata/en-US/(tokenize_and_classify|verbalize)\.far$)'
- id: check-yaml
- id: check-merge-conflict
- id: end-of-file-fixer
exclude: '^(patches/|proto/riva-common/).*'
exclude: '^(patches/|proto/riva-common/|deploy/nemotron_native/kernels/|native/tts-normalizer/|python/nemotron_native_backend/|deploy/nemotron_native/(convert_model|serve)\.py$|scripts/nemotron_native/(build_kernels|build_package|runtime_dependencies)\.py$)'
- id: trailing-whitespace
exclude: '^(patches/|proto/riva-common/).*'
exclude: '^(patches/|proto/riva-common/|deploy/nemotron_native/kernels/|native/tts-normalizer/|python/nemotron_native_backend/|deploy/nemotron_native/(convert_model|serve)\.py$|scripts/nemotron_native/(build_kernels|build_package|runtime_dependencies)\.py$)'
- id: mixed-line-ending
args: ['--fix=lf']
exclude: '^(patches/|proto/riva-common/).*'
exclude: '^(patches/|proto/riva-common/|deploy/nemotron_native/kernels/|native/tts-normalizer/|python/nemotron_native_backend/|deploy/nemotron_native/(convert_model|serve)\.py$|scripts/nemotron_native/(build_kernels|build_package|runtime_dependencies)\.py$)'

- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v18.1.8
hooks:
- id: clang-format
types_or: [c++, c, cuda]
# llama.cpp patches + vendored riva protos are upstream code — don't reformat.
exclude: '^(patches/|proto/riva-common/|build/|build-.*/).*'
exclude: '^(patches/|proto/riva-common/|build/|build-.*/|deploy/nemotron_native/kernels/|native/tts-normalizer/)'

- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: black
args: ['--skip-string-normalization', '--line-length=100']
exclude: '^(proto/riva-common/|build/).*'
exclude: '^(proto/riva-common/|build/|python/nemotron_native_backend/|native/tts-normalizer/|deploy/nemotron_native/kernels/|deploy/nemotron_native/(convert_model|serve)\.py$|scripts/nemotron_native/(build_kernels|build_package|runtime_dependencies)\.py$)'

- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
name: isort (python)
exclude: '^(python/nemotron_native_backend/|native/tts-normalizer/|deploy/nemotron_native/kernels/|deploy/nemotron_native/(convert_model|serve)\.py$|scripts/nemotron_native/(build_kernels|build_package|runtime_dependencies)\.py$)'
args:
- '--profile=black'
- '--line-length=100'
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
| Speaker diarization | [Streaming Sortformer 4-speaker v2](https://huggingface.co/nvidia/diar_streaming_sortformer_4spk-v2) and [Nemotron 3 Diarization](https://huggingface.co/nvidia/Nemotron-3-Diarization), standalone or combined with ASR |
| Text and speech translation | [Riva Translate 4B Instruct v2](https://huggingface.co/nvidia/Riva-Translate-4B-Instruct-v2), with composed ASR-to-NMT-to-TTS speech translation |
| Speech synthesis | [MagpieTTS Multilingual 357M](https://huggingface.co/nvidia/magpie_tts_multilingual_357m) with [NeMo NanoCodec](https://huggingface.co/nvidia/nemo-nano-codec-22khz-1.89kbps-21.5fps) |
| Nemotron-TTS megakernels | Optional H100 backend with fixed cohorts or in-flight admission; [integration and benchmarks](docs/nemotron-megakernel.md) |
| Full-duplex voicechat | [Nemotron Labs VoiceChat](https://huggingface.co/nvidia/NVIDIA-NemotronLabs-VoiceChat-11B), including realtime audio, transcripts, and tool calling |
| Speech processing | [Silero VAD](https://github.com/snakers4/silero-vad), punctuation and capitalization, endpointing, text normalization, and subtitles |

Expand Down
47 changes: 46 additions & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ include each selected dependency's license.

### Container system libraries

The Linux container copies the shared libraries required by its executables
The default Linux container copies the shared libraries required by its executables
from the CUDA/Ubuntu builder into a `FROM scratch` runtime. This includes the
GNU C Library, GCC runtime libraries, c-ares, zlib, bzip2, and XZ Utils/liblzma,
subject to the exact feature set and package revisions in the built image.
Expand All @@ -142,6 +142,51 @@ applicable agreement is installed with the archive under

## Other incorporated third-party code and data

### Imported Nemotron native deployment closure

The retained deployment code in `deploy/nemotron_native/`, the Python controller
in `python/nemotron_native_backend/`, the native normalizer in
`native/tts-normalizer/`, and their imported build utilities retain their original
license terms and file-level notices. Importing these components does not
relicense them under this repository's Apache-2.0 license. Apache-2.0 headers on
new integration files do not change the terms of the imported code or its
dependencies.

The legacy NVIDIA notice is preserved in
[`deploy/nemotron_native/LICENSE`](deploy/nemotron_native/LICENSE) and
[`native/tts-normalizer/LICENSE.legacy`](native/tts-normalizer/LICENSE.legacy).
The normalizer also preserves the Riva source notice in
[`native/tts-normalizer/LICENSE.riva`](native/tts-normalizer/LICENSE.riva).
The processor ABI and media-schema sources used by the native preparation
library retain the same legacy terms in
[`native/text_preparation/LICENSE.imported`](native/text_preparation/LICENSE.imported),
with their source revision recorded in
[`native/text_preparation/NOTICE`](native/text_preparation/NOTICE). The library
pins Hugging Face tokenizers 0.21.4 and its transitive dependencies through
`native/text_preparation/Cargo.lock`; their upstream licenses also apply.
These notices reserve NVIDIA's proprietary rights and require an express
license agreement for use, reproduction, disclosure, or distribution. Licensing
and redistribution review of the imported closure is pending; this notice and
a successful build do not grant publication or redistribution permission.

The imported kernel dependencies include two ThunderKittens snapshots,
Copyright (c) 2024-2026 HazyResearch, under the MIT License. Their complete
notices remain in
[`thunderkittens/LICENSE`](deploy/nemotron_native/kernels/third_party/thunderkittens/LICENSE)
and
[`thunderkittens-v4-prefill/LICENSE`](deploy/nemotron_native/kernels/third_party/thunderkittens-v4-prefill/LICENSE).
The normalizer's pinned OpenFst, Sparrowhawk, Thrax, Abseil, protobuf, RE2,
and other dependencies are listed separately in
[`native/tts-normalizer/THIRD_PARTY_NOTICES.md`](native/tts-normalizer/THIRD_PARTY_NOTICES.md).
Their individual license terms remain applicable.

The optional `docker/Dockerfile.nemotron` runtime uses the pinned NVIDIA PyTorch
container rather than the default scratch image described above. Its CUDA,
PyTorch, Python packages, and operating-system dependencies retain their own
licenses and notices. Model weights, tokenizer files, voice context, and
production normalization grammars are supplied separately; this repository does
not grant rights to those assets.

### miniaudio

- Source: [`mackron/miniaudio`](https://github.com/mackron/miniaudio), version
Expand Down
4 changes: 4 additions & 0 deletions app/bench_tts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ class TtsWorkload : public Workload {
texts_.push_back(value());
} else if (arg == "--text-file") {
text_files_.push_back(value());
} else if (arg == "--tts-backend") {
parsed_.backend = value();
} else if (arg == "--native-socket") {
parsed_.native_socket = value();
} else if (arg == "--magpie-model" || arg == "--model" || arg == "-m") {
parsed_.runtime.magpie_model = value();
} else if (arg == "--codec-model") {
Expand Down
46 changes: 38 additions & 8 deletions app/serve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ validate_compiled_feature_options(int argc, char** argv) {
add(option, "NEMO_SPEECH_BUILD_NMT=ON");
#endif
#if !defined(NEMO_SPEECH_CLI_TTS)
if (option == "--tts-model" || option == "--codec-model" || option == "--tokenizer-dir" ||
if (option == "--tts-backend" || option == "--native-socket" || option == "--tts-model" ||
option == "--codec-model" || option == "--tokenizer-dir" ||
option == "--tn-model-dir" || option.rfind("--tts.", 0) == 0)
add(option, "NEMO_SPEECH_BUILD_TTS=ON");
#endif
Expand Down Expand Up @@ -304,7 +305,7 @@ run_server(int argc, char** argv) {
server_config.address = value(i, arg);
else if (arg == "--port")
server_config.port = parse_int(value(i, arg), arg, 1, 65535);
else if (arg == "--threads") {
else if (arg == "--threads" || arg == "--http-threads") {
server_config.threads = parse_int(value(i, arg), arg, 1, 1024);
http_threads_set = true;
} else if (arg == "--max-upload-mb")
Expand Down Expand Up @@ -365,6 +366,10 @@ run_server(int argc, char** argv) {
nmt_model = value(i, arg);
#endif
#if defined(NEMO_SPEECH_CLI_TTS)
else if (arg == "--tts-backend")
tts_config.backend = value(i, arg);
else if (arg == "--native-socket")
tts_config.native_socket = value(i, arg);
else if (arg == "--tts-model")
tts_model = value(i, arg);
else if (arg == "--codec-model")
Expand Down Expand Up @@ -482,15 +487,19 @@ run_server(int argc, char** argv) {
}
#endif
#if defined(NEMO_SPEECH_CLI_TTS)
const bool native_tts = tts_enabled != 0 && tts_config.backend == "nemotron";
if (tts_config.backend != "magpie" && tts_config.backend != "nemotron")
throw std::invalid_argument("tts.backend must be magpie or nemotron");
const bool tts_requested = tts_enabled > 0 || !tts_model.empty() ||
!tts_config.runtime.magpie_model.empty() || !codec_model.empty() ||
!tokenizer_model.empty() || !tn_model.empty();
const auto magpie_path = tts_enabled == 0 ? std::string() : resolve("TTS model", [&] {
return optional_model(
tts_model.empty() ? tts_config.runtime.magpie_model : tts_model, "tts",
"MagpieTTS model", tts_requested);
});
if (tts_enabled != 0 && (tts_requested || !magpie_path.empty())) {
const auto magpie_path =
(tts_enabled == 0 || native_tts) ? std::string() : resolve("TTS model", [&] {
return optional_model(
tts_model.empty() ? tts_config.runtime.magpie_model : tts_model, "tts",
"MagpieTTS model", tts_requested);
});
if (!native_tts && tts_enabled != 0 && (tts_requested || !magpie_path.empty())) {
tts_config.runtime.codec_model = resolve("TTS codec model", [&] {
return optional_model(
codec_model.empty() ? tts_config.runtime.codec_model : codec_model, "codec",
Expand Down Expand Up @@ -579,6 +588,25 @@ run_server(int argc, char** argv) {
}
#endif
#if defined(NEMO_SPEECH_CLI_TTS)
if (native_tts) {
nemo_speech::tts::SynthesizerConfig config;
config.backend = "nemotron";
config.runtime = tts_config.runtime;
config.native_socket = tts_config.native_socket;
config.native_timeout_seconds = tts_config.native_timeout_seconds;
config.default_language_code = tts_config.default_language_code;
config.default_voice_name = tts_config.default_voice_name;
engines.load_tts(std::move(config));
const int needed = engines.tts()->backend_batch_size() + 8;
if (!http_threads_set)
server_config.threads = std::max(server_config.threads, needed);
if (engines.tts()->backend_mode() == "fixed_cohort" &&
server_config.threads < engines.tts()->backend_batch_size())
throw std::invalid_argument(
"fixed cohorts require at least physical-width HTTP workers");
if (server_config.preempt_tts)
throw std::invalid_argument("Nemotron batching does not support tts.preempt");
}
if (!magpie_path.empty()) {
bool tts_cuda = false;
#if defined(NEMO_SPEECH_CLI_CUDA)
Expand Down Expand Up @@ -735,6 +763,8 @@ print_serve_help(const char* program) {
" --diar-model MODEL Optional Sortformer path or indexed model\n"
#endif
#if defined(NEMO_SPEECH_CLI_TTS)
" --tts-backend NAME magpie (default) or nemotron\n"
" --native-socket PATH Nemotron controller bridge Unix socket\n"
" --tts-model MODEL Optional MagpieTTS path or indexed model\n"
" --codec-model MODEL NanoCodec path or indexed model\n"
" --tokenizer-dir MODEL TTS tokenizer directory or indexed model\n"
Expand Down
17 changes: 17 additions & 0 deletions app/synthesize.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ nemo_speech::tts::SynthesizerConfig
make_synthesizer_config(
nemo_speech::tts::MagpieTtsServerConfig parsed, const std::string& device_name,
bool device_set) {
if (parsed.backend == "nemotron") {
nemo_speech::tts::SynthesizerConfig config;
config.backend = parsed.backend;
config.native_socket = parsed.native_socket;
config.native_timeout_seconds = parsed.native_timeout_seconds;
config.default_language_code = parsed.default_language_code;
config.default_voice_name = parsed.default_voice_name;
return config;
Comment on lines +65 to +72

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Configured seeds are ignored

The Nemotron branch drops parsed.runtime. For example, synthesize "Hello" --tts-backend nemotron --native-socket /tmp/nemotron.sock --tts.seed 42 accepts the setting but sends seed 0, because the new configuration keeps the default seed. Benchmarks reuse this helper and can report a seed that the worker did not use. Copy parsed.runtime into config.runtime before returning, as the serving path already does.

Suggested change
if (parsed.backend == "nemotron") {
nemo_speech::tts::SynthesizerConfig config;
config.backend = parsed.backend;
config.native_socket = parsed.native_socket;
config.native_timeout_seconds = parsed.native_timeout_seconds;
config.default_language_code = parsed.default_language_code;
config.default_voice_name = parsed.default_voice_name;
return config;
if (parsed.backend == "nemotron") {
nemo_speech::tts::SynthesizerConfig config;
config.backend = parsed.backend;
config.runtime = parsed.runtime;
config.native_socket = parsed.native_socket;
config.native_timeout_seconds = parsed.native_timeout_seconds;
config.default_language_code = parsed.default_language_code;
config.default_voice_name = parsed.default_voice_name;
return config;

}
if (parsed.backend != "magpie")
throw std::invalid_argument("tts.backend must be magpie or nemotron");
parsed.runtime.magpie_model =
resolve_model_file(parsed.runtime.magpie_model, "tts", "MagpieTTS model").string();
parsed.runtime.codec_model =
Expand Down Expand Up @@ -110,6 +121,8 @@ print_synthesize_help(const char* program) {
std::printf(
"Usage: %s synthesize TEXT [options]\n\n"
"Options:\n"
" --tts-backend NAME magpie (default) or nemotron\n"
" --native-socket PATH Nemotron controller bridge Unix socket\n"
" --magpie-model MODEL MagpieTTS GGUF path or indexed HF repo\n"
" (default: nvidia/magpie_tts_multilingual_357m)\n"
" --codec-model MODEL NanoCodec GGUF path or indexed HF repo\n"
Expand Down Expand Up @@ -173,6 +186,10 @@ command_synthesize(int argc, char** argv) {
const std::string arg = argv[i];
if (arg == "--config")
++i;
else if (arg == "--tts-backend")
parsed.backend = value(i, arg);
else if (arg == "--native-socket")
parsed.native_socket = value(i, arg);
else if (arg == "--magpie-model")
parsed.runtime.magpie_model = value(i, arg);
else if (arg == "--codec-model")
Expand Down
7 changes: 7 additions & 0 deletions deploy/nemotron_native/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright (c) 2020-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved.

NVIDIA CORPORATION and its licensors retain all intellectual property
and proprietary rights in and to this software, related documentation
and any modifications thereto. Any use, reproduction, disclosure or
distribution of this software and related documentation without an express
license agreement from NVIDIA CORPORATION is strictly prohibited.
129 changes: 129 additions & 0 deletions deploy/nemotron_native/convert_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import argparse
import hashlib
import json
import os
import shlex
import subprocess
import sys
from pathlib import Path

LOCK = Path(__file__).resolve().parent / "model.lock.json"


def sha256(path):
digest = hashlib.sha256()
with open(path, "rb") as stream:
for block in iter(lambda: stream.read(16 * 1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()


def verify(root, rows, label):
for row in rows:
path = root / row["path"]
if not path.is_file():
raise SystemExit(f"missing {label}: {path}")
if "bytes" in row and path.stat().st_size != row["bytes"]:
raise SystemExit(f"{label} size mismatch: {path}")
if "sha256" in row and sha256(path) != row["sha256"]:
raise SystemExit(f"{label} sha256 mismatch: {path}")


def download(lock, inputs):
source = lock["inputs"]
if not os.environ.get(source["token_environment_variable"]):
raise SystemExit(
f"set {source['token_environment_variable']} to download {source['repository']}"
)
argv = ["hf", "download", source["repository"], "--repo-type", "model"]
argv += ["--revision", source["revision"], "--local-dir", str(inputs)]
argv += [row["path"] for row in source["files"]]
subprocess.run(argv, check=True)


def checkout(lock, converter):
spec = lock["converter"]
if not (converter / ".git").exists():
subprocess.run(["git", "init", "--quiet", str(converter)], check=True)
subprocess.run(
["git", "-C", str(converter), "remote", "add", "origin", spec["repository"]], check=True
)
subprocess.run(
["git", "-C", str(converter), "fetch", "--depth", "1", "origin", spec["commit"]], check=True
)
subprocess.run(
["git", "-C", str(converter), "checkout", "--quiet", "--detach", "FETCH_HEAD"], check=True
)
head = subprocess.check_output(
["git", "-C", str(converter), "rev-parse", "HEAD"], text=True
).strip()
if head != spec["commit"]:
raise SystemExit("converter commit mismatch")
if sha256(converter / spec["script"]) != spec["script_sha256"]:
raise SystemExit("converter script sha256 mismatch")


def convert(lock, inputs, converter, output, gpu):
spec = lock["converter"]
output.mkdir(parents=True, exist_ok=True)
install = " && ".join(
f"python3 -m pip install --no-cache-dir {shlex.quote(r)}"
for r in spec["extra_pip_requirements"]
)
command = " ".join(shlex.quote(a) for a in lock["argv"])
script = f"{install} && {command}" if install else command
argv = ["docker", "run", "--rm", "--gpus", f"device={gpu}", "--network", "bridge"]
argv += ["--user", f"{os.getuid()}:{os.getgid()}", "--env", "HOME=/tmp"]
argv += [
"--env",
"PYTHONPATH=/repo",
"--env",
"PYTHONDONTWRITEBYTECODE=1",
"--workdir",
"/repo",
]
argv += ["--volume", f"{converter}:/repo:ro", "--volume", f"{inputs}:/ckpt:ro"]
argv += ["--volume", f"{output}:/out", "--entrypoint", "bash", spec["image"], "-c", script]
subprocess.run(argv, check=True)


def seal(lock, output, path):
verify(output, lock["outputs"], "model output")
files = {row["path"]: dict(sha256=row["sha256"], bytes=row["bytes"]) for row in lock["outputs"]}
value = dict(model_identity=lock["model_identity"], files=files)
path.write_text(json.dumps(value, indent=2) + "\n")
return path


def main(argv=None):
parser = argparse.ArgumentParser()
parser.add_argument("--inputs", type=Path)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--converter", type=Path)
parser.add_argument("--seal", type=Path)
parser.add_argument("--gpu", default="0")
parser.add_argument("--download", action="store_true")
parser.add_argument("--seal-only", action="store_true")
args = parser.parse_args(argv)
lock = json.loads(LOCK.read_text())
output = args.output.resolve()
if not args.seal_only:
if args.inputs is None:
parser.error("--inputs is required unless --seal-only")
inputs = args.inputs.resolve()
if args.converter is None:
parser.error("--converter is required unless --seal-only")
if args.download:
download(lock, inputs)
verify(inputs, lock["inputs"]["files"], "input")
converter = args.converter.resolve()
checkout(lock, converter)
convert(lock, inputs, converter, output, args.gpu)
written = seal(lock, output, (args.seal or output / "model-seal.json").resolve())
print(
json.dumps(dict(status="SEALED", model_identity=lock["model_identity"], seal=str(written)))
)


if __name__ == "__main__":
sys.exit(main())
Loading