ba4be087d5
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
693 lines
26 KiB
Python
693 lines
26 KiB
Python
# 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()
|