Skip to content

Commit 0768201

Browse files
devohmycodeclaude
andcommitted
feat: add Groq Cloud Whisper Large v3 and v3 Turbo voice models
Add Groq API key input (DPAPI-secured) in the voice model settings panel to enable cloud-based speech-to-text via Groq's Whisper Large v3 and Whisper Large v3 Turbo models. Fix NoteWindow mic button not working with Groq models by passing the API key to the recognizer factory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cc826a5 commit 0768201

6 files changed

Lines changed: 440 additions & 14 deletions

File tree

‎ActionPanel.cs‎

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,8 @@ internal static Button CreateButton(ActionItem action, Action handler)
638638

639639
public static void ShowVoiceModelsPanel(Flyout flyout, string? currentModelId,
640640
XamlRoot xamlRoot,
641-
Action<SttModelInfo> onSelectModel, Action<SttModelInfo> onDeleteModel, Action onBack, Action onRebuild)
641+
Action<SttModelInfo> onSelectModel, Action<SttModelInfo> onDeleteModel, Action onBack, Action onRebuild,
642+
AiManager? aiManager = null)
642643
{
643644
var panel = CreateSubPanelWithHeader(Lang.T("voice_model"), onBack);
644645

@@ -664,9 +665,159 @@ public static void ShowVoiceModelsPanel(Flyout flyout, string? currentModelId,
664665
foreach (var model in SttModels.Available.Where(m => m.Languages == "Anglais"))
665666
panel.Children.Add(CreateModelItem(model, currentModelId, flyout, xamlRoot, onSelectModel, onDeleteModel, onRebuild));
666667

668+
// Groq Cloud section
669+
if (aiManager != null)
670+
{
671+
panel.Children.Add(CreateSeparator());
672+
panel.Children.Add(new TextBlock
673+
{
674+
Text = Lang.T("groq_cloud_section"),
675+
Style = (Style)Application.Current.Resources["CaptionTextBlockStyle"],
676+
Opacity = 0.6,
677+
Margin = new Thickness(10, 8, 0, 4)
678+
});
679+
680+
var hasKey = aiManager.HasApiKey("groq");
681+
682+
// API key input
683+
var keyBlock = new StackPanel { Margin = new Thickness(10, 4, 10, 4), Spacing = 3 };
684+
685+
var nameRow = new Grid();
686+
nameRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
687+
nameRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
688+
689+
var nameText = new TextBlock
690+
{
691+
Text = Lang.T("groq_api_key"),
692+
FontSize = 12,
693+
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
694+
VerticalAlignment = VerticalAlignment.Center,
695+
};
696+
Grid.SetColumn(nameText, 0);
697+
698+
var statusIcon = new FontIcon
699+
{
700+
Glyph = hasKey ? "\uE73E" : "\uE785",
701+
FontSize = 12,
702+
Foreground = hasKey
703+
? new SolidColorBrush(Windows.UI.Color.FromArgb(255, 108, 203, 95))
704+
: (Brush)Application.Current.Resources["TextFillColorTertiaryBrush"],
705+
VerticalAlignment = VerticalAlignment.Center,
706+
};
707+
Grid.SetColumn(statusIcon, 1);
708+
nameRow.Children.Add(nameText);
709+
nameRow.Children.Add(statusIcon);
710+
keyBlock.Children.Add(nameRow);
711+
712+
var keyRow = new Grid();
713+
keyRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
714+
keyRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
715+
716+
var keyBox = new PasswordBox
717+
{
718+
PlaceholderText = Lang.T("groq_api_key_placeholder"),
719+
Password = aiManager.GetApiKey("groq"),
720+
FontSize = 11,
721+
MaxWidth = 200,
722+
HorizontalAlignment = HorizontalAlignment.Stretch,
723+
};
724+
Grid.SetColumn(keyBox, 0);
725+
726+
var saveKeyBtn = new Button
727+
{
728+
Content = new FontIcon { Glyph = "\uE73E", FontSize = 12 },
729+
Padding = new Thickness(8, 4, 8, 4),
730+
Margin = new Thickness(4, 0, 0, 0),
731+
};
732+
Grid.SetColumn(saveKeyBtn, 1);
733+
734+
saveKeyBtn.Click += (_, _) =>
735+
{
736+
aiManager.SetApiKey("groq", keyBox.Password);
737+
var nowHasKey = aiManager.HasApiKey("groq");
738+
statusIcon.Glyph = nowHasKey ? "\uE73E" : "\uE785";
739+
statusIcon.Foreground = nowHasKey
740+
? new SolidColorBrush(Windows.UI.Color.FromArgb(255, 108, 203, 95))
741+
: (Brush)Application.Current.Resources["TextFillColorTertiaryBrush"];
742+
onRebuild();
743+
};
744+
745+
keyRow.Children.Add(keyBox);
746+
keyRow.Children.Add(saveKeyBtn);
747+
keyBlock.Children.Add(keyRow);
748+
panel.Children.Add(keyBlock);
749+
750+
// Groq cloud models (only show if key exists)
751+
if (hasKey)
752+
{
753+
foreach (var model in SttModels.Available.Where(m => m.Engine == SttEngine.GroqCloud))
754+
panel.Children.Add(CreateGroqModelItem(model, currentModelId, onSelectModel, flyout));
755+
}
756+
}
757+
667758
flyout.Content = panel;
668759
}
669760

761+
private static UIElement CreateGroqModelItem(SttModelInfo model, string? currentModelId,
762+
Action<SttModelInfo> onSelect, Flyout parentFlyout)
763+
{
764+
var isCurrent = model.Id == currentModelId;
765+
766+
var btn = new Button
767+
{
768+
HorizontalAlignment = HorizontalAlignment.Stretch,
769+
HorizontalContentAlignment = HorizontalAlignment.Stretch,
770+
Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent),
771+
BorderThickness = new Thickness(0),
772+
Padding = new Thickness(10, 7, 10, 7),
773+
CornerRadius = new CornerRadius(5),
774+
Tag = model.Name
775+
};
776+
777+
var grid = new Grid { ColumnSpacing = 8 };
778+
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
779+
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
780+
781+
var textPanel = new StackPanel { Spacing = 1 };
782+
textPanel.Children.Add(new TextBlock
783+
{
784+
Text = model.Name,
785+
FontSize = 13,
786+
FontWeight = isCurrent
787+
? Microsoft.UI.Text.FontWeights.SemiBold
788+
: Microsoft.UI.Text.FontWeights.Normal,
789+
});
790+
textPanel.Children.Add(new TextBlock
791+
{
792+
Text = isCurrent ? $"Groq — {Lang.T("model_active")}" : $"Groq — {Lang.T("groq_cloud_model")}",
793+
FontSize = 11,
794+
Opacity = isCurrent ? 0.85 : 0.45
795+
});
796+
Grid.SetColumn(textPanel, 0);
797+
grid.Children.Add(textPanel);
798+
799+
var icon = new FontIcon
800+
{
801+
Glyph = isCurrent ? "\uE73E" : "\uE753", // checkmark or cloud
802+
FontSize = isCurrent ? 14 : 12,
803+
Foreground = isCurrent
804+
? new SolidColorBrush(Windows.UI.Color.FromArgb(255, 108, 203, 95))
805+
: (Brush)Application.Current.Resources["TextFillColorTertiaryBrush"],
806+
VerticalAlignment = VerticalAlignment.Center
807+
};
808+
Grid.SetColumn(icon, 1);
809+
grid.Children.Add(icon);
810+
811+
btn.Content = grid;
812+
btn.Click += (_, _) =>
813+
{
814+
onSelect(model);
815+
parentFlyout.Hide();
816+
};
817+
818+
return btn;
819+
}
820+
670821
private static UIElement CreateModelItem(SttModelInfo model, string? currentModelId,
671822
Flyout parentFlyout, XamlRoot xamlRoot, Action<SttModelInfo> onSelect, Action<SttModelInfo> onDelete, Action onRebuild)
672823
{

‎Lang.cs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,11 @@ public static class Lang
216216
["note_saved"] = "Note saved!",
217217
["voice_model_not_configured"] = "Voice model not configured",
218218
["configure_model_hint"] = "Configure a model in Settings → Model",
219+
["groq_cloud_section"] = "Groq Cloud (Whisper)",
220+
["groq_api_key"] = "Groq API Key",
221+
["groq_api_key_placeholder"] = "gsk_...",
222+
["groq_cloud_model"] = "Cloud — API",
223+
["groq_key_required"] = "Groq API key required",
219224

220225
// ── Tooltips ────────────────────────────────────────
221226
["tip_new"] = "New",
@@ -508,6 +513,11 @@ public static class Lang
508513
["note_saved"] = "Note enregistrée !",
509514
["voice_model_not_configured"] = "Modèle vocal non configuré",
510515
["configure_model_hint"] = "Configurez un modèle dans Paramètres → Modèle",
516+
["groq_cloud_section"] = "Groq Cloud (Whisper)",
517+
["groq_api_key"] = "Clé API Groq",
518+
["groq_api_key_placeholder"] = "gsk_...",
519+
["groq_cloud_model"] = "Cloud — API",
520+
["groq_key_required"] = "Clé API Groq requise",
511521

512522
// ── Tooltips ────────────────────────────────────────
513523
["tip_new"] = "Nouveau",

‎MainWindow.xaml.cs‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,9 @@ private void OpenVoiceNote()
507507
return;
508508
}
509509

510-
_voiceNoteWindow = new VoiceNoteWindow(_notes);
510+
_aiManager ??= new AiManager();
511+
_aiManager.Load();
512+
_voiceNoteWindow = new VoiceNoteWindow(_notes, _aiManager);
511513
_voiceNoteWindow.NoteCreated += () => DispatcherQueue.TryEnqueue(() => RefreshCurrentView());
512514
_voiceNoteWindow.Closed += (_, _) => _voiceNoteWindow = null;
513515
_voiceNoteWindow.Activate();
@@ -597,6 +599,9 @@ private void Settings_Click(object sender, RoutedEventArgs e)
597599

598600
private void ShowVoiceModelsInSettings(Flyout flyout)
599601
{
602+
_aiManager ??= new AiManager();
603+
_aiManager.Load();
604+
600605
var settingsDir = Path.Combine(
601606
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "NoteUI");
602607
var settingsPath = Path.Combine(settingsDir, "voice_settings.json");
@@ -647,7 +652,8 @@ void RebuildModelsPanel()
647652
// Re-show main settings
648653
Settings_Click(SettingsButton, new RoutedEventArgs());
649654
},
650-
onRebuild: RebuildModelsPanel);
655+
onRebuild: RebuildModelsPanel,
656+
aiManager: _aiManager);
651657
}
652658

653659
RebuildModelsPanel();

‎NoteWindow.xaml.cs‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1503,9 +1503,32 @@ private void StartVoiceRecording()
15031503
return;
15041504
}
15051505

1506+
// For Groq cloud models, check API key
1507+
string? groqKey = null;
1508+
if (model.Engine == SttEngine.GroqCloud)
1509+
{
1510+
try { _aiManager ??= new AiManager(); _aiManager.Load(); } catch { }
1511+
groqKey = _aiManager?.GetApiKey("groq");
1512+
if (string.IsNullOrWhiteSpace(groqKey))
1513+
{
1514+
var tip = new TeachingTip
1515+
{
1516+
Title = Lang.T("groq_key_required"),
1517+
Subtitle = Lang.T("configure_model_hint"),
1518+
IsLightDismissEnabled = true,
1519+
PreferredPlacement = TeachingTipPlacementMode.Top,
1520+
Target = VoiceButton,
1521+
};
1522+
RootGrid.Children.Add(tip);
1523+
tip.IsOpen = true;
1524+
tip.Closed += (_, _) => RootGrid.Children.Remove(tip);
1525+
return;
1526+
}
1527+
}
1528+
15061529
try
15071530
{
1508-
_voiceRecognizer = SpeechRecognizerFactory.Create(model);
1531+
_voiceRecognizer = SpeechRecognizerFactory.Create(model, groqKey);
15091532
}
15101533
catch
15111534
{

0 commit comments

Comments
 (0)