Domain layer

The modules under addon/synthDrivers/dengjen_neural_voices/domain/, plus the two persistence modules beside them. The domain layer talks to engines only through the TTSBackend port (see Speech pipeline); the NVDA driver and the gRPC adapter sit on either side.

Module Owns

domain/tts_system.py

Voices, speech options and the system that ties them together.

domain/model_profiles.py

Which tuning sliders a model type supports, and the engine key behind each.

domain/voice_metadata.py

The voice.json sidecar that identifies an installed voice.

domain/app_profiles.py

Per-application voice settings.

domain/structural_reading.py

Splitting text so quoted or bracketed passages use a second speaker.

domain/audio_processing.py

Normalisation, night mode and stereo panning of PCM.

domain/phrase_cache.py

The PCM cache, described in Speech pipeline.

_config.py

NVDA configuration for the add-on.

voice_migration.py

Moving voices across the 4.0.0 rename.

Voices and the speech system

DengjenVoice represents one installed voice.

  • DengjenVoice.from_path(path, backend) takes the key from the directory name and the rest of its identity from the voice’s voice.json. For Piper voices it reads the quality from the last segment of a <language>-<name>-<quality> key.

  • begin_load() picks the voice’s JSON config file, preferring config.json and ignoring voice.json, and asks the backend to load it on a worker thread. It returns a future that concurrent callers share; a failed load is not cached, so the next call retries. load() waits on that future, and reading a load-dependent attribute such as sample_rate does the same. is_loaded is true once the result is applied. The result supplies the backend voice id, sample rate, speakers, whether streaming output is supported, and the default scales (from model_profiles).

  • synthesize() forwards to the backend. A voice never calls the engine directly.

SpeechOptions holds the voice and the rate, volume, pitch and sentence silence for one utterance. Assigning a voice starts its load (voice.begin_load()) and returns without waiting. DengjenTextToSpeechSystem.create_speech_provider(text) hands each SpeechProvider a copy of the current options, so changing the rate, volume, pitch or selected voice after a task is queued does not alter that task. The copy is shallow: the voice object itself is shared, which is why SpeechTask sets and restores the speaker around its own run. create_break_provider(ms) returns a SilenceProvider for the voice’s sample rate.

Voices are discovered by load_all_voices_from_nvda_config_dir(backend). It first runs migrate_voices_directory(), then scans the Piper voices directory and the Kokoro voices directory and returns the voices sorted by key. Directories that do not parse as a voice are skipped.

Model profiles

profile_for(model_type) returns a ModelProfile listing which of length_scale, noise_scale and noise_w apply, and which engine key each maps to. piper and melotts tune all three; MeloTTS sends noise_w as the backend parameter noise_scale_w. Any other model type is not tunable, so its sliders have no effect. Add a profile here when a new backend exposes tunables.

Voice metadata

voice.json holds model_type, name, language and description, all strings. It exists so NVDA-facing identity does not depend on an engine’s own config format or on Piper’s directory-name convention. read_or_migrate() reads the sidecar; when it is missing or malformed, it falls back to parsing a Piper-style directory name and writes the sidecar so later reads skip the fallback. A name that does not parse raises ValueError.

App profiles

AppProfileManager stores per-application settings (voice, variant, speaker, rate, volume, pitch) in NVDA’s configuration under speech/dengjen_neural_voices/app_profiles, keyed by the lower-cased executable name. Rate, volume and pitch are clamped to 0-100.

The global plugin applies them: on focus change it looks up the profile for the focused application, first restores the settings it previously overrode, then applies the new profile and remembers the values it replaced. The domain module only reads, writes and applies profile data; it does not watch focus.

Structural reading

split_into_segments(text, default_speaker, alt_speaker) finds passages inside ( ), [ ], " " or 「 」 of up to 200 characters, merges overlapping spans, and returns segments that alternate between the two speakers. The driver uses it only when the Structural reading setting is on and the current voice has at least two speakers.

Audio processing

AudioStreamProcessor post-processes PCM chunks while keeping state across an utterance, so gain and filter changes do not click at chunk boundaries. It does the following:

  • Normalisation towards an RMS of 0.20 with a peak ceiling of 0.95.

  • Night mode: a gain of 0.55 and damping of high frequencies.

  • Spatial audio: stereo output with an equal-power pan by pan, a value from -1 to 1 that the driver derives from the focused object’s horizontal position on screen.

flush() returns whatever the processor still holds at the end of an utterance.

Configuration and migration

DengjenConfig (_config.py) extends NVDA’s speech configuration section with dengjen_neural_voices. Its spec defines per-voice variant, speaker and the three scales (0-100, default 50), per-language preferred voices, whether the user declined the language offer, and the download URLs under hosting.

voice_migration.py carries voices across the 4.0.0 rename from Sonata Neural Voices. When Sonata is no longer installed it moves its voices folder to the new location, retrying on every voice scan until the move succeeds. When Sonata is still installed it leaves the folder alone, and the voice manager copies voices on request. The module imports nothing from its own package, because installTasks.py loads it by file path.