Initial commit: Win Dictation - AI Voice to Text for Windows with automatic model selection

This commit is contained in:
2025-12-06 10:28:46 +13:00
commit 81b3d0073e
1666 changed files with 388157 additions and 0 deletions
+558
View File
@@ -0,0 +1,558 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <shellapi.h>
#include <dwmapi.h>
#include <string>
#include <vector>
#include "transcriber.h"
#include "whisper.h"
#include <cstring>
#pragma comment(lib, "comctl32.lib")
#pragma comment(lib, "dwmapi.lib")
// Constants
#define WM_TRAYICON (WM_USER + 1)
#define WM_APPEND_TEXT (WM_USER + 2)
#define ID_TRAY_APP_ICON 1001
#define ID_TRAY_EXIT 1002
#define ID_TRAY_SHOW 1003
#define ID_BTN_RECORD 1004
#define ID_BTN_CLEAR 1005
#define ID_EDIT_TEXT 1006
#define ID_COMBO_AUDIO 1007
#define ID_PROGRESS_VU 1008
#define ID_STATIC_STATUS 1009
#define ID_PROGRESS_BUFFER 1010
#define ID_TIMER_UPDATE 2
#define HOTKEY_ID 1
#define IDI_ICON1 101
// Modern colors (dark theme)
#define COLOR_BG RGB(32, 33, 36)
#define COLOR_SURFACE RGB(41, 42, 45)
#define COLOR_PRIMARY RGB(138, 180, 248)
#define COLOR_SUCCESS RGB(129, 201, 149)
#define COLOR_TEXT RGB(232, 234, 237)
#define COLOR_TEXT_DIM RGB(154, 160, 166)
#define COLOR_ACCENT RGB(66, 133, 244)
// Globals
HINSTANCE hInst;
HWND hMainWnd;
NOTIFYICONDATA nid;
Transcriber g_transcriber;
WhisperConfig g_config;
bool g_isRecording = false;
// UI Resources
HBRUSH g_hBrushBg = NULL;
HBRUSH g_hBrushSurface = NULL;
HFONT g_hFontNormal = NULL;
HFONT g_hFontLarge = NULL;
HFONT g_hFontMono = NULL;
// Forward declarations
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
void ShowContextMenu(HWND hwnd, POINT pt);
void ToggleRecording(HWND hwnd);
void RefreshAudioDevices(HWND hwnd);
void InitializeUI(HWND hwnd);
void UpdateStatus(HWND hwnd);
bool DetectGPUAvailability();
std::string SelectOptimalModel(bool has_gpu);
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow) {
hInst = hInstance;
// Initialize Common Controls
INITCOMMONCONTROLSEX icex;
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_WIN95_CLASSES | ICC_STANDARD_CLASSES;
InitCommonControlsEx(&icex);
// Create UI resources
g_hBrushBg = CreateSolidBrush(COLOR_BG);
g_hBrushSurface = CreateSolidBrush(COLOR_SURFACE);
g_hFontNormal = CreateFont(16, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI");
g_hFontLarge = CreateFont(20, 0, 0, 0, FW_SEMIBOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI");
g_hFontMono = CreateFont(16, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Consolas");
// Register Window Class
WNDCLASSEX wc = {0};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = g_hBrushBg;
wc.lpszClassName = L"WhisperDictationClass";
wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1));
RegisterClassEx(&wc);
// Create Window (modern, larger size)
hMainWnd = CreateWindowEx(
0,
L"WhisperDictationClass",
L"Whisper Dictation - AI Voice to Text",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 720, 600,
NULL, NULL, hInstance, NULL
);
if (!hMainWnd) return FALSE;
// Enable dark mode for title bar (Windows 10+)
BOOL useDarkMode = TRUE;
DwmSetWindowAttribute(hMainWnd, 20, &useDarkMode, sizeof(useDarkMode));
InitializeUI(hMainWnd);
RefreshAudioDevices(hMainWnd);
// Tray Icon
nid.cbSize = sizeof(NOTIFYICONDATA);
nid.hWnd = hMainWnd;
nid.uID = ID_TRAY_APP_ICON;
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
nid.uCallbackMessage = WM_TRAYICON;
nid.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1));
wcscpy_s(nid.szTip, L"Whisper Dictation");
Shell_NotifyIcon(NIM_ADD, &nid);
// Register Hotkey (Ctrl + Shift + R)
RegisterHotKey(hMainWnd, HOTKEY_ID, MOD_CONTROL | MOD_SHIFT, 'R');
// Detect GPU availability and select optimal model
bool has_gpu = DetectGPUAvailability();
g_config.model_path = SelectOptimalModel(has_gpu);
// Setup callback for transcribed text
g_transcriber.set_callback([](const std::string& text) {
std::string* msg = new std::string(text);
PostMessage(hMainWnd, WM_APPEND_TEXT, (WPARAM)msg, 0);
});
// Start UI update timer
SetTimer(hMainWnd, ID_TIMER_UPDATE, 33, NULL); // ~30 FPS for smooth animations
ShowWindow(hMainWnd, nCmdShow);
UpdateWindow(hMainWnd);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// Cleanup
Shell_NotifyIcon(NIM_DELETE, &nid);
DeleteObject(g_hBrushBg);
DeleteObject(g_hBrushSurface);
DeleteObject(g_hFontNormal);
DeleteObject(g_hFontLarge);
DeleteObject(g_hFontMono);
return (int)msg.wParam;
}
void InitializeUI(HWND hwnd) {
// Create all controls with modern styling
// Record button (large, primary)
HWND hBtnRecord = CreateWindow(L"BUTTON", L"⬤ Start Recording",
WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_OWNERDRAW,
20, 20, 300, 50, hwnd, (HMENU)ID_BTN_RECORD, hInst, NULL);
SendMessage(hBtnRecord, WM_SETFONT, (WPARAM)g_hFontLarge, TRUE);
// Clear button
HWND hBtnClear = CreateWindow(L"BUTTON", L"Clear",
WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
340, 20, 120, 50, hwnd, (HMENU)ID_BTN_CLEAR, hInst, NULL);
SendMessage(hBtnClear, WM_SETFONT, (WPARAM)g_hFontNormal, TRUE);
// Status text
HWND hStatus = CreateWindow(L"STATIC", L"Ready • GPU: Detecting...",
WS_CHILD | WS_VISIBLE | SS_LEFT,
20, 85, 640, 25, hwnd, (HMENU)ID_STATIC_STATUS, hInst, NULL);
SendMessage(hStatus, WM_SETFONT, (WPARAM)g_hFontNormal, TRUE);
// Audio device selector
CreateWindow(L"STATIC", L"Microphone:",
WS_CHILD | WS_VISIBLE | SS_LEFT,
20, 120, 120, 20, hwnd, NULL, hInst, NULL);
HWND hCombo = CreateWindow(L"COMBOBOX", L"",
WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_VSCROLL,
140, 118, 360, 200, hwnd, (HMENU)ID_COMBO_AUDIO, hInst, NULL);
SendMessage(hCombo, WM_SETFONT, (WPARAM)g_hFontNormal, TRUE);
// VU Meter label and progress
CreateWindow(L"STATIC", L"Level:",
WS_CHILD | WS_VISIBLE | SS_LEFT,
520, 120, 60, 20, hwnd, NULL, hInst, NULL);
HWND hVU = CreateWindow(PROGRESS_CLASS, L"",
WS_CHILD | WS_VISIBLE | PBS_SMOOTH,
580, 118, 100, 22, hwnd, (HMENU)ID_PROGRESS_VU, hInst, NULL);
SendMessage(hVU, PBM_SETRANGE, 0, MAKELPARAM(0, 100));
SendMessage(hVU, PBM_SETBARCOLOR, 0, (LPARAM)COLOR_SUCCESS);
SendMessage(hVU, PBM_SETBKCOLOR, 0, (LPARAM)COLOR_SURFACE);
// Buffer progress bar
CreateWindow(L"STATIC", L"Buffer:",
WS_CHILD | WS_VISIBLE | SS_LEFT,
20, 155, 60, 20, hwnd, NULL, hInst, NULL);
HWND hBuffer = CreateWindow(PROGRESS_CLASS, L"",
WS_CHILD | WS_VISIBLE | PBS_SMOOTH,
85, 153, 595, 22, hwnd, (HMENU)ID_PROGRESS_BUFFER, hInst, NULL);
SendMessage(hBuffer, PBM_SETRANGE, 0, MAKELPARAM(0, 100));
SendMessage(hBuffer, PBM_SETBARCOLOR, 0, (LPARAM)COLOR_PRIMARY);
SendMessage(hBuffer, PBM_SETBKCOLOR, 0, (LPARAM)COLOR_SURFACE);
// Transcription text box (large, monospaced)
HWND hEdit = CreateWindowEx(WS_EX_CLIENTEDGE, L"EDIT", L"",
WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN,
20, 195, 660, 340, hwnd, (HMENU)ID_EDIT_TEXT, hInst, NULL);
SendMessage(hEdit, WM_SETFONT, (WPARAM)g_hFontMono, TRUE);
SendMessage(hEdit, EM_SETLIMITTEXT, 0, 0); // No limit
}
void RefreshAudioDevices(HWND hwnd) {
HWND hCombo = GetDlgItem(hwnd, ID_COMBO_AUDIO);
SendMessage(hCombo, CB_RESETCONTENT, 0, 0);
std::vector<std::string> devices = Transcriber::get_audio_devices();
if (devices.empty()) {
SendMessage(hCombo, CB_ADDSTRING, 0, (LPARAM)L"No devices found");
SendMessage(hCombo, CB_SETCURSEL, 0, 0);
return;
}
for (const auto& device : devices) {
int len = MultiByteToWideChar(CP_UTF8, 0, device.c_str(), -1, NULL, 0);
if (len > 0) {
std::vector<wchar_t> wbuf(len);
MultiByteToWideChar(CP_UTF8, 0, device.c_str(), -1, wbuf.data(), len);
SendMessage(hCombo, CB_ADDSTRING, 0, (LPARAM)wbuf.data());
}
}
SendMessage(hCombo, CB_SETCURSEL, 0, 0);
g_config.capture_id = 0;
}
void UpdateStatus(HWND hwnd) {
wchar_t status[256] = {0};
if (g_isRecording) {
bool gpu = g_transcriber.is_using_gpu();
float buffer = g_transcriber.get_buffer_fullness() * 100.0f;
swprintf_s(status, L"⬤ Recording • GPU: %s • Buffer: %.0f%% • Threads: %d",
gpu ? L"ON" : L"CPU", buffer, g_config.n_threads);
} else {
swprintf_s(status, L"Ready • Press Ctrl+Shift+R to start • Threads: %d",
g_config.n_threads);
}
SetDlgItemText(hwnd, ID_STATIC_STATUS, status);
}
void ToggleRecording(HWND hwnd) {
if (g_isRecording) {
// Stop recording
g_transcriber.stop();
SetDlgItemText(hwnd, ID_BTN_RECORD, L"⬤ Start Recording");
g_isRecording = false;
// Reset progress bars
SendMessage(GetDlgItem(hwnd, ID_PROGRESS_VU), PBM_SETPOS, 0, 0);
SendMessage(GetDlgItem(hwnd, ID_PROGRESS_BUFFER), PBM_SETPOS, 0, 0);
UpdateStatus(hwnd);
} else {
// Start recording
HWND hCombo = GetDlgItem(hwnd, ID_COMBO_AUDIO);
int idx = SendMessage(hCombo, CB_GETCURSEL, 0, 0);
if (idx != CB_ERR) {
g_config.capture_id = idx;
}
// Initialize if not loaded
if (!g_transcriber.is_loaded()) {
SetDlgItemText(hwnd, ID_STATIC_STATUS, L"Loading model...");
UpdateWindow(hwnd);
if (!g_transcriber.init(g_config)) {
MessageBox(hwnd, L"Failed to initialize Whisper.\n\nPlease check:\n- Model file exists in models/\n- GPU drivers are up to date (if using GPU)",
L"Error", MB_OK | MB_ICONERROR);
UpdateStatus(hwnd);
return;
}
}
g_transcriber.start();
SetDlgItemText(hwnd, ID_BTN_RECORD, L"⬛ Stop Recording");
g_isRecording = true;
UpdateStatus(hwnd);
}
}
// Custom button drawing for modern look
void DrawButton(LPDRAWITEMSTRUCT pDIS) {
HDC hdc = pDIS->hDC;
RECT rect = pDIS->rcItem;
bool pressed = (pDIS->itemState & ODS_SELECTED) != 0;
bool hover = (pDIS->itemState & ODS_HOTLIGHT) != 0;
// Background
COLORREF bgColor = g_isRecording ? RGB(201, 70, 70) : COLOR_ACCENT;
if (pressed) {
bgColor = RGB(50, 110, 220);
} else if (hover) {
bgColor = g_isRecording ? RGB(220, 85, 85) : RGB(88, 145, 255);
}
HBRUSH hBrush = CreateSolidBrush(bgColor);
FillRect(hdc, &rect, hBrush);
DeleteObject(hBrush);
// Text
wchar_t text[128] = {0};
GetWindowText(pDIS->hwndItem, text, 128);
SetBkMode(hdc, TRANSPARENT);
SetTextColor(hdc, RGB(255, 255, 255));
SelectObject(hdc, g_hFontLarge);
DrawText(hdc, text, -1, &rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_CTLCOLORSTATIC:
{
HDC hdcStatic = (HDC)wParam;
SetTextColor(hdcStatic, COLOR_TEXT);
SetBkColor(hdcStatic, COLOR_BG);
return (LRESULT)g_hBrushBg;
}
case WM_CTLCOLOREDIT:
{
HDC hdcEdit = (HDC)wParam;
SetTextColor(hdcEdit, COLOR_TEXT);
SetBkColor(hdcEdit, COLOR_SURFACE);
return (LRESULT)g_hBrushSurface;
}
case WM_DRAWITEM:
if (wParam == ID_BTN_RECORD) {
DrawButton((LPDRAWITEMSTRUCT)lParam);
return TRUE;
}
break;
case WM_SIZE:
{
int width = LOWORD(lParam);
int height = HIWORD(lParam);
// Responsive layout
MoveWindow(GetDlgItem(hWnd, ID_BTN_RECORD), 20, 20, 300, 50, TRUE);
MoveWindow(GetDlgItem(hWnd, ID_BTN_CLEAR), 340, 20, 120, 50, TRUE);
MoveWindow(GetDlgItem(hWnd, ID_STATIC_STATUS), 20, 85, width - 40, 25, TRUE);
MoveWindow(GetDlgItem(hWnd, ID_COMBO_AUDIO), 140, 118, width - 280, 22, TRUE);
MoveWindow(GetDlgItem(hWnd, ID_PROGRESS_VU), width - 120, 118, 100, 22, TRUE);
MoveWindow(GetDlgItem(hWnd, ID_PROGRESS_BUFFER), 85, 153, width - 105, 22, TRUE);
MoveWindow(GetDlgItem(hWnd, ID_EDIT_TEXT), 20, 195, width - 40, height - 215, TRUE);
}
break;
case WM_COMMAND:
switch (LOWORD(wParam)) {
case ID_TRAY_EXIT:
DestroyWindow(hWnd);
break;
case ID_TRAY_SHOW:
ShowWindow(hWnd, SW_SHOW);
SetForegroundWindow(hWnd);
break;
case ID_BTN_RECORD:
ToggleRecording(hWnd);
break;
case ID_BTN_CLEAR:
SetDlgItemText(hWnd, ID_EDIT_TEXT, L"");
break;
case ID_COMBO_AUDIO:
if (HIWORD(wParam) == CBN_SELCHANGE) {
if (g_isRecording) {
// Stop current recording
g_transcriber.stop();
SetDlgItemText(hWnd, ID_BTN_RECORD, L"⬤ Start Recording");
g_isRecording = false;
// Wait for complete shutdown
Sleep(100);
// Update config with new device
HWND hCombo = GetDlgItem(hWnd, ID_COMBO_AUDIO);
int idx = SendMessage(hCombo, CB_GETCURSEL, 0, 0);
if (idx != CB_ERR) {
g_config.capture_id = idx;
}
// Restart with new device
g_transcriber.start();
SetDlgItemText(hWnd, ID_BTN_RECORD, L"⬛ Stop Recording");
g_isRecording = true;
UpdateStatus(hWnd);
}
}
break;
}
break;
case WM_TIMER:
if (wParam == ID_TIMER_UPDATE && g_isRecording) {
// Update VU meter (smooth animation)
float energy = g_transcriber.get_audio_energy();
int pos = (int)(energy * 100.0f);
SendMessage(GetDlgItem(hWnd, ID_PROGRESS_VU), PBM_SETPOS, pos, 0);
// Update buffer indicator
float buffer = g_transcriber.get_buffer_fullness();
int buf_pos = (int)(buffer * 100.0f);
SendMessage(GetDlgItem(hWnd, ID_PROGRESS_BUFFER), PBM_SETPOS, buf_pos, 0);
// Update status text
UpdateStatus(hWnd);
}
break;
case WM_TRAYICON:
if (lParam == WM_RBUTTONUP) {
POINT pt;
GetCursorPos(&pt);
ShowContextMenu(hWnd, pt);
} else if (lParam == WM_LBUTTONDBLCLK) {
ShowWindow(hWnd, SW_SHOW);
SetForegroundWindow(hWnd);
}
break;
case WM_HOTKEY:
if (wParam == HOTKEY_ID) {
ToggleRecording(hWnd);
if (g_isRecording) {
ShowWindow(hWnd, SW_SHOW);
SetForegroundWindow(hWnd);
}
}
break;
case WM_APPEND_TEXT:
{
std::string* s = (std::string*)wParam;
if (s) {
int len = MultiByteToWideChar(CP_UTF8, 0, s->c_str(), -1, NULL, 0);
if (len > 0) {
std::vector<wchar_t> wbuf(len);
MultiByteToWideChar(CP_UTF8, 0, s->c_str(), -1, wbuf.data(), len);
HWND hEdit = GetDlgItem(hWnd, ID_EDIT_TEXT);
int ndx = GetWindowTextLength(hEdit);
SendMessage(hEdit, EM_SETSEL, (WPARAM)ndx, (LPARAM)ndx);
SendMessage(hEdit, EM_REPLACESEL, 0, (LPARAM)wbuf.data());
SendMessage(hEdit, EM_REPLACESEL, 0, (LPARAM)L" ");
// Auto-scroll to bottom
SendMessage(hEdit, EM_SCROLLCARET, 0, 0);
}
delete s;
}
}
break;
case WM_CLOSE:
ShowWindow(hWnd, SW_HIDE);
return 0;
case WM_DESTROY:
g_transcriber.stop();
UnregisterHotKey(hWnd, HOTKEY_ID);
KillTimer(hWnd, ID_TIMER_UPDATE);
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
void ShowContextMenu(HWND hwnd, POINT pt) {
HMENU hMenu = CreatePopupMenu();
InsertMenu(hMenu, 0, MF_BYPOSITION | MF_STRING, ID_TRAY_SHOW, L"Show Window");
InsertMenu(hMenu, 1, MF_BYPOSITION | MF_SEPARATOR, 0, NULL);
InsertMenu(hMenu, 2, MF_BYPOSITION | MF_STRING, ID_TRAY_EXIT, L"Exit");
SetForegroundWindow(hwnd);
TrackPopupMenu(hMenu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, hwnd, NULL);
DestroyMenu(hMenu);
}
// Detect GPU availability without loading a model
bool DetectGPUAvailability() {
// Use whisper_print_system_info to check for GPU backends
// This function works without loading a model
const char* info = whisper_print_system_info();
if (!info) {
return false;
}
// Check for GPU backends in the system info string
return (strstr(info, "CUDA") != nullptr ||
strstr(info, "Metal") != nullptr ||
strstr(info, "HIP") != nullptr ||
strstr(info, "Vulkan") != nullptr);
}
// Select optimal model based on GPU availability
// CPU-only systems get tiny.en (faster, smaller), GPU systems get base.en (better accuracy)
std::string SelectOptimalModel(bool has_gpu) {
if (has_gpu) {
// GPU available - use base.en for better accuracy
if (GetFileAttributesA("models/ggml-base.en.bin") != INVALID_FILE_ATTRIBUTES) {
return "models/ggml-base.en.bin";
} else if (GetFileAttributesA("models/ggml-medium.en.bin") != INVALID_FILE_ATTRIBUTES) {
return "models/ggml-medium.en.bin";
} else if (GetFileAttributesA("models/ggml-large-v3-turbo.bin") != INVALID_FILE_ATTRIBUTES) {
return "models/ggml-large-v3-turbo.bin";
}
// Fallback to tiny if base not available
if (GetFileAttributesA("models/ggml-tiny.en.bin") != INVALID_FILE_ATTRIBUTES) {
return "models/ggml-tiny.en.bin";
}
} else {
// CPU-only - use tiny.en for better performance on slower machines
if (GetFileAttributesA("models/ggml-tiny.en.bin") != INVALID_FILE_ATTRIBUTES) {
return "models/ggml-tiny.en.bin";
}
// Fallback to base if tiny not available
if (GetFileAttributesA("models/ggml-base.en.bin") != INVALID_FILE_ATTRIBUTES) {
return "models/ggml-base.en.bin";
}
}
// Ultimate fallback
return "models/ggml-base.en.bin";
}