nvidia-nemo--speech
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 行
26 KiB
Python
693 行
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()
|