11using System ;
22using System . Collections . Generic ;
33using System . Diagnostics ;
4- using System . IO ;
54using System . Linq ;
65using System . Threading ;
76using System . Threading . Tasks ;
@@ -28,15 +27,11 @@ public sealed class VoiceService : IAsyncDisposable
2827 private readonly IOpenClawLogger _logger ;
2928 private readonly SettingsManager _settings ;
3029 private readonly SpeechToTextService _stt ;
31- private readonly VoiceActivityDetector _vad ;
3230 private readonly WhisperModelManager _modelManager ;
3331 private AudioPipeline ? _pipeline ;
3432 private VoiceMode _currentMode = VoiceMode . Inactive ;
3533 private CancellationTokenSource ? _sessionCts ;
3634
37- // Path to the bundled Silero VAD model (deployed with the app)
38- private string ? _vadModelPath ;
39-
4035 /// <summary>Current voice interaction mode.</summary>
4136 public VoiceMode CurrentMode => _currentMode ;
4237
@@ -106,7 +101,6 @@ public VoiceService(IOpenClawLogger logger, SettingsManager settings)
106101 _logger = logger ;
107102 _settings = settings ;
108103 _stt = new SpeechToTextService ( logger ) ;
109- _vad = new VoiceActivityDetector ( logger ) ;
110104 _modelManager = new WhisperModelManager ( SettingsManager . SettingsDirectoryPath , logger ) ;
111105 }
112106
@@ -118,26 +112,6 @@ public async Task InitializeAsync(
118112 IProgress < ( long downloaded , long total ) > ? downloadProgress = null ,
119113 CancellationToken cancellationToken = default )
120114 {
121- // Load VAD model
122- if ( ! _vad . IsLoaded )
123- {
124- var vadPath = FindVadModelPath ( ) ;
125- if ( vadPath == null )
126- {
127- // Auto-download Silero VAD model
128- DiagnosticMessage ? . Invoke ( "Downloading voice activity model…" ) ;
129- vadPath = await DownloadVadModelAsync ( cancellationToken ) ;
130- }
131- if ( vadPath != null )
132- {
133- _vad . LoadModel ( vadPath ) ;
134- }
135- else
136- {
137- _logger . Info ( "Silero VAD model not found — VAD will be unavailable" ) ;
138- }
139- }
140-
141115 // Download Whisper model if needed
142116 var modelName = _settings . SttModelName ;
143117 if ( ! _modelManager . IsModelDownloaded ( modelName ) )
@@ -172,7 +146,7 @@ public async Task StartPushToTalkAsync()
172146 SetMode ( VoiceMode . PushToTalk ) ;
173147
174148 _sessionCts = new CancellationTokenSource ( ) ;
175- _pipeline = new AudioPipeline ( _logger , _stt , _vad ) ;
149+ _pipeline = new AudioPipeline ( _logger , _stt ) ;
176150 WirePipelineEvents ( _pipeline ) ;
177151
178152 var options = new AudioPipelineOptions
@@ -214,7 +188,7 @@ public async Task StartVoiceChatAsync()
214188 SetMode ( VoiceMode . VoiceChat ) ;
215189
216190 _sessionCts = new CancellationTokenSource ( ) ;
217- _pipeline = new AudioPipeline ( _logger , _stt , _vad ) ;
191+ _pipeline = new AudioPipeline ( _logger , _stt ) ;
218192 WirePipelineEvents ( _pipeline ) ;
219193
220194 var options = new AudioPipelineOptions
@@ -276,7 +250,7 @@ public async Task<SttListenResult> ListenOnceAsync(SttListenArgs args, Cancellat
276250 using var timeoutCts = new CancellationTokenSource ( args . TimeoutMs ) ;
277251 using var linkedCts = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken , timeoutCts . Token ) ;
278252
279- var pipeline = new AudioPipeline ( _logger , _stt , _vad ) ;
253+ var pipeline = new AudioPipeline ( _logger , _stt ) ;
280254 WirePipelineEvents ( pipeline ) ;
281255 var tcs = new TaskCompletionSource < SttListenResult > ( ) ;
282256 var sw = Stopwatch . StartNew ( ) ;
@@ -368,7 +342,7 @@ public async Task<SttTranscribeResult> TranscribeFixedDurationAsync(
368342
369343 await EnsureInitializedAsync ( ) ;
370344
371- var pipeline = new AudioPipeline ( _logger , _stt , _vad ) ;
345+ var pipeline = new AudioPipeline ( _logger , _stt ) ;
372346 var sw = Stopwatch . StartNew ( ) ;
373347 try
374348 {
@@ -504,91 +478,9 @@ private void SetMode(VoiceMode mode)
504478 _logger . Info ( $ "Voice mode changed: { mode } ") ;
505479 }
506480
507- /// <summary>
508- /// Locate the Silero VAD ONNX model. Looks in the app's Assets folder
509- /// and the models directory.
510- /// </summary>
511- private string ? FindVadModelPath ( )
512- {
513- if ( _vadModelPath != null && File . Exists ( _vadModelPath ) )
514- return _vadModelPath ;
515-
516- // Check Assets directory (deployed with the app)
517- var assetsPath = Path . Combine ( AppContext . BaseDirectory , "Assets" , "silero_vad.onnx" ) ;
518- if ( File . Exists ( assetsPath ) )
519- {
520- _vadModelPath = assetsPath ;
521- return assetsPath ;
522- }
523-
524- // Check models directory
525- var modelsPath = Path . Combine ( SettingsManager . SettingsDirectoryPath , "models" , "silero_vad.onnx" ) ;
526- if ( File . Exists ( modelsPath ) )
527- {
528- _vadModelPath = modelsPath ;
529- return modelsPath ;
530- }
531-
532- return null ;
533- }
534-
535- private const string VadDownloadUrl = SileroVadModelManifest . DownloadUrl ;
536-
537- /// <summary>Download the Silero VAD ONNX model if not already present.
538- /// SHA-256 is verified before the atomic rename so a tampered or
539- /// truncated file never lands at the canonical path. See
540- /// <see cref="SileroVadModelManifest"/>.</summary>
541- private async Task < string ? > DownloadVadModelAsync ( CancellationToken cancellationToken )
542- {
543- var destPath = Path . Combine ( SettingsManager . SettingsDirectoryPath , "models" , SileroVadModelManifest . FileName ) ;
544- if ( File . Exists ( destPath ) )
545- return destPath ;
546-
547- _logger . Info ( "Downloading Silero VAD model..." ) ;
548- Directory . CreateDirectory ( Path . GetDirectoryName ( destPath ) ! ) ;
549- var tempPath = destPath + ".tmp" ;
550- try
551- {
552- using var http = new System . Net . Http . HttpClient ( ) ;
553- http . Timeout = TimeSpan . FromMinutes ( 5 ) ;
554- using var response = await http . GetAsync ( VadDownloadUrl , cancellationToken ) ;
555- response . EnsureSuccessStatusCode ( ) ;
556- using ( var fs = new FileStream ( tempPath , FileMode . Create , FileAccess . Write , FileShare . None ) )
557- {
558- await response . Content . CopyToAsync ( fs , cancellationToken ) ;
559- }
560-
561- // SECURITY: verify SHA-256 BEFORE the atomic rename so a
562- // tampered file never reaches ONNX Runtime.
563- using ( var sha = System . Security . Cryptography . SHA256 . Create ( ) )
564- using ( var verifyStream = new FileStream ( tempPath , FileMode . Open , FileAccess . Read , FileShare . Read , 81920 , useAsync : true ) )
565- {
566- var actual = await sha . ComputeHashAsync ( verifyStream , cancellationToken ) ;
567- var actualHex = Convert . ToHexString ( actual ) . ToLowerInvariant ( ) ;
568- if ( ! string . Equals ( actualHex , SileroVadModelManifest . Sha256 , StringComparison . OrdinalIgnoreCase ) )
569- {
570- throw new System . Security . SecurityException (
571- "Silero VAD model failed integrity check. The downloaded file does not match the pinned SHA-256." ) ;
572- }
573- }
574-
575- File . Move ( tempPath , destPath , overwrite : true ) ;
576- _logger . Info ( $ "Silero VAD model downloaded and verified ({ new FileInfo ( destPath ) . Length : N0} bytes)") ;
577- _vadModelPath = destPath ;
578- return destPath ;
579- }
580- catch ( Exception ex )
581- {
582- _logger . Error ( "Failed to download VAD model" , ex ) ;
583- try { if ( File . Exists ( tempPath ) ) File . Delete ( tempPath ) ; } catch { }
584- return null ;
585- }
586- }
587-
588481 public async ValueTask DisposeAsync ( )
589482 {
590483 await StopAsync ( ) ;
591484 _stt . Dispose ( ) ;
592- _vad . Dispose ( ) ;
593485 }
594486}
0 commit comments