Skip to content

Commit c35a0ed

Browse files
authored
Separate speech capability controls from voice settings (#947)
Move STT/TTS capability toggles to Permissions while keeping speech provider/model/voice setup in Voice & Audio. Disabled voice input now routes users to Permissions; missing speech model setup routes users to Voice & Audio. Validation: local build; GitHub Build and Test, CodeQL, Socket checks all passed. Live UX proof confirmed disabled STT opens Permissions, missing model opens Voice & Audio, and Voice & Audio setup controls remain available.
1 parent c3371d6 commit c35a0ed

16 files changed

Lines changed: 758 additions & 596 deletions

src/OpenClaw.Tray.WinUI/App.xaml.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2772,8 +2772,8 @@ private void OnGatewayNotificationReceived(object? sender, OpenClawNotification
27722772
// });
27732773
// }
27742774

2775-
// TTS: read response aloud whenever the toggle is on (any chat surface).
2776-
if (_settings?.VoiceTtsEnabled == true)
2775+
// TTS: read response aloud whenever chat TTS is enabled and ready (any chat surface).
2776+
if (SpeechSetupReadiness.IsAutomaticChatTtsEnabled(_settings))
27772777
{
27782778
_ = (_chatCoordinator?.SpeakResponseAsync(speechText) ?? Task.CompletedTask);
27792779
}

src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public sealed partial class ChatPage : Page
3333
private bool _webViewInitialized;
3434
private bool _webViewMode;
3535
private bool _pageActive;
36+
private readonly SemaphoreSlim _speakerMuteGate = new(1, 1);
37+
private int _voiceSettingsDialogOpen;
3638
private bool _navigationStarted;
3739
private CancellationTokenSource? _navigationCts;
3840
private global::Windows.Foundation.TypedEventHandler<CoreWebView2, CoreWebView2NavigationCompletedEventArgs>? _navCompletedHandler;
@@ -232,7 +234,7 @@ private void ShowFunctionalSurface()
232234

233235
var app = App.Current as App;
234236
var provider = ResolveChatProvider(app);
235-
Func<string, Task>? readAloud = app is null ? null : app.SpeakChatTextAsync;
237+
Func<string, Task>? readAloud = app is null ? null : ReadChatTextAloudAsync;
236238

237239
// Consume a pending session-key hand-off from SessionsPage or a
238240
// notification toast so the chat root mounts with that thread selected.
@@ -292,8 +294,8 @@ private void ShowFunctionalSurface()
292294
onVoiceRequest: VoiceTranscribeAsync,
293295
onAttachClick: OnAttachClicked,
294296
onSettingsClick: () => _hub?.NavigateTo("voice"),
295-
onSpeakerMuteChanged: muted => (App.Current as App)?.SetChatSpeakerMuted(muted),
296-
initialMuted: CurrentApp.Settings?.VoiceTtsEnabled == false,
297+
onSpeakerMuteChanged: muted => _ = OnSpeakerMuteChangedAsync(muted),
298+
initialMuted: ShouldStartSpeakerMuted(CurrentApp.Settings),
297299
suppressAutoDispose: true);
298300
_mountedProvider = provider;
299301
_mountedThreadId = threadIdToMount;
@@ -829,7 +831,8 @@ private void OnRetryChat(object sender, RoutedEventArgs e)
829831
await ShowVoiceSettingsDialogAsync(
830832
LocalizationHelper.GetString("ChatVoiceDialog_InputOffTitle"),
831833
LocalizationHelper.GetString("ChatVoiceDialog_InputOffMessage"),
832-
NavigateToVoiceSettings);
834+
LocalizationHelper.GetString("ChatVoiceDialog_OpenPermissionsSettings"),
835+
NavigateToPermissionsSettings);
833836
return null;
834837
}
835838

@@ -840,7 +843,8 @@ await ShowVoiceSettingsDialogAsync(
840843
await ShowVoiceSettingsDialogAsync(
841844
LocalizationHelper.GetString("ChatVoiceDialog_InputOffTitle"),
842845
LocalizationHelper.GetString("ChatVoiceDialog_InputOffMessage"),
843-
NavigateToVoiceSettings);
846+
LocalizationHelper.GetString("ChatVoiceDialog_OpenPermissionsSettings"),
847+
NavigateToPermissionsSettings);
844848
return null;
845849
}
846850

@@ -850,6 +854,7 @@ await ShowVoiceSettingsDialogAsync(
850854
await ShowVoiceSettingsDialogAsync(
851855
LocalizationHelper.GetString("ChatVoiceDialog_ModelRequiredTitle"),
852856
LocalizationHelper.GetString("ChatVoiceDialog_ModelRequiredMessage"),
857+
LocalizationHelper.GetString("ChatVoiceDialog_OpenVoiceSettings"),
853858
NavigateToVoiceSettings);
854859
return null;
855860
}
@@ -880,8 +885,80 @@ await ShowVoiceSettingsDialogAsync(
880885
}
881886
}
882887

883-
private async Task ShowVoiceSettingsDialogAsync(string title, string message, Action openVoiceSettings)
888+
private async Task ReadChatTextAloudAsync(string text)
889+
{
890+
if (!await EnsureTtsReadyForChatAsync())
891+
return;
892+
893+
await CurrentApp.SpeakChatTextAsync(text);
894+
}
895+
896+
private async Task OnSpeakerMuteChangedAsync(bool muted)
897+
{
898+
if (!await _speakerMuteGate.WaitAsync(0))
899+
return;
900+
901+
try
902+
{
903+
if (muted)
904+
{
905+
(App.Current as App)?.SetChatSpeakerMuted(true);
906+
return;
907+
}
908+
909+
if (IsTtsReadyForChat())
910+
{
911+
(App.Current as App)?.SetChatSpeakerMuted(false);
912+
return;
913+
}
914+
915+
(App.Current as App)?.SetChatSpeakerMuted(true);
916+
_functionalHost?.SetSpeakerMuted(true);
917+
await ShowTtsUnavailableDialogAsync();
918+
}
919+
catch (Exception ex)
920+
{
921+
Logger.Warn($"Speaker mute change failed: {ex.Message}");
922+
}
923+
finally
924+
{
925+
_speakerMuteGate.Release();
926+
}
927+
}
928+
929+
private async Task<bool> EnsureTtsReadyForChatAsync()
930+
{
931+
if (IsTtsReadyForChat())
932+
return true;
933+
934+
await ShowTtsUnavailableDialogAsync();
935+
return false;
936+
}
937+
938+
private static bool IsTtsReadyForChat()
939+
{
940+
return SpeechSetupReadiness.IsChatTtsPlaybackReady(CurrentApp.Settings);
941+
}
942+
943+
private async Task ShowTtsUnavailableDialogAsync()
944+
{
945+
await ShowVoiceSettingsDialogAsync(
946+
LocalizationHelper.GetString("ChatVoiceDialog_OutputOffTitle"),
947+
LocalizationHelper.GetString("ChatVoiceDialog_OutputOffMessage"),
948+
LocalizationHelper.GetString("ChatVoiceDialog_OpenPermissionsSettings"),
949+
NavigateToPermissionsSettings);
950+
}
951+
952+
private static bool ShouldStartSpeakerMuted(SettingsManager? settings)
953+
{
954+
return !SpeechSetupReadiness.IsAutomaticChatTtsEnabled(settings);
955+
}
956+
957+
private async Task ShowVoiceSettingsDialogAsync(string title, string message, string primaryButtonText, Action openSettings)
884958
{
959+
if (Interlocked.Exchange(ref _voiceSettingsDialogOpen, 1) == 1)
960+
return;
961+
885962
var tcs = new TaskCompletionSource();
886963
if (DispatcherQueue is null || !DispatcherQueue.TryEnqueue(async () =>
887964
{
@@ -891,7 +968,7 @@ private async Task ShowVoiceSettingsDialogAsync(string title, string message, Ac
891968
{
892969
Title = title,
893970
Content = message,
894-
PrimaryButtonText = LocalizationHelper.GetString("ChatVoiceDialog_OpenVoiceSettings"),
971+
PrimaryButtonText = primaryButtonText,
895972
CloseButtonText = LocalizationHelper.GetString("ChatVoiceDialog_Dismiss"),
896973
DefaultButton = ContentDialogButton.Primary,
897974
XamlRoot = Content?.XamlRoot
@@ -912,7 +989,7 @@ private async Task ShowVoiceSettingsDialogAsync(string title, string message, Ac
912989
};
913990

914991
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
915-
openVoiceSettings();
992+
openSettings();
916993
}
917994
catch (InvalidOperationException ex)
918995
{
@@ -924,10 +1001,18 @@ private async Task ShowVoiceSettingsDialogAsync(string title, string message, Ac
9241001
}
9251002
}))
9261003
{
1004+
Interlocked.Exchange(ref _voiceSettingsDialogOpen, 0);
9271005
return;
9281006
}
9291007

930-
await tcs.Task;
1008+
try
1009+
{
1010+
await tcs.Task;
1011+
}
1012+
finally
1013+
{
1014+
Interlocked.Exchange(ref _voiceSettingsDialogOpen, 0);
1015+
}
9311016
}
9321017

9331018
private void NavigateToVoiceSettings()
@@ -938,6 +1023,14 @@ private void NavigateToVoiceSettings()
9381023
(App.Current as App)?.ShowHub("voice");
9391024
}
9401025

1026+
private void NavigateToPermissionsSettings()
1027+
{
1028+
if (_hub is not null)
1029+
_hub.NavigateTo("permissions");
1030+
else
1031+
(App.Current as App)?.ShowHub("permissions");
1032+
}
1033+
9411034
private void OnAttachClicked()
9421035
{
9431036
Logger.Info("[ChatPage] OnAttachClicked invoked");

src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml

Lines changed: 25 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -99,61 +99,35 @@
9999
</ItemsRepeater.Layout>
100100
</ItemsRepeater>
101101

102-
<!-- ═════════ Speech-to-text engine details (only when STT capability is on) ═════════ -->
103-
<Border x:Name="SttCard" Background="{ThemeResource SubtleFillColorSecondaryBrush}"
102+
<!-- ═════════ Voice settings link (only when STT or TTS capability is on) ═════════ -->
103+
<Border x:Name="VoiceSettingsCard" Background="{ThemeResource SubtleFillColorSecondaryBrush}"
104104
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
105105
BorderThickness="1" CornerRadius="8" Padding="16,10,16,12" Margin="0,-4,0,0"
106-
Visibility="Collapsed">
107-
<StackPanel Orientation="Horizontal" Spacing="10">
108-
<FontIcon Glyph="&#xE946;" FontSize="14" VerticalAlignment="Top" Margin="0,3,0,0"
109-
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
110-
<StackPanel Spacing="4">
111-
<TextBlock x:Name="SttEngineHint"
106+
Visibility="Collapsed"
107+
AutomationProperties.AutomationId="PermissionsVoiceSettingsCard">
108+
<StackPanel Spacing="6">
109+
<StackPanel x:Name="VoiceSettingsHelpPanel"
110+
Orientation="Horizontal"
111+
Spacing="8"
112+
Visibility="Collapsed">
113+
<FontIcon x:Name="VoiceSettingsWarningIcon"
114+
Glyph="&#xE7BA;"
115+
FontSize="14"
116+
Margin="0,1,0,0"
117+
VerticalAlignment="Top"
118+
Foreground="{ThemeResource SystemFillColorCautionBrush}"
119+
AutomationProperties.AccessibilityView="Raw"/>
120+
<TextBlock x:Name="VoiceSettingsHelpText"
121+
Text=""
112122
Style="{StaticResource CaptionTextBlockStyle}"
113-
Foreground="{ThemeResource TextFillColorSecondaryBrush}" TextWrapping="Wrap"/>
114-
<HyperlinkButton x:Name="SttMoreSettingsLink"
115-
x:Uid="PermissionsPage_SttMoreSettingsLink"
116-
Content="More voice settings…"
117-
Click="OnSttMoreSettingsClick"
118-
Padding="0"/>
119-
</StackPanel>
120-
</StackPanel>
121-
</Border>
122-
123-
<!-- ═════════ Text-to-speech engine details (only when TTS capability is on) ═════════ -->
124-
<Border x:Name="TtsCard" Background="{ThemeResource SubtleFillColorSecondaryBrush}"
125-
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
126-
BorderThickness="1" CornerRadius="8" Padding="16,10,16,12" Margin="0,-4,0,0"
127-
Visibility="Collapsed">
128-
<StackPanel Spacing="8">
129-
<ComboBox x:Name="TtsProviderComboBox" x:Uid="PermissionsPage_TtsProviderComboBox" Header="Provider"
130-
SelectionChanged="OnTtsProviderSelectionChanged">
131-
<ComboBoxItem x:Uid="PermissionsPage_TtsProviderPiper" Content="Piper (local ML, recommended)" Tag="piper"/>
132-
<ComboBoxItem x:Uid="PermissionsPage_TtsProviderWindows" Content="Windows built-in speech" Tag="windows"/>
133-
<ComboBoxItem x:Uid="PermissionsPage_TtsProviderElevenLabs" Content="ElevenLabs" Tag="elevenlabs"/>
134-
</ComboBox>
135-
<StackPanel x:Name="TtsElevenLabsPanel" Spacing="6" Visibility="Collapsed">
136-
<PasswordBox x:Name="TtsElevenLabsApiKeyBox"
137-
x:Uid="PermissionsPage_TtsElevenLabsApiKey"
138-
Header="ElevenLabs API key"
139-
LostFocus="OnTtsElevenLabsCommitted"/>
140-
<TextBox x:Name="TtsElevenLabsVoiceIdBox"
141-
x:Uid="PermissionsPage_TtsElevenLabsVoiceId"
142-
Header="ElevenLabs voice ID"
143-
LostFocus="OnTtsElevenLabsCommitted"/>
144-
<TextBox x:Name="TtsElevenLabsModelBox"
145-
x:Uid="PermissionsPage_TtsElevenLabsModel"
146-
Header="ElevenLabs model"
147-
PlaceholderText="eleven_multilingual_v2"
148-
LostFocus="OnTtsElevenLabsCommitted"/>
149-
<TextBlock x:Uid="PermissionsPage_TtsElevenLabsHelp"
150-
Text="API key is encrypted at rest with Windows DPAPI. Leave blank to keep the previously saved value when you change other fields."
151-
Style="{StaticResource CaptionTextBlockStyle}"
152-
Foreground="{ThemeResource TextFillColorSecondaryBrush}" TextWrapping="Wrap"/>
123+
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
124+
TextWrapping="Wrap"/>
153125
</StackPanel>
154-
<TextBlock x:Name="TtsStatusText"
155-
Style="{StaticResource CaptionTextBlockStyle}"
156-
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
126+
<HyperlinkButton x:Name="VoiceSettingsLink"
127+
x:Uid="PermissionsPage_VoiceSettingsLink"
128+
Content="Voice &amp; Audio settings"
129+
Click="OnVoiceSettingsClick"
130+
Padding="0"/>
157131
</StackPanel>
158132
</Border>
159133

0 commit comments

Comments
 (0)