Skip to content

Guard ScaleSilence against invalid silence scales - #3745

Merged
csukuangfj merged 1 commit into
k2-fsa:masterfrom
ekenberg:pr/guard-invalid-silence-scale
Jul 10, 2026
Merged

csukuangfj merged 1 commit into
k2-fsa:masterfrom
ekenberg:pr/guard-invalid-silence-scale

Conversation

@ekenberg

@ekenberg ekenberg commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #3744. A review comment on that PR pointed out that a
negative or NaN scale could produce a negative n. It was partly correct:
there is undefined behaviour here, but not quite where it said, and the
suggested fix would not have removed it.

What is actually wrong

ScaleSilence computes

int32_t n = static_cast<int32_t>(len * scale);

Converting NaN or infinity to int32_t is undefined, and a finite but very
large scale overflows the conversion. All three abort the process today:

--tts-silence-scale=nan   terminate called after throwing 'std::length_error'
--tts-silence-scale=inf   terminate called after throwing 'std::length_error'
--tts-silence-scale=1e9   terminate called after throwing 'std::length_error'

The bot suggested clamping n to 0 after the cast. That would not help:
the undefined behaviour is at the cast, so NaN and infinity still get through,
and it does not address overflow at all.

Negative values are already handled. OfflineTtsConfig::Validate() requires
silence_scale >= 0.001 and the CLI calls it, so --tts-silence-scale=-1 is
rejected cleanly. But:

  • NaN < 0.001 is false, so NaN passes Validate().
  • Callers of the Generate API pass GeneratedAudioConfig::silence_scale
    directly and bypass Validate() entirely.

The change

Reject any scale outside [0.01, 2], log, and return the audio unscaled rather
than aborting.

Edited after review: the merged version uses the single range check
@csukuangfj suggested, not the guard originally proposed here. Note that
scale == 0 is now rejected too; it was already unreachable from the CLI, since
Validate() requires silence_scale >= 0.001, but a caller of the Generate
API could previously pass 0 to remove pauses.

Before / after

Same build, kokoro-multi-lang-v1_0, --sid=10,
"First sentence here. Second sentence follows."

--tts-silence-scale before after
nan abort (std::length_error) logs, returns unscaled audio
inf abort (std::length_error) logs, returns unscaled audio
1e9 abort (std::length_error) logs, returns unscaled audio
-inf rejected by Validate() rejected by Validate()
-1 rejected by Validate() rejected by Validate()
2.0 scaled scaled (unchanged)
1.0 early return early return (unchanged)

Checked with clang-format --dry-run --Werror using the repo's own
.clang-format; it reports no changes.

Summary by CodeRabbit

  • Bug Fixes
    • Improved validation for audio silence scaling.
    • Prevented invalid (negative, NaN/∞, or out-of-range) scaling values from modifying generated audio.
    • Audio remains unchanged when an invalid scale is provided, with an error logged.

@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

GeneratedAudio::ScaleSilence now rejects scale values outside [0.01, 2.0], logs an error, and returns the original audio without modifying silence.

Changes

Silence scaling validation

Layer / File(s) Summary
Scale input validation
sherpa-onnx/csrc/offline-tts.cc
Validates the scale range before scaling silence, rejecting invalid, non-finite, and negative values while preserving the original audio.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: adding validation to ScaleSilence for invalid scale values.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist

Copy link
Copy Markdown

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sherpa-onnx/csrc/offline-tts.cc`:
- Around line 82-95: Use double-precision arithmetic for the silence-length
scaling in the interval-processing logic, replacing the float-based
multiplication before the int32_t conversion. Update the code associated with
the scaling operation (including the expression using len) so it matches the
double-based overflow guard and prevents out-of-range conversion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9858fbbb-ed90-4454-8815-6a5870995b6f

📥 Commits

Reviewing files that changed from the base of the PR and between 4b0cf7f and 756374f.

📒 Files selected for processing (1)
  • sherpa-onnx/csrc/offline-tts.cc

Comment thread sherpa-onnx/csrc/offline-tts.cc Outdated
Comment on lines +82 to +95
// A finite but very large scale can still overflow the int32_t conversion
// below, so check the longest interval before scaling anything.
int32_t max_len = 0;
for (const auto &interval : intervals) {
max_len = std::max(max_len, interval.end - interval.start);
}

if (static_cast<double>(max_len) * scale >
std::numeric_limits<int32_t>::max()) {
SHERPA_ONNX_LOGE("Silence scale %f is too large. Skip scaling silence.",
scale);
return *this;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Overflow check uses double but the computation at line 106 uses float — narrow UB window remains.

The guard at line 89 checks static_cast<double>(max_len) * scale > INT32_MAX, but the actual scaling at line 106 computes static_cast<int32_t>(len * scale) where len (int32_t) is promoted to float for the multiplication. For len > 2²⁴ (~16M samples), static_cast<float>(len) can round up, making float(len) * scale exceed INT32_MAX even when double(len) * scale does not. This leaves a narrow path to the exact UB the PR aims to prevent.

Fix: use double at line 106 to match the guard's arithmetic. Since int32_t→double is always exact and len ≤ max_len, the guard becomes a perfect bound.

🔧 Proposed fix for line 106
-    int32_t n = static_cast<int32_t>(len * scale);
+    int32_t n = static_cast<int32_t>(static_cast<double>(len) * scale);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// A finite but very large scale can still overflow the int32_t conversion
// below, so check the longest interval before scaling anything.
int32_t max_len = 0;
for (const auto &interval : intervals) {
max_len = std::max(max_len, interval.end - interval.start);
}
if (static_cast<double>(max_len) * scale >
std::numeric_limits<int32_t>::max()) {
SHERPA_ONNX_LOGE("Silence scale %f is too large. Skip scaling silence.",
scale);
return *this;
}
int32_t n = static_cast<int32_t>(static_cast<double>(len) * scale);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sherpa-onnx/csrc/offline-tts.cc` around lines 82 - 95, Use double-precision
arithmetic for the silence-length scaling in the interval-processing logic,
replacing the float-based multiplication before the int32_t conversion. Update
the code associated with the scaling operation (including the expression using
len) so it matches the double-based overflow guard and prevents out-of-range
conversion.

@csukuangfj

Copy link
Copy Markdown
Collaborator

Can you limit the value of scale, e.g., log and return if scale > 2 or scale < 0.01?

The motivation of using scale is to reduce long pauses in the generated speech and the expected value of scale is usually in the range (0, 1).

The current changes make the code complicated.

n below is computed as interval_length * scale and converted to an int32_t.
Converting NaN or infinity is undefined behaviour, and a large finite scale
overflows the conversion. In practice each of these aborts the process:

  --tts-silence-scale=nan   terminate called after throwing std::length_error
  --tts-silence-scale=inf   terminate called after throwing std::length_error
  --tts-silence-scale=1e9   terminate called after throwing std::length_error

Since scale exists to shorten long pauses and is normally within (0, 1),
reject anything outside [0.01, 2], log, and return the audio unscaled. Any
comparison with NaN is false, so a single range check rejects NaN too.

Negative values were already rejected by OfflineTtsConfig::Validate(), which
requires silence_scale >= 0.001. NaN passed that check, because NaN < 0.001
is false, and callers of the Generate API pass
GeneratedAudioConfig::silence_scale directly and bypass Validate() entirely.
@ekenberg
ekenberg force-pushed the pr/guard-invalid-silence-scale branch from 756374f to 355d51c Compare July 10, 2026 09:31
@ekenberg

Copy link
Copy Markdown
Contributor Author

Thanks — good call, that's much simpler. Replaced my version with the range check you suggested: log and return unless scale is in [0.01, 2].

Since any comparison with NaN is false, that single check also covers nan and inf, which previously aborted with std::length_error, along with large finite values like 1e9.

One detail: the literals are 0.01f and 2.0f, not 0.01 and 2. With the double literal, float(0.01) is 0.00999999977… and scale = 0.01 gets rejected at its own boundary.

@coderabbitai the float/double mismatch you flagged is moot now — with scale <= 2 the conversion can't overflow.

Verified: 0.0099 and 2.001 are skipped; 0.01, 0.8, 1.0, 1.4, 2.0 scale as before; nan, inf, 1e9 are skipped instead of aborting.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd offline-tts.cc --exec cat -n {} \; | sed -n '1,80p'

Length of output: 3569


🏁 Script executed:

#!/bin/bash
fd offline-tts.cc sherpa-onnx/csrc --exec cat -n {} \;

Length of output: 25010


@ekenberg Confirmed — I checked the current sherpa-onnx/csrc/offline-tts.cc, and the guard at Line 44 now reads:

if (!(scale >= 0.01f && scale <= 2.0f)) {
  SHERPA_ONNX_LOGE("Silence scale %f is not in [0.01, 2]. Skip scaling.",
                   scale);
  return *this;
}

Using 0.01f/2.0f float literals avoids the double-to-float rounding issue you described, and the NaN/infinity handling via the always-false comparison is a nice, self-documenting side effect. This is indeed much simpler than the original explicit isfinite/overflow-check approach, and since scale is now bounded to [0.01, 2], len * scale cannot overflow int32_t regardless of len's range. Nice cleanup.


✏️ Learnings added
Learnt from: ekenberg
URL: https://github.com/k2-fsa/sherpa-onnx/pull/3745

Timestamp: 2026-07-10T09:32:45.626Z
Learning: In sherpa-onnx/csrc/offline-tts.cc, `GeneratedAudio::ScaleSilence(float scale)` rejects invalid/out-of-range scales with a single range check: `if (!(scale >= 0.01f && scale <= 2.0f)) { log and return *this; }`. The literals must be float (`0.01f`, `2.0f`) rather than double (`0.01`, `2`), because `float(0.01)` rounds to ~0.00999999977, which would incorrectly reject `scale == 0.01f` at its own boundary. This single check also covers NaN and infinity, since any comparison with NaN is false, and it also bounds `len * scale` (interval length times scale) so casting to int32_t cannot overflow.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@csukuangfj csukuangfj left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for your contribution!

@csukuangfj
csukuangfj merged commit 986a386 into k2-fsa:master Jul 10, 2026
25 of 27 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sherpa-onnx/csrc/offline-tts.cc (1)

94-94: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Integer overflow in static_cast<int32_t>(len * scale) — no overflow guard exists.

The PR summary states "skips scaling when a finite scale would overflow that type," but the code has no such check. The range [0.01, 2.0] bounds scale but not the product len * scale. With scale = 2.0 and len > INT32_MAX / 2 (~1.07 billion samples, ~6.7 hours at 48 kHz), the product exceeds INT32_MAX and the static_cast<int32_t> is undefined behavior. If the result wraps negative, n <= len at line 96 is true, causing iterator UB at lines 97–98; or n - len at line 106 converts to a huge size_t, triggering a massive allocation.

Additionally, len (int32_t) is promoted to float for the multiplication, losing precision for len > 2²⁴ (~16M). This was flagged in a prior review and remains unaddressed.

Suggested fix: use double arithmetic and add an overflow guard.

🔧 Proposed fix
     int32_t len = interval.end - interval.start;
-    int32_t n = static_cast<int32_t>(len * scale);
+    double product = static_cast<double>(len) * static_cast<double>(scale);
+    if (product > static_cast<double>(INT32_MAX)) {
+      // Skip scaling for this interval to avoid overflow.
+      ans.samples.insert(ans.samples.end(), samples.begin() + interval.start,
+                         samples.begin() + interval.end);
+      continue;
+    }
+    int32_t n = static_cast<int32_t>(product);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sherpa-onnx/csrc/offline-tts.cc` at line 94, Fix the sample-count calculation
around n by using double-precision arithmetic for len * scale, checking that the
result is finite and within the int32_t range before casting, and skipping
scaling or handling the overflow safely when it is not. Preserve the existing
behavior for valid results while preventing invalid iterator ranges or oversized
allocations in the subsequent n <= len and resize logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sherpa-onnx/csrc/offline-tts.cc`:
- Around line 44-48: Align OfflineTtsConfig::Validate() with ScaleSilence by
validating silence_scale within the inclusive range [0.01, 2.0], rejecting
values below 0.01 or above 2.0 and reporting the validation error consistently.
Update the existing silence_scale validation logic in
OfflineTtsConfig::Validate() so all values accepted by configuration are handled
by ScaleSilence without being silently skipped.
- Around line 44-48: Update the range validation in ScaleSilence to allow scale
= 0 by changing the lower bound from 0.01f to 0.0f, preserving the existing
behavior that removes silence intervals when scaling by zero.

---

Outside diff comments:
In `@sherpa-onnx/csrc/offline-tts.cc`:
- Line 94: Fix the sample-count calculation around n by using double-precision
arithmetic for len * scale, checking that the result is finite and within the
int32_t range before casting, and skipping scaling or handling the overflow
safely when it is not. Preserve the existing behavior for valid results while
preventing invalid iterator ranges or oversized allocations in the subsequent n
<= len and resize logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51682f94-2ec9-45e5-8f6d-d16fbe0cbab4

📥 Commits

Reviewing files that changed from the base of the PR and between 756374f and 355d51c.

📒 Files selected for processing (1)
  • sherpa-onnx/csrc/offline-tts.cc

Comment on lines +44 to +48
if (!(scale >= 0.01f && scale <= 2.0f)) {
SHERPA_ONNX_LOGE("Silence scale %f is not in [0.01, 2]. Skip scaling.",
scale);
return *this;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Config validation range [0.001, ∞) doesn't match ScaleSilence range [0.01, 2.0].

OfflineTtsConfig::Validate() at line 224 only rejects silence_scale < 0.001 with no upper bound, but ScaleSilence rejects values outside [0.01, 2.0]. Values in [0.001, 0.01) or above 2.0 pass config validation but are silently no-op'd at runtime — the user gets unscaled audio with no indication beyond a log message.

Align the two ranges to avoid surprising behavior:

🔧 Proposed fix for Validate()
-  if (silence_scale < 0.001) {
-    SHERPA_ONNX_LOGE("--tts-silence-scale '%.3f' is too small", silence_scale);
+  if (silence_scale < 0.01f || silence_scale > 2.0f) {
+    SHERPA_ONNX_LOGE("--tts-silence-scale '%.3f' is not in [0.01, 2]",
+                     silence_scale);
     return false;
   }

Also applies to: 224-227

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sherpa-onnx/csrc/offline-tts.cc` around lines 44 - 48, Align
OfflineTtsConfig::Validate() with ScaleSilence by validating silence_scale
within the inclusive range [0.01, 2.0], rejecting values below 0.01 or above 2.0
and reporting the validation error consistently. Update the existing
silence_scale validation logic in OfflineTtsConfig::Validate() so all values
accepted by configuration are handled by ScaleSilence without being silently
skipped.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Range check rejects scale = 0, breaking existing pause-removal behavior.

The PR summary states "Scale 0 retains its existing behavior of removing pauses," but 0 < 0.01f fails the range check, so ScaleSilence(0) now logs an error and returns unscaled audio with pauses intact. Previously, scale = 0 produced n = 0, effectively removing all silence intervals.

If scale = 0 should still remove pauses, lower the bound to 0.0f:

🔧 Proposed fix
-  if (!(scale >= 0.01f && scale <= 2.0f)) {
-    SHERPA_ONNX_LOGE("Silence scale %f is not in [0.01, 2]. Skip scaling.",
+  if (!(scale >= 0.0f && scale <= 2.0f)) {
+    SHERPA_ONNX_LOGE("Silence scale %f is not in [0, 2]. Skip scaling.",
                      scale);

If rejecting scale = 0 is intentional, the PR summary should be corrected to reflect this behavior change.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!(scale >= 0.01f && scale <= 2.0f)) {
SHERPA_ONNX_LOGE("Silence scale %f is not in [0.01, 2]. Skip scaling.",
scale);
return *this;
}
if (!(scale >= 0.0f && scale <= 2.0f)) {
SHERPA_ONNX_LOGE("Silence scale %f is not in [0, 2]. Skip scaling.",
scale);
return *this;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sherpa-onnx/csrc/offline-tts.cc` around lines 44 - 48, Update the range
validation in ScaleSilence to allow scale = 0 by changing the lower bound from
0.01f to 0.0f, preserving the existing behavior that removes silence intervals
when scaling by zero.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants