chore: import upstream snapshot with attribution
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,692 @@
|
||||
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Evaluation script for Duplex EARTTS models following MagpieTTS evaluation recipe.
|
||||
|
||||
Args:
|
||||
config-path (str):
|
||||
Path to the directory containing the YAML configuration file.
|
||||
|
||||
config-name (str):
|
||||
Name of the YAML configuration file.
|
||||
|
||||
checkpoint_path (str):
|
||||
Path to the Duplex EARTTS checkpoint file.
|
||||
|
||||
datasets_json_path (str):
|
||||
Path to a JSONL (JSON Lines) file describing the evaluation dataset.
|
||||
Each line must be a valid JSON object representing one sample.
|
||||
|
||||
Supported formats:
|
||||
|
||||
----------------------------------------------------------------------
|
||||
1) SINGLE-TURN FORMAT
|
||||
----------------------------------------------------------------------
|
||||
"text" is a string.
|
||||
|
||||
Example:
|
||||
{"text": "Like really quickly and they go haha and then they run off.",
|
||||
"context_audio_filepath": "speaker_1.wav",
|
||||
"audio_filepath": "audio_1.wav"}
|
||||
|
||||
{"text": "Sure. Okay.",
|
||||
"context_audio_filepath": "speaker_2.wav",
|
||||
"audio_filepath": "audio_2.wav"}
|
||||
|
||||
----------------------------------------------------------------------
|
||||
2) MULTI-TURN FORMAT
|
||||
----------------------------------------------------------------------
|
||||
"text" is a list of utterances (List[str]).
|
||||
Each element represents one conversational turn. The model will
|
||||
tokenize and pad each segment sequentially.
|
||||
|
||||
Example:
|
||||
{"text": ["Okay yeah.", "Yeah.", "Right.", "I get what you’re saying.", "That makes sense."],
|
||||
"context_audio_filepath": "speaker_1.wav",
|
||||
"audio_filepath": "dummy_blank_audio_mt_0001.wav"}
|
||||
|
||||
{"text": ["Okay.", "Really?", "Yeah, okay.", "I didn’t know that.", "That’s interesting."],
|
||||
"context_audio_filepath": "speaker_2.wav",
|
||||
"audio_filepath": "audio_2.wav"}
|
||||
|
||||
----------------------------------------------------------------------
|
||||
FIELD DESCRIPTIONS
|
||||
----------------------------------------------------------------------
|
||||
|
||||
text:
|
||||
Either:
|
||||
- str (single-turn)
|
||||
- List[str] (multi-turn)
|
||||
|
||||
context_audio_filepath:
|
||||
Path to the reference speaker audio used for conditioning.
|
||||
This can be overridden by setting:
|
||||
++user_custom_speaker_reference=<path>
|
||||
|
||||
audio_filepath:
|
||||
Output audio file name.
|
||||
This is used only as the base filename for saving generated audio
|
||||
inside `out_dir`. The file does NOT need to exist beforehand.
|
||||
|
||||
out_dir (str):
|
||||
Directory where generated audio samples will be saved.
|
||||
|
||||
inference_dtype (str, optional):
|
||||
Target dtype used during inference. This controls the precision
|
||||
of model weights and operations.
|
||||
|
||||
Supported values:
|
||||
- "float32" (default)
|
||||
- "float16"
|
||||
- "bfloat16"
|
||||
|
||||
Notes:
|
||||
- If set to a lower precision (e.g., float16), the model weights
|
||||
and/or execution dtype will be adjusted accordingly.
|
||||
- Internally mapped via `getattr(torch, inference_dtype)`.
|
||||
|
||||
keep_codec_original_dtype (bool, optional):
|
||||
Controls whether the audio codec module keeps its original dtype
|
||||
when `inference_dtype` is not float32.
|
||||
|
||||
If True (default):
|
||||
- Only the TTS backbone (`model.tts_model`) is cast to the target dtype.
|
||||
- The codec remains in its original precision (typically float32).
|
||||
- Useful to isolate precision effects and avoid degradation from
|
||||
codec quantization.
|
||||
|
||||
If False:
|
||||
- The entire model (including codec) is cast to `inference_dtype`.
|
||||
- `model.audio_codec_run_dtype` is also set accordingly.
|
||||
|
||||
debug_dtype (bool, optional):
|
||||
Enables runtime inspection of tensor dtypes flowing through the model.
|
||||
|
||||
If True:
|
||||
- Forward hooks are attached to all leaf modules.
|
||||
- During the first batch, dtype usage statistics are collected
|
||||
and logged.
|
||||
- Outputs include:
|
||||
- Per-module-group dtype distribution
|
||||
- Example module names per dtype
|
||||
|
||||
Usage:
|
||||
# Example with fp32 inference
|
||||
python duplex_eartts_eval.py \
|
||||
--config-path=conf/ \
|
||||
--config-name=duplex_eartts.yaml \
|
||||
++checkpoint_path=duplex_eartts_results/duplex_eartts/model.ckpt \
|
||||
++datasets_json_path=/path/to/evalset_config.jsonl \
|
||||
++out_dir=duplex_eartts_results/duplex_eartts/audio_samples/dummy_dataset
|
||||
|
||||
# Example with fp16 inference and dtype debugging
|
||||
python duplex_eartts_eval.py \
|
||||
--config-path=conf/ \
|
||||
--config-name=duplex_eartts.yaml \
|
||||
++checkpoint_path=duplex_eartts_results/duplex_eartts/model.ckpt \
|
||||
++datasets_json_path=/path/to/evalset_config.jsonl \
|
||||
++out_dir=uplex_eartts_results/duplex_eartts/audio_samples/dummy_dataset \
|
||||
++inference_dtype=float16 \
|
||||
++keep_codec_original_dtype=True \
|
||||
++debug_dtype=True
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from functools import partial
|
||||
|
||||
import librosa
|
||||
import soundfile as sf
|
||||
import torch
|
||||
from torch.nn.utils.rnn import pad_sequence
|
||||
from torch.utils.data import DataLoader, Dataset
|
||||
|
||||
from nemo.collections.audio.parts.utils.transforms import resample
|
||||
|
||||
torch.set_float32_matmul_precision("medium")
|
||||
torch.backends.cudnn.allow_tf32 = True
|
||||
torch.backends.cuda.matmul.allow_tf32 = True
|
||||
from contextlib import nullcontext
|
||||
|
||||
from omegaconf import OmegaConf
|
||||
|
||||
from nemo.collections.speechlm2.models.duplex_ear_tts import DuplexEARTTS
|
||||
from nemo.collections.speechlm2.parts.metrics.asr_cer_wer import Intelligibility
|
||||
from nemo.collections.speechlm2.parts.metrics.secs import SECS
|
||||
from nemo.collections.speechlm2.parts.precision import fp32_precision
|
||||
from nemo.core.config import hydra_runner
|
||||
from nemo.utils import logging
|
||||
|
||||
# Use .get() to avoid crashing when running a single GPU without torchrun
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.set_device(int(os.environ.get("LOCAL_RANK", 0)))
|
||||
|
||||
|
||||
def attach_dtype_counter(model):
|
||||
"""
|
||||
Attaches forward hooks to all leaf modules of a model to track the dtype
|
||||
of their outputs during inference.
|
||||
|
||||
This utility is designed for debugging precision behavior, especially when
|
||||
using mixed precision or reduced precision (fp16 / bf16).
|
||||
|
||||
Behavior:
|
||||
- Registers a forward hook on each leaf module (modules with no children).
|
||||
- For each forward pass, records the dtype of the module output.
|
||||
- Aggregates statistics grouped by top-level module name.
|
||||
- Stores a few example module class names per dtype.
|
||||
|
||||
Returns:
|
||||
handles (List[RemovableHandle]):
|
||||
List of hook handles. These must be removed manually to avoid
|
||||
memory leaks or performance degradation.
|
||||
|
||||
stats (Dict[str, Dict[str, int]]):
|
||||
Nested dictionary containing dtype counts per module group.
|
||||
Structure:
|
||||
stats[module_group][dtype] = count
|
||||
|
||||
Example:
|
||||
{
|
||||
"tts_model": {
|
||||
"torch.float16": 120,
|
||||
"torch.float32": 0,
|
||||
"torch.bfloat16": 0,
|
||||
"other": 2
|
||||
}
|
||||
}
|
||||
|
||||
examples (Dict[str, Dict[str, List[str]]]):
|
||||
Stores up to 3 example module class names per dtype per group.
|
||||
Useful for quickly identifying which layers are running in
|
||||
unexpected precision.
|
||||
|
||||
Notes:
|
||||
- Only inspects outputs (not inputs or parameters).
|
||||
- Dtype is inferred from the first tensor found in the output.
|
||||
- Non-floating dtypes are categorized as "other".
|
||||
- Grouping is based on the top-level module name (prefix before first dot).
|
||||
|
||||
Typical usage:
|
||||
handles, stats, examples = attach_dtype_counter(model)
|
||||
|
||||
# Run inference ...
|
||||
|
||||
for h in handles:
|
||||
h.remove()
|
||||
"""
|
||||
handles = []
|
||||
|
||||
# structure: stats[module_group][dtype] = count
|
||||
stats = {}
|
||||
examples = {}
|
||||
|
||||
def is_leaf(module):
|
||||
return len(list(module.children())) == 0
|
||||
|
||||
def get_dtype(x):
|
||||
if torch.is_tensor(x):
|
||||
return str(x.dtype)
|
||||
elif isinstance(x, (list, tuple)):
|
||||
for t in x:
|
||||
if torch.is_tensor(t):
|
||||
return str(t.dtype)
|
||||
return "other"
|
||||
|
||||
def get_module_group(name):
|
||||
# top-level module (before first dot)
|
||||
return name.split(".")[0] if "." in name else name
|
||||
|
||||
def hook_fn(name):
|
||||
def fn(module, inputs, outputs):
|
||||
dtype = get_dtype(outputs)
|
||||
if dtype not in ["torch.float16", "torch.bfloat16", "torch.float32"]:
|
||||
dtype = "other"
|
||||
|
||||
group = get_module_group(name)
|
||||
|
||||
if group not in stats:
|
||||
stats[group] = {
|
||||
"torch.float16": 0,
|
||||
"torch.bfloat16": 0,
|
||||
"torch.float32": 0,
|
||||
"other": 0,
|
||||
}
|
||||
examples[group] = {
|
||||
"torch.float16": [],
|
||||
"torch.bfloat16": [],
|
||||
"torch.float32": [],
|
||||
"other": [],
|
||||
}
|
||||
|
||||
stats[group][dtype] += 1
|
||||
|
||||
# store a few examples per dtype per group
|
||||
if len(examples[group][dtype]) < 3:
|
||||
examples[group][dtype].append(module.__class__.__name__)
|
||||
|
||||
return fn
|
||||
|
||||
for name, module in model.named_modules():
|
||||
if is_leaf(module):
|
||||
handles.append(module.register_forward_hook(hook_fn(name)))
|
||||
|
||||
return handles, stats, examples
|
||||
|
||||
|
||||
def report_dtype_stats(handles, stats, examples):
|
||||
"""
|
||||
Cleans up monitoring hooks and logs a detailed report of the tensor precisions
|
||||
(dtypes) observed during the model forward pass.
|
||||
|
||||
This function should be called after at least one inference iteration has
|
||||
completed while hooks are attached. It removes the hooks to prevent
|
||||
performance overhead and prints a structured summary of which module groups
|
||||
executed in which dtypes.
|
||||
|
||||
Args:
|
||||
handles (List[torch.utils.hooks.RemovableHandle]): The list of hooks
|
||||
returned by `attach_dtype_counter`.
|
||||
stats (Dict): Nested dictionary containing dtype counts per module group.
|
||||
examples (Dict): Dictionary containing example module names for each
|
||||
observed dtype.
|
||||
"""
|
||||
for h in handles:
|
||||
h.remove()
|
||||
|
||||
logging.info("\n=== DTYPE USAGE PER MODULE ===")
|
||||
|
||||
for group, group_stats in stats.items():
|
||||
total = sum(group_stats.values())
|
||||
if total == 0:
|
||||
continue
|
||||
|
||||
logging.info(f"\n--- {group} ---")
|
||||
for dtype, count in group_stats.items():
|
||||
if count > 0:
|
||||
logging.info(f"{dtype}: {count} ({100*count/total:.2f}%)")
|
||||
|
||||
logging.info("\n=== EXAMPLES ===")
|
||||
for group, group_examples in examples.items():
|
||||
logging.info(f"\n--- {group} ---")
|
||||
for dtype, mods in group_examples.items():
|
||||
if mods:
|
||||
logging.info(f"{dtype}: {mods}")
|
||||
|
||||
|
||||
class EvalJSONLDataset(Dataset):
|
||||
"""
|
||||
Standard PyTorch Dataset for reading JSONL evaluation files.
|
||||
"""
|
||||
|
||||
def __init__(self, file_path):
|
||||
self.samples = []
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
for line_idx, line in enumerate(f, 1):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
self.samples.append(json.loads(line))
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid JSON on line {line_idx}: {e}")
|
||||
|
||||
def __len__(self):
|
||||
return len(self.samples)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return self.samples[idx]
|
||||
|
||||
|
||||
def collate_and_tokenize_custom(
|
||||
batch,
|
||||
model,
|
||||
extra_duration_thrshould=1.3,
|
||||
sample_rate=22050,
|
||||
root_path=None,
|
||||
add_beginning_pad_tokens=False,
|
||||
add_eos=False,
|
||||
pad_factor_text_speech=10,
|
||||
force_interruption=False,
|
||||
):
|
||||
tokenized_list = []
|
||||
|
||||
# --- TEXT TOKENIZATION ---
|
||||
for s in batch:
|
||||
text_data = s["text"]
|
||||
|
||||
# Check if text is a list (New Logic)
|
||||
if isinstance(text_data, list):
|
||||
# Start with BOS
|
||||
full_ids = []
|
||||
|
||||
for segment in text_data:
|
||||
# Tokenize segment
|
||||
seg_ids = [model.tokenizer.bos]
|
||||
seg_ids = seg_ids + model.tokenizer.text_to_ids(segment)
|
||||
seg_len = len(seg_ids)
|
||||
|
||||
# Calculate pad length (pad_factor_text_speechx the size of the text)
|
||||
pad_len = seg_len * pad_factor_text_speech
|
||||
|
||||
# Construct: text + 4x pads
|
||||
# We extend the list with the tokens and then the pad tokens
|
||||
pad_ids = [model.text_pad_id] * pad_len
|
||||
|
||||
if force_interruption:
|
||||
fname = s["audio_filepath"]
|
||||
no_ext = fname.split(".")[0]
|
||||
sample_id = int(no_ext.split("_")[-1])
|
||||
|
||||
case = sample_id % 3 # 0,1,2 -> ~33% each
|
||||
|
||||
if case == 0:
|
||||
# 33%: emulate interruption where text was not fully processed
|
||||
# (no pad eos placement at all)
|
||||
if len(seg_ids) >= 2:
|
||||
seg_ids[-2] = model.text_eos_id
|
||||
seg_ids[-1] = model.text_pad_id
|
||||
else:
|
||||
# fallback: if seg_ids is too short, emulate with pad EOS at 0
|
||||
pad_ids[0] = model.text_eos_id
|
||||
elif case == 1:
|
||||
# 33%: put EOS at pad index 6 - so 0.5 seconds after the whole text was processed
|
||||
eos_idx = min(6, len(pad_ids) - 1)
|
||||
pad_ids[eos_idx] = model.text_eos_id
|
||||
else:
|
||||
# 33%: put EOS at pad index 0
|
||||
eos_idx = 0
|
||||
pad_ids[eos_idx] = model.text_eos_id
|
||||
else:
|
||||
if (
|
||||
add_eos
|
||||
): # add eos in the end of the paddding sequence keep 70% for the speech and the rest for after EOS
|
||||
eos_idx = int(len(pad_ids) * 0.7)
|
||||
pad_ids[eos_idx] = model.text_eos_id
|
||||
|
||||
full_ids.extend(seg_ids)
|
||||
full_ids.extend(pad_ids)
|
||||
|
||||
tokenized_list.append(torch.as_tensor(full_ids, dtype=torch.long))
|
||||
|
||||
else:
|
||||
# Standard String Handling
|
||||
tokenized_list.append(
|
||||
torch.as_tensor([model.tokenizer.bos] + model.tokenizer.text_to_ids(text_data), dtype=torch.long)
|
||||
)
|
||||
|
||||
if add_beginning_pad_tokens:
|
||||
pad_len = 25
|
||||
prefix = torch.full((pad_len,), model.text_pad_id, dtype=torch.long)
|
||||
for i in range(len(tokenized_list)):
|
||||
tokenized_list[i] = torch.cat([prefix, tokenized_list[i]])
|
||||
|
||||
# Pad the text sequences (batch-wise)
|
||||
input_ids = pad_sequence(tokenized_list, batch_first=True, padding_value=model.text_pad_id)
|
||||
|
||||
# load the target audio if available
|
||||
audio_list = []
|
||||
audio_lengths = []
|
||||
target_num_frames = []
|
||||
|
||||
for i, s in enumerate(batch):
|
||||
# Load Context Audio
|
||||
audio_path = s["context_audio_filepath"]
|
||||
if root_path is not None:
|
||||
audio_path = os.path.join(root_path, audio_path)
|
||||
|
||||
# Safety check for context audio presence, though usually required
|
||||
if os.path.exists(audio_path):
|
||||
wav, sr = librosa.load(audio_path, sr=sample_rate, mono=True)
|
||||
wav = torch.as_tensor(wav, dtype=torch.float32)
|
||||
else:
|
||||
# Fallback if context missing (optional safety)
|
||||
wav = torch.zeros(1, dtype=torch.float32)
|
||||
|
||||
audio_list.append(wav)
|
||||
audio_lengths.append(len(wav))
|
||||
|
||||
# Handle Target Audio / Duration
|
||||
tdur_audio_path = s["audio_filepath"]
|
||||
if root_path is not None:
|
||||
tdur_audio_path = os.path.join(root_path, tdur_audio_path)
|
||||
|
||||
# Check availability
|
||||
if tdur_audio_path and os.path.exists(tdur_audio_path):
|
||||
wav_dur, sr_ = librosa.load(tdur_audio_path, sr=sample_rate, mono=True)
|
||||
tdur = wav_dur.shape[0] // model.target_samples_per_frame
|
||||
target_num_frames.append(tdur * extra_duration_thrshould)
|
||||
else:
|
||||
# Audio not available: Derive size from text channel
|
||||
# We follow the 4x approach logic here to determine frames.
|
||||
# If text was a list, it already has physical pads (1 + 4 ratio).
|
||||
# We map 1 token roughly to 1 frame (or whatever the model scale is).
|
||||
# Assuming 1 token ~ 1 frame in the model's alignment, we just take the input length.
|
||||
|
||||
current_text_len = len(tokenized_list[i])
|
||||
|
||||
if isinstance(s["text"], list):
|
||||
# The text tokens are already physically padded 10x.
|
||||
# Target frames should match this structure exactly.
|
||||
target_num_frames.append(current_text_len)
|
||||
else:
|
||||
# If text was a string (no physical pads added), but audio is missing,
|
||||
# we simulate the 4x duration expansion (1 part text, 4 parts silence = 5x total).
|
||||
target_num_frames.append(current_text_len * 5)
|
||||
|
||||
# audio padding
|
||||
max_audio_len = max(audio_lengths)
|
||||
B = len(audio_lengths)
|
||||
|
||||
padded_audio = torch.zeros((B, max_audio_len), dtype=torch.float32)
|
||||
|
||||
for i, wav in enumerate(audio_list):
|
||||
padded_audio[i, : len(wav)] = wav
|
||||
|
||||
# Keep on CPU
|
||||
audio_lengths = torch.tensor(audio_lengths, dtype=torch.long)
|
||||
|
||||
# Expand text length to match expected output speech duration
|
||||
B, L = input_ids.shape
|
||||
target_len = int(max(target_num_frames))
|
||||
|
||||
# Ensure target_len is at least as long as the input text
|
||||
# (prevents truncation if calc was slightly off)
|
||||
target_len = max(target_len, L)
|
||||
|
||||
padded_input_ids = torch.full((B, target_len), fill_value=model.text_pad_id, dtype=input_ids.dtype)
|
||||
|
||||
# Copy the actual tokens (which might already contain list-based padding)
|
||||
padded_input_ids[:, :L] = input_ids
|
||||
|
||||
# If text is a list ["Hi", "There"], join it into "Hi There"
|
||||
collapsed_raw_text = [" ".join(s["text"]) if isinstance(s["text"], list) else s["text"] for s in batch]
|
||||
|
||||
return {
|
||||
"input_ids": padded_input_ids,
|
||||
"raw_text": collapsed_raw_text,
|
||||
"context_audio": padded_audio,
|
||||
"context_audio_lengths": audio_lengths,
|
||||
"target_audio_paths": [s["audio_filepath"] for s in batch],
|
||||
"target_num_frames": target_num_frames,
|
||||
}
|
||||
|
||||
|
||||
@hydra_runner(config_path="conf", config_name="duplex_eartts")
|
||||
def inference(cfg):
|
||||
OmegaConf.resolve(cfg)
|
||||
|
||||
distributed = int(os.environ.get("WORLD_SIZE", "1")) > 1
|
||||
if distributed and not torch.distributed.is_initialized():
|
||||
torch.distributed.init_process_group(backend="nccl")
|
||||
|
||||
# Dynamically determine the correct GPU for this process
|
||||
if torch.cuda.is_available():
|
||||
local_rank = int(os.environ.get("LOCAL_RANK", 0))
|
||||
target_device = torch.device(f"cuda:{local_rank}")
|
||||
else:
|
||||
target_device = torch.device("cpu")
|
||||
|
||||
torch.set_float32_matmul_precision("medium")
|
||||
torch.backends.cudnn.allow_tf32 = True
|
||||
torch.backends.cuda.matmul.allow_tf32 = True
|
||||
|
||||
target_dtype = getattr(torch, cfg.get("inference_dtype", "float32"))
|
||||
if target_dtype != torch.float32:
|
||||
torch.set_default_dtype(target_dtype)
|
||||
|
||||
if cfg.get("checkpoint_path", None):
|
||||
model = DuplexEARTTS.load_from_checkpoint(
|
||||
cfg.checkpoint_path, cfg=OmegaConf.to_container(cfg, resolve=True), map_location=target_device
|
||||
).eval()
|
||||
else:
|
||||
raise ValueError("For evaluation, you must provide `cfg.checkpoint_path`.")
|
||||
|
||||
if target_dtype != torch.float32:
|
||||
if cfg.get("keep_codec_original_dtype", True):
|
||||
model.tts_model.to(dtype=target_dtype)
|
||||
model.ensures_codec_target_dtype() # ensures that codec is in the right precision
|
||||
else:
|
||||
model.audio_codec_run_dtype = target_dtype
|
||||
model.to(dtype=target_dtype)
|
||||
|
||||
if cfg.get("debug_dtype", False):
|
||||
handles, stats, examples = attach_dtype_counter(model)
|
||||
|
||||
with fp32_precision():
|
||||
intelligibility = Intelligibility("stt_en_fastconformer_transducer_large", reuse_asr_hyps=False).reset()
|
||||
secs_metric = SECS("titanet_large").reset()
|
||||
|
||||
# Initialize the Dataset
|
||||
eval_dataset = EvalJSONLDataset(cfg.datasets_json_path)
|
||||
|
||||
# Use partial to bind the model and config parameters to the collate function
|
||||
collate_fn = partial(
|
||||
collate_and_tokenize_custom,
|
||||
model=model,
|
||||
extra_duration_thrshould=1.5,
|
||||
sample_rate=model.target_sample_rate,
|
||||
root_path=cfg.audio_dir,
|
||||
add_beginning_pad_tokens=cfg.get("add_beginning_pad_tokens", True),
|
||||
add_eos=cfg.get("add_eos", True),
|
||||
pad_factor_text_speech=cfg.get("pad_factor_text_speech", 10),
|
||||
force_interruption=cfg.get("force_interruption", False),
|
||||
)
|
||||
|
||||
# Initialize the DataLoader
|
||||
dataloader = DataLoader(
|
||||
dataset=eval_dataset,
|
||||
batch_size=cfg.batch_size,
|
||||
collate_fn=collate_fn,
|
||||
num_workers=cfg.get("num_workers", 4),
|
||||
pin_memory=True,
|
||||
shuffle=False,
|
||||
drop_last=False,
|
||||
)
|
||||
|
||||
if cfg.get("user_custom_speaker_reference", None):
|
||||
wav, sr = librosa.load(cfg.model.inference_speaker_reference, sr=model.target_sample_rate, mono=True)
|
||||
speaker_wav = torch.as_tensor(wav, dtype=target_dtype).unsqueeze(0).to(model.device)
|
||||
|
||||
# Iterate over the DataLoader
|
||||
for batch_id, inputs in enumerate(dataloader):
|
||||
|
||||
# Move required tensors to the GPU immediately
|
||||
inputs["input_ids"] = inputs["input_ids"].to(model.device)
|
||||
inputs["context_audio"] = inputs["context_audio"].to(model.device)
|
||||
inputs["context_audio_lengths"] = inputs["context_audio_lengths"].to(model.device)
|
||||
|
||||
if cfg.get("user_custom_speaker_reference", None):
|
||||
inputs["context_audio"] = speaker_wav.expand(inputs["input_ids"].size(0), *speaker_wav.shape[1:])
|
||||
inputs["context_audio_lengths"][:] = speaker_wav.size(-1)
|
||||
|
||||
with torch.no_grad():
|
||||
model.set_init_inputs(
|
||||
speaker_audio=inputs["context_audio"],
|
||||
speaker_audio_lens=inputs["context_audio_lengths"],
|
||||
system_prompt=cfg.get("inference_system_prompt", None),
|
||||
)
|
||||
init_inputs = model.get_init_inputs(B=inputs["input_ids"].size(0))
|
||||
|
||||
audio, audio_len = model.offline_inference(
|
||||
next_subword_ids=inputs["input_ids"],
|
||||
task="custom",
|
||||
init_inputs=init_inputs,
|
||||
)
|
||||
|
||||
if cfg.get("debug_dtype", False) and batch_id == 0:
|
||||
report_dtype_stats(handles, stats, examples)
|
||||
|
||||
with fp32_precision():
|
||||
audio = audio.float()
|
||||
|
||||
# reset audio len to the actual size removing extra long audio padding
|
||||
audio_len = (
|
||||
torch.tensor(inputs["target_num_frames"], device=audio.device) * model.target_samples_per_frame
|
||||
).int()
|
||||
|
||||
# resample audio to the asr sampling rate
|
||||
metric_audio_pred = resample(audio, model.target_sample_rate, 16000)
|
||||
metric_audio_pred_lens = (audio_len / model.target_sample_rate * 16000).to(torch.long)
|
||||
|
||||
intelligibility.update(
|
||||
name="dataset",
|
||||
refs=inputs["raw_text"],
|
||||
pred_audio=metric_audio_pred,
|
||||
pred_audio_lens=metric_audio_pred_lens,
|
||||
asr_hyps=None,
|
||||
)
|
||||
|
||||
secs_metric.update(
|
||||
name="dataset",
|
||||
target_audio=resample(inputs["context_audio"], model.target_sample_rate, 16000),
|
||||
target_audio_lens=(inputs["context_audio_lengths"] / model.target_sample_rate * 16000).to(torch.long),
|
||||
pred_audio=metric_audio_pred,
|
||||
pred_audio_lens=metric_audio_pred_lens,
|
||||
)
|
||||
|
||||
# save audio to cfg.out_dir
|
||||
os.makedirs(cfg.out_dir, exist_ok=True)
|
||||
audio = audio.detach().cpu().float()
|
||||
audio_len = audio_len.cpu()
|
||||
|
||||
for i in range(audio.size(0)):
|
||||
wav = audio[i, : audio_len[i]].numpy()
|
||||
# Use original target audio filename
|
||||
target_path = inputs["target_audio_paths"][i]
|
||||
base_name = os.path.basename(target_path)
|
||||
out_path = os.path.join(cfg.out_dir, base_name)
|
||||
|
||||
sf.write(
|
||||
out_path,
|
||||
wav,
|
||||
samplerate=model.target_sample_rate,
|
||||
)
|
||||
|
||||
logging.info(f"Saved: {out_path}")
|
||||
|
||||
with fp32_precision():
|
||||
logging.info("\n--- Evaluation Metrics ---")
|
||||
cer_wer = intelligibility.compute()
|
||||
for k, m in cer_wer.items():
|
||||
logging.info(f"Intelligibility - {k}: {m}")
|
||||
|
||||
secs_scores = secs_metric.compute()
|
||||
for k, m in secs_scores.items():
|
||||
logging.info(f"SECS - {k}: {m}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
inference()
|
||||
Reference in New Issue
Block a user