Skip to content

fix: harden TTS reliability, installer safety, and error surfacing#4

Open
yuto-trd wants to merge 2 commits into
mainfrom
fix/voice-review-batch
Open

fix: harden TTS reliability, installer safety, and error surfacing#4
yuto-trd wants to merge 2 commits into
mainfrom
fix/voice-review-batch

Conversation

@yuto-trd

@yuto-trd yuto-trd commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

Hardening pass over the whole extension, driven by a full-project review. Each finding went through a multi-layer process: dimension-based review → adversarial verification → independent false-positive audit → implementation → code review → final audit. The branch builds clean (0 warnings / 0 errors, net10.0).

Two confirmed false positives from the review were deliberately excluded, and large/separate items (download integrity verification, resampler no-op cleanup, render-time allocations) were left out of scope and are noted as follow-ups.

Changes

Reliability (TtsTabViewModel)

  • Surface query/synthesis/play failures to the user via NotificationService instead of silently logging and returning.
  • Add an Interlocked reentrancy guard and split out CreateQueryCore so Generate/Play no longer reset IsGenerating mid-run and no longer lose the AudioQuery to a fire-and-forget Dispatcher.Post race (they now branch on a local query value).
  • Make _initTcs completion idempotent (TrySetResult on all OnLoaded paths) so ReadFromJson cannot wait forever when VOICEVOX is missing or not loaded.

Installer safety (VoiceVoxInstaller / VoiceVoxLoader)

  • Install into a temp directory and atomically swap on success after validating the result, so a failed or cancelled install never leaves a broken state behind (and the previous working install is preserved until the new one is verified).
  • Strengthen the "installed" check to validate the actual structure (core lib, OpenJTalk dict, onnxruntime lib, at least one .vvm) instead of just directory existence.
  • Add Zip Slip containment in ExtractZip (normalized prefix check, reject .. and single-segment entries).
  • Add an idle timeout to downloads so a stalled connection cannot hang the install forever.
  • Skip a single malformed model's metas (JsonException) instead of aborting all model loading; centralize native library path resolution.

Notifications / threading (TtsLoader)

  • Marshal the install notification to the UI thread, observe ContinueWith faults, and avoid the duplicate "not installed" warning when load actually failed with an error.

Audio / models

  • Clamp the float → PCM16 conversion in SimpleWavePlayer to avoid wraparound noise.
  • Mark required JSON properties as required on VoiceMetadata / VoiceStyle (clears the CS8618 warnings).

Out of scope (follow-ups)

  • Checksum/signature verification of downloaded native binaries and models.
  • WdlResamplingSampleProvider configured as a no-op (input rate == output rate).
  • Per-frame allocations in AccentPhrasePanel.Render.

Testing

  • dotnet build src/Beutl.Extensions.Voice/Beutl.Extensions.Voice.csproj -c Release → Build succeeded, 0 warnings, 0 errors.
  • Behavioral changes (synthesis, install flow, audio playback) were reviewed by static analysis; not exercised against a live VOICEVOX install in this pass.

https://claude.ai/code/session_01F5pRvtNeKb5nagNh83hWRz

Summary by CodeRabbit

  • Bug Fixes
    • Stabilized audio playback with safer PCM sample conversion (clamping).
    • Hardened VoiceVox installation and extraction with safer staging, validation, and protection against malformed archive paths, including resilient cleanup.
    • Improved voice model loading reliability, with clearer handling of invalid metadata and missing assets.
    • Refined concurrent text-to-speech generation/playback to avoid race conditions and ensure consistent busy state.
    • Enhanced user-facing error/warning reporting and safer UI updates during background loading.

Address issues found in a full-project review (verified across multiple
review/audit passes; build stays at 0 warnings / 0 errors on net10.0).

TtsTabViewModel:
- Surface synthesis/query/play failures to the user via NotificationService
  instead of silently logging and returning.
- Add an Interlocked reentrancy guard and split out CreateQueryCore so
  Generate/Play no longer reset IsGenerating mid-run and no longer lose the
  AudioQuery to a fire-and-forget Dispatcher.Post race (they now decide on a
  local query value).
- Make _initTcs completion idempotent (TrySetResult on all OnLoaded paths) so
  ReadFromJson never waits forever when VOICEVOX is missing/not loaded.

VoiceVoxInstaller:
- Install into a temp directory and atomically swap on success, validating the
  installation first, so a failed/cancelled install never leaves a broken state.
- Add Zip Slip containment in ExtractZip (normalized prefix check, reject ".."
  and single-segment entries).
- Add an idle timeout to downloads so a stalled connection cannot hang forever.

VoiceVoxLoader:
- Validate the installation structure (core/dict/onnxruntime/vvm), not just
  directory existence.
- Skip a single malformed model's metas (JsonException) instead of aborting all
  model loading.
- Centralize native library path resolution.

TtsLoader:
- Marshal the install notification to the UI thread, observe ContinueWith
  faults, and avoid the duplicate "not installed" warning on load failure.

SimpleWavePlayer:
- Clamp the float -> PCM16 conversion to avoid wraparound noise.

Models:
- Mark required JSON properties as `required` (clears CS8618 warnings).

Claude-Session: https://claude.ai/code/session_01F5pRvtNeKb5nagNh83hWRz
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 13747066-cf4c-4df5-918a-6e510678e8e6

📥 Commits

Reviewing files that changed from the base of the PR and between f5f99f2 and c6ec17a.

📒 Files selected for processing (3)
  • src/Beutl.Extensions.Voice/Services/VoiceVoxInstaller.cs
  • src/Beutl.Extensions.Voice/Services/VoiceVoxLoader.cs
  • src/Beutl.Extensions.Voice/ViewModels/TtsTabViewModel.cs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/Beutl.Extensions.Voice/Services/VoiceVoxLoader.cs
  • src/Beutl.Extensions.Voice/Services/VoiceVoxInstaller.cs
  • src/Beutl.Extensions.Voice/ViewModels/TtsTabViewModel.cs

📝 Walkthrough

Walkthrough

This PR hardens the VoiceVox extension across multiple layers: model properties are changed to C# required, PCM16 float conversion gains clamping, the installer adopts a staged temp-directory approach with HTTP timeouts and zip path-traversal validation, the loader adds installation validation and JSON error recovery, and TtsTabViewModel gains an interlocked busy guard with parameterized synthesis and improved error reporting.

Changes

VoiceVox Extension Hardening

Layer / File(s) Summary
Required model property contracts
src/Beutl.Extensions.Voice/Models/VoiceMetadata.cs, src/Beutl.Extensions.Voice/Models/VoiceStyle.cs
VoiceMetadata properties Name, Version, SpeakerUuid, Styles and VoiceStyle.Name are changed to required init, enforcing initialization at construction time.
PCM16 clamped audio conversion
src/Beutl.Extensions.Voice/Services/SimpleWavePlayer.cs
Adds a ToPcm16 helper that clamps float samples to [-1f, 1f] before scaling; replaces unclamped multiplies in both the initial buffer fill and the streaming refill loops.
Staged install, timeouts, zip path-traversal protection
src/Beutl.Extensions.Voice/Services/VoiceVoxInstaller.cs
Install stages into a temp directory, validates, then moves into place; catch path deletes temp and invalid existing directories. DownloadFile and InstallVoiceModels add HTTP header timeouts; a new CopyDownloadedStream helper adds idle-timeout detection and incremental progress. ExtractZip validates entry paths against the target root. DeleteDirectoryIfExists logs and swallows deletion errors.
Installation validation and JSON error handling in VoiceVoxLoader
src/Beutl.Extensions.Voice/Services/VoiceVoxLoader.cs
Load() now calls a new IsValidInstallation check (required libraries, open_jtalk contents, at least one .vvm). Native library paths are extracted into GetVoiceVoxCoreLibraryPath / GetOnnxRuntimeLibraryPath helpers. Voice model metadata deserialization catches JsonException, logs it, disposes the model, and continues.
TtsLoader continuation and TtsTabViewModel busy guard + synthesis refactor
src/Beutl.Extensions.Voice/TtsLoader.cs, src/Beutl.Extensions.Voice/ViewModels/TtsTabViewModel.cs
TtsLoader adds fault logging and dispatches the "not installed" UI warning onto Dispatcher.UIThread. TtsTabViewModel adds a _busy interlocked field; OnLoaded uses TrySetResult; CreateQueryCore performs granular validation with UI notifications; Generate and Play use the busy guard and build AudioQuery via CreateQueryCore when missing; SynthesisFromQuery accepts an AudioQueryModel parameter and emits warnings/errors for uninitialized state and synthesis failures.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 A rabbit hops through streams and zips,
Clamping floats with careful tips,
Staged installs move with temp-dir grace,
No path traversal can give chase.
Busy guards keep queries in line—
Every voice now works just fine! 🎤

🚥 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 'fix: harden TTS reliability, installer safety, and error surfacing' accurately reflects the main changes across the PR, covering the primary objectives of improving reliability, safety, and error handling.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/voice-review-batch

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

@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: 3

🤖 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 `@src/Beutl.Extensions.Voice/Services/VoiceVoxInstaller.cs`:
- Around line 54-60: The current code deletes the existing voicevoxHomePath
before calling MoveDirectoryCrossDevice to activate the staged directory. If
MoveDirectoryCrossDevice fails after the deletion, the user will lose their
working installation. Reorder the operations so that Directory.Delete is called
only after the MoveDirectoryCrossDevice operation completes successfully. This
ensures that if the move operation fails, the previous installation remains
intact and can still be used.

In `@src/Beutl.Extensions.Voice/Services/VoiceVoxLoader.cs`:
- Around line 42-55: The validation in line 42 uses the generic
HasDirectoryEntries method to check the open_jtalk directory, which only
verifies the directory is non-empty but does not validate that required
dictionary artifacts are present. Replace this validation by creating a new
method that specifically checks for the required dictionary files or
subdirectories within the open_jtalk folder (such as checking for required
dictionary files with specific extensions or known subdirectory names). Update
the validation logic where HasDirectoryEntries is called for the open_jtalk path
to use this new specialized method instead, ensuring that only properly
structured open_jtalk directories with all necessary artifacts are accepted as
valid.

In `@src/Beutl.Extensions.Voice/ViewModels/TtsTabViewModel.cs`:
- Around line 93-129: The OnLoaded method in TtsTabViewModel calls
_initTcs.TrySetResult() only at the end, but if voice metadata materialization
throws an exception during the LINQ operations and GroupBy call, the completion
never occurs and code waiting on _initTcs.Task will hang indefinitely. Refactor
the OnLoaded method to wrap its entire body in a try-finally block and move the
_initTcs.TrySetResult() call into the finally block to ensure completion is
guaranteed on all code paths, regardless of exceptions.
🪄 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 Plus

Run ID: 284ea368-d517-4295-b6a3-93c435e81d89

📥 Commits

Reviewing files that changed from the base of the PR and between 7f9f1e2 and f5f99f2.

📒 Files selected for processing (7)
  • src/Beutl.Extensions.Voice/Models/VoiceMetadata.cs
  • src/Beutl.Extensions.Voice/Models/VoiceStyle.cs
  • src/Beutl.Extensions.Voice/Services/SimpleWavePlayer.cs
  • src/Beutl.Extensions.Voice/Services/VoiceVoxInstaller.cs
  • src/Beutl.Extensions.Voice/Services/VoiceVoxLoader.cs
  • src/Beutl.Extensions.Voice/TtsLoader.cs
  • src/Beutl.Extensions.Voice/ViewModels/TtsTabViewModel.cs

Comment thread src/Beutl.Extensions.Voice/Services/VoiceVoxInstaller.cs Outdated
Comment thread src/Beutl.Extensions.Voice/Services/VoiceVoxLoader.cs Outdated
Comment thread src/Beutl.Extensions.Voice/ViewModels/TtsTabViewModel.cs Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f5f99f2983

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Beutl.Extensions.Voice/ViewModels/TtsTabViewModel.cs Outdated
Comment thread src/Beutl.Extensions.Voice/Services/VoiceVoxInstaller.cs Outdated
…eed init

Follow-up to automated review feedback (CodeRabbit / Codex) on the PR:

- VoiceVoxInstaller: make the staged-directory activation rollback-safe.
  Rename the existing install to a backup before moving the staged build
  into place, restore it if the move fails, and only delete the backup
  after a successful swap — so a failed update can no longer leave the
  user without a working installation.
- VoiceVoxLoader: validate the open_jtalk dictionary by required artifacts
  (sys.dic, char.bin, matrix.bin, unk.dic) instead of only checking that
  the directory is non-empty, so a half-extracted dictionary is not
  accepted as a valid install.
- TtsTabViewModel.OnLoaded: wrap the body in try/catch/finally and complete
  _initTcs from the finally so ReadFromJson can never hang awaiting it,
  even if voice-metadata materialization throws.

Build: 0 warnings, 0 errors (net10.0).

Claude-Session: https://claude.ai/code/session_01F5pRvtNeKb5nagNh83hWRz

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6ec17a7fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +67 to +68
DeleteDirectoryIfExists(backupVoicevoxHomePath);
backupVoicevoxHomePath = null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep backup until the new install loads

In the replace-existing-install path, the backup is deleted immediately after the filesystem swap, before TtsLoader.StaticLoad() verifies that the new native libraries and models can actually be loaded. If the new installation is structurally valid but Load() fails (for example due to an incompatible native library or bad model metadata), the catch path records the error but can no longer restore the previously working install, leaving the user stuck on a failing replacement. Keep the backup until after StaticLoad() succeeds and restore it on load failure.

Useful? React with 👍 / 👎.

Comment on lines +171 to +175
catch (JsonException ex)
{
_logger.LogError(ex, "Failed to deserialize VoiceMetadata: {Path}", path);
voiceModel.Dispose();
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject loads with no usable voice metadata

When every non-song .vvm has malformed metadata, this new JsonException path skips each model and the method still falls through to InitializationTcs.TrySetResult(true)/IsLoaded = true with an empty VoiceSets. Because the install validator only requires that at least one .vvm file exists, a corrupted or incompatible model set is reported as loaded and the UI becomes enabled with no selectable voices instead of surfacing the bad install; fail the load if no usable voice metadata remains after skipping malformed models.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant