#define WIN32_LEAN_AND_MEAN #define NOMINMAX #include #include #include #include #include #include #include #include #pragma comment(lib, "comctl32.lib") #pragma comment(lib, "dwmapi.lib") #pragma comment(lib, "gdiplus.lib") #pragma comment(lib, "uxtheme.lib") using namespace Gdiplus; #include #include #include #include #include #include #include #include #include "transcriber.h" #include "text_util.h" #include "whisper.h" #include "settings.h" #include "logging.h" #include "timing.h" #include "stats.h" #include "history.h" #include "downloader.h" #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif #ifndef DWMWA_BORDER_COLOR #define DWMWA_BORDER_COLOR 34 #endif #ifndef DWMWA_CAPTION_COLOR #define DWMWA_CAPTION_COLOR 35 #endif #ifndef DWMWA_TEXT_COLOR #define DWMWA_TEXT_COLOR 36 #endif #ifndef DWMWA_WINDOW_CORNER_PREFERENCE #define DWMWA_WINDOW_CORNER_PREFERENCE 33 #endif #ifndef DWMWCP_ROUND #define DWMWCP_ROUND 2 #endif #ifndef DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 #define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 ((DPI_AWARENESS_CONTEXT)-4) #endif #define WM_TRAYICON (WM_USER + 1) #define WM_APP_RESULT (WM_USER + 2) #define WM_APP_SHOW (WM_USER + 3) #define WM_APP_SELECT (WM_USER + 5) #define WM_APP_PROGRESS (WM_USER + 6) #define ID_TRAY_APP_ICON 1001 #define ID_TRAY_EXIT 1002 #define ID_TRAY_SHOW 1003 #define ID_TRAY_AUTOPASTE 1016 #define ID_TRAY_TOPMOST 1017 #define ID_TRAY_AUTOHIDE 1018 #define ID_BTN_RECORD 1004 #define ID_BTN_CLEAR 1005 #define ID_EDIT_TEXT 1006 #define ID_COMBO_AUDIO 1007 #define ID_STATIC_STATUS 1009 #define ID_BTN_COPY 1011 #define ID_BTN_PASTE 1012 #define ID_BTN_PIN 1013 #define ID_STATIC_PLACEHOLDER 1014 #define ID_COMBO_MODEL 1015 #define ID_SEL_AUDIO 1007 #define ID_SEL_MODEL 1015 #define ID_TIMER_UPDATE 2 #define HK_TOGGLE 1 #define HK_HIDE 2 #define IDI_ICON1 101 #define CR_BG RGB(0x0E,0x10,0x14) #define CR_SURFACE RGB(0x16,0x19,0x20) #define CR_TEXT RGB(0xEC,0xEE,0xF2) static const Color T_BG (255, 0x0E, 0x10, 0x14); static const Color T_CARD (255, 0x16, 0x19, 0x20); static const Color T_CARD_HI (255, 0x1E, 0x22, 0x2B); static const Color T_CARD_LO (255, 0x12, 0x15, 0x1B); static const Color T_TEXT (255, 0xEC, 0xEE, 0xF2); static const Color T_DIM (255, 0x8A, 0x90, 0x9C); static const Color T_FAINT (255, 0x5A, 0x60, 0x6C); static const Color T_ACCENT (255, 0x6E, 0x8B, 0xFF); static const Color T_ACCENT_HI(255, 0x83, 0x9C, 0xFF); static const Color T_DANGER (255, 0xFF, 0x5C, 0x5C); static const Color T_GOOD (255, 0x46, 0xD3, 0x9A); static const Color T_TOPLIGHT (26, 0xFF, 0xFF, 0xFF); static const Color C_BG = T_BG; static const Color C_SURFACE = T_CARD; static const Color C_SURFACEHI= T_CARD_HI; static const Color C_BORDER = Color(255,0x26,0x2B,0x36); static const Color C_TEXT = T_TEXT; static const Color C_TEXTDIM = T_DIM; static const Color C_ACCENT = T_ACCENT; static const Color C_ACCENTHI = T_ACCENT_HI; static const Color C_DANGER = T_DANGER; static const Color C_GOOD = T_GOOD; HINSTANCE hInst; HWND hMainWnd; NOTIFYICONDATA nid; Transcriber g_tx; WhisperConfig g_config; HWND g_prevForeground = nullptr; bool g_autoPaste = true; bool g_pinned = true; bool g_autoHide = false; HANDLE g_hMutex = nullptr; ULONG_PTR g_gdipToken = 0; std::atomic g_modelLoaded{false}; std::atomic g_modelOk{false}; std::atomic g_recordingSecs{0}; std::atomic g_progress{0}; DWORD g_busyStart = 0; float g_energy = 0.0f; bool g_cancelRequested = false; TimingModel g_timing; ProgressEstimator g_est; double g_lastAudioLen = 0.0; DWORD g_lastTick = 0; float g_progressFrac = 0.0f; float g_progressRemain = 0.0f; float g_dpiScale = 1.0f; std::wstring g_statusOverride; DWORD g_statusOverrideUntil = 0; AppSettings g_set; UsageStats g_stats; bool g_editDirty = false; DWORD g_insStart = 0, g_insEnd = 0; REAL g_statsTopY = 0; std::vector g_history; std::wstring g_lastLoadedText; #define ID_SEL_HISTORY 1019 enum class View { Main, Settings }; View g_view = View::Main; std::string g_pendingModelPath; int g_setScroll = 0, g_setScrollMax = 0; RectF g_sBack, g_sSave, g_sCancel; RECT g_sContent = {0,0,0,0}; struct CatRowRect { RectF row, btn; }; int g_setHot = -1; Downloader g_dl; enum class DlState { NotInstalled, Downloading, Installed }; struct CatState { DlState state = DlState::NotInstalled; int pct = 0; }; static const float kMaxRecordSeconds = 600.0f; void PersistNow() { g_set.pinned = g_pinned; g_set.autoPaste = g_autoPaste; g_set.autoHide = g_autoHide; g_set.captureId = g_config.capture_id; { std::string path = g_config.model_path; g_set.modelFile = std::wstring(path.begin(), path.end()); } RECT r; if (GetWindowRect(hMainWnd, &r)) { g_set.winX = r.left; g_set.winY = r.top; g_set.winW = r.right - r.left; g_set.winH = r.bottom - r.top; } SaveSettings(g_set); } RECT g_vuRect = {0,0,0,0}; RECT g_panelRect = {0,0,0,0}; HFONT g_fUI = nullptr, g_fUISemi = nullptr, g_fSmall = nullptr, g_fText = nullptr; HBRUSH g_brBg = nullptr, g_brSurface = nullptr; Gdiplus::Font* g_gpUI = nullptr; Gdiplus::Font* g_gpUISemi = nullptr; Gdiplus::Font* g_gpSmall = nullptr; Gdiplus::Font* g_gpText = nullptr; Gdiplus::Font* g_gpIcon = nullptr; struct ModelInfo { const wchar_t* display; const char* fileName; const wchar_t* sizeLabel; const wchar_t* hint; }; static const ModelInfo kCatalog[] = { { L"tiny.en", "ggml-tiny.en.bin", L"~75 MB", L"fastest" }, { L"tiny.en-q8_0", "ggml-tiny.en-q8_0.bin", L"~42 MB", L"fastest, smaller file" }, { L"base.en-q5_1", "ggml-base.en-q5_1.bin", L"~59 MB", L"good balance" }, { L"base.en", "ggml-base.en.bin", L"~142 MB", L"more accurate" }, { L"small.en-q5_1", "ggml-small.en-q5_1.bin", L"~182 MB", L"accurate — slow on this CPU" }, { L"small.en", "ggml-small.en.bin", L"~466 MB", L"most accurate — slowest" }, }; static const int kCatalogCount = (int)std::size(kCatalog); CatRowRect g_catRect[kCatalogCount]; CatState g_cat[kCatalogCount]; std::vector g_modelComboPaths; std::vector g_audioItems; int g_audioSel = 0; std::vector g_modelItems; int g_modelSel = 0; bool g_initializing = true; struct PopupState { std::vector items; int sel; int hot; HWND owner; int ctrlId; }; static PopupState g_pop; enum class WK { RecordHero, Pin, SettingsCog, Copy, Paste, Clear, SelAudio, History, Transcript }; struct Widget { WK kind; RectF r; bool hover = false; bool pressed = false; float anim = 0.0f; }; static Widget g_w[ (int)WK::Transcript + 1 ]; static int g_hot = -1; static int g_active = -1; static DWORD g_lastFrame = 0; static bool g_animating = false; static bool AnyAnimating() { if (g_animating) return true; if (g_tx.is_recording() || g_tx.is_busy()) return true; for (auto& w : g_w) { float target = g_active == (&w - g_w) ? 1.0f : (w.hover ? 0.6f : 0.0f); if (std::fabs(w.anim - target) > 0.002f) return true; } return false; } static void StepAnimations(float dt) { for (auto& w : g_w) { float target = (g_active == (&w - g_w)) ? 1.0f : (w.hover ? 0.6f : 0.0f); w.anim += (target - w.anim) * std::min(1.0f, dt * 12.0f); } } static void EnsureAnimating(HWND h) { if (!g_animating) { g_animating = true; g_lastFrame = GetTickCount(); SetTimer(h, 3, 16, nullptr); // 16ms animation timer } } static void StopAnimating(HWND h) { KillTimer(h, 3); g_animating = false; } LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); LRESULT CALLBACK PopupProc(HWND, UINT, WPARAM, LPARAM); void ShowContextMenu(HWND, POINT); void RefreshAudioDevices(HWND); void RefreshModelList(HWND); void SetStatus(HWND, const wchar_t*); void UpdatePlaceholder(HWND); void LayoutControls(HWND, int, int); void LayoutWidgets(int W, int H); void PaintSurface(HWND hwnd); int HitTest(POINT p); void OnClick(HWND hwnd, WK kind); void DrawHero(Graphics& g, const Widget& w); void DrawGhost(Graphics& g, const Widget& w, const wchar_t* label, bool active); void DrawPinSurface(Graphics& g, const Widget& w); void DrawIconChip(Graphics& g, const Widget& w, const wchar_t* glyph, const wchar_t* fallback, bool active); void DrawSelectSurface(Graphics& g, const Widget& w, const std::wstring& text); void DrawStatusStrip(Graphics& g, const RectF& strip); void DrawCard(Graphics& g, const Rect& cardRect); void DrawVU(Graphics&, const RECT&, float); void DrawProgress(Graphics&, const RECT&, float); void ShowSelectPopup(HWND owner, int ctrlId, const std::vector& items, int sel, const RectF& anchor); void SwitchView(HWND hwnd, View v); void LayoutSettings(int W, int H); void PaintSettings(Graphics& g, HWND hwnd, int W, int H); int SettingsHitTest(POINT p); void OnSettingsClick(HWND hwnd, POINT p); void ApplySettings(HWND hwnd); std::string exe_dir(); std::wstring to_w(const std::string&); std::wstring GetEditText(HWND hwnd); bool SetClipboardTextUtf8(HWND, const std::string&); void send_ctrl_v(); void PasteIntoWindow(HWND); bool DetectGPUAvailability(); std::string SelectOptimalModel(bool); std::vector BuildStatsLines(); static HFONT MakeFont(int px, int weight, float scale = 1.0f) { return CreateFontW(-(int)(px * scale + 0.5f), 0, 0, 0, weight, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI Variable Display"); } static void RebuildGdipFonts(); static void RecreateFonts(float scale) { if (g_fUI) DeleteObject(g_fUI); if (g_fUISemi) DeleteObject(g_fUISemi); if (g_fSmall) DeleteObject(g_fSmall); if (g_fText) DeleteObject(g_fText); g_fUI = MakeFont(15, FW_NORMAL, scale); g_fUISemi = MakeFont(15, FW_SEMIBOLD, scale); g_fSmall = MakeFont(12, FW_NORMAL, scale); g_fText = MakeFont(16, FW_NORMAL, scale); if (!g_fUI) g_fUI = MakeFont(15, FW_NORMAL, scale); RebuildGdipFonts(); } static Gdiplus::Font* GdipFontFromHFont(HFONT hf) { HDC sdc = GetDC(nullptr); Gdiplus::Font* f = new Gdiplus::Font(sdc, hf); ReleaseDC(nullptr, sdc); if (f->GetLastStatus() != Ok) { delete f; return nullptr; } return f; } static void RebuildGdipFonts() { delete g_gpUI; delete g_gpUISemi; delete g_gpSmall; delete g_gpText; delete g_gpIcon; g_gpUI = GdipFontFromHFont(g_fUI); g_gpUISemi = GdipFontFromHFont(g_fUISemi); g_gpSmall = GdipFontFromHFont(g_fSmall); g_gpText = GdipFontFromHFont(g_fText); g_gpIcon = new Gdiplus::Font(L"Segoe MDL2 Assets", 15.0f * g_dpiScale, FontStyleRegular, UnitPixel); if (g_gpIcon->GetLastStatus() != Ok) { delete g_gpIcon; g_gpIcon = nullptr; } if (!g_gpUI) g_gpUI = new Gdiplus::Font(L"Segoe UI", 15.0f * g_dpiScale, FontStyleRegular, UnitPixel); if (!g_gpUISemi) g_gpUISemi = new Gdiplus::Font(L"Segoe UI", 15.0f * g_dpiScale, FontStyleBold, UnitPixel); if (!g_gpSmall) g_gpSmall = new Gdiplus::Font(L"Segoe UI", 12.0f * g_dpiScale, FontStyleRegular, UnitPixel); if (!g_gpText) g_gpText = new Gdiplus::Font(L"Segoe UI", 16.0f * g_dpiScale, FontStyleRegular, UnitPixel); } static void RoundPath(GraphicsPath& p, const Rect& r, int rad) { int d = rad * 2; p.AddArc(r.X, r.Y, d, d, 180, 90); p.AddArc(r.GetRight() - d, r.Y, d, d, 270, 90); p.AddArc(r.GetRight() - d, r.GetBottom() - d, d, d, 0, 90); p.AddArc(r.X, r.GetBottom() - d, d, d, 90, 90); p.CloseFigure(); } static void FillRound(Graphics& g, const Color& c, const Rect& r, int rad) { GraphicsPath p; RoundPath(p, r, rad); SolidBrush b(c); g.FillPath(&b, &p); } static void StrokeRound(Graphics& g, const Color& c, const Rect& r, int rad, REAL w = 1.0f) { GraphicsPath p; RoundPath(p, r, rad); Pen pen(c, w); g.DrawPath(&pen, &p); } static void DrawTextC(Graphics& g, const wchar_t* s, const Font& f, const Color& c, const RectF& box, StringAlignment h, StringAlignment v) { StringFormat sf; sf.SetAlignment(h); sf.SetLineAlignment(v); sf.SetTrimming(StringTrimmingEllipsisCharacter); SolidBrush b(c); g.DrawString(s, -1, &f, box, &sf, &b); } int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) { hInst = hInstance; g_hMutex = CreateMutexW(nullptr, TRUE, L"WhisperDictation_SingleInstance"); if (GetLastError() == ERROR_ALREADY_EXISTS) { HWND existing = FindWindowW(L"WhisperDictationClass", nullptr); if (existing) PostMessage(existing, WM_APP_SHOW, 0, 0); CloseHandle(g_hMutex); return 0; } GdiplusStartupInput gdipIn; if (GdiplusStartup(&g_gdipToken, &gdipIn, nullptr) != Ok) return 1; SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); INITCOMMONCONTROLSEX icex; icex.dwSize = sizeof(INITCOMMONCONTROLSEX); icex.dwICC = ICC_WIN95_CLASSES | ICC_STANDARD_CLASSES; InitCommonControlsEx(&icex); g_fUI = MakeFont(15, FW_NORMAL, g_dpiScale); g_fUISemi = MakeFont(15, FW_SEMIBOLD, g_dpiScale); g_fSmall = MakeFont(12, FW_NORMAL, g_dpiScale); g_fText = MakeFont(16, FW_NORMAL, g_dpiScale); if (!g_fUI) g_fUI = MakeFont(15, FW_NORMAL, g_dpiScale); g_brBg = CreateSolidBrush(CR_BG); g_brSurface = CreateSolidBrush(CR_SURFACE); WNDCLASSEX wc = {0}; wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WndProc; wc.hInstance = hInstance; wc.hCursor = LoadCursor(nullptr, IDC_ARROW); wc.hbrBackground = g_brBg; wc.lpszClassName = L"WhisperDictationClass"; wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1)); RegisterClassEx(&wc); LoadSettings(g_set); g_pinned = g_set.pinned; g_autoPaste = g_set.autoPaste; g_autoHide = g_set.autoHide; hMainWnd = CreateWindowExW( g_pinned ? WS_EX_TOPMOST : 0, L"WhisperDictationClass", L"Dictation", WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, g_set.winX, g_set.winY, g_set.winW, g_set.winH, nullptr, nullptr, hInstance, nullptr); if (!hMainWnd) return FALSE; g_dpiScale = GetDpiForWindow(hMainWnd) / 96.0f; if (g_dpiScale <= 0.0f) g_dpiScale = 1.0f; RecreateFonts(g_dpiScale); BOOL dark = TRUE; DwmSetWindowAttribute(hMainWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark, sizeof(dark)); COLORREF cap = CR_BG, bord = RGB(0x5A,0x60,0x6C), txt = CR_TEXT; DwmSetWindowAttribute(hMainWnd, DWMWA_CAPTION_COLOR, &cap, sizeof(cap)); DwmSetWindowAttribute(hMainWnd, DWMWA_BORDER_COLOR, &bord, sizeof(bord)); DwmSetWindowAttribute(hMainWnd, DWMWA_TEXT_COLOR, &txt, sizeof(txt)); int corner = DWMWCP_ROUND; DwmSetWindowAttribute(hMainWnd, DWMWA_WINDOW_CORNER_PREFERENCE, &corner, sizeof(corner)); { HWND hBtnRecord = CreateWindow(L"BUTTON", L"", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_OWNERDRAW, 0, 0, 0, 0, hMainWnd, (HMENU)ID_BTN_RECORD, hInst, nullptr); SetWindowTheme(hBtnRecord, L"", L""); HWND hPin = CreateWindow(L"BUTTON", L"", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_OWNERDRAW, 0, 0, 0, 0, hMainWnd, (HMENU)ID_BTN_PIN, hInst, nullptr); SetWindowTheme(hPin, L"", L""); CreateWindow(L"STATIC", L"Ready", WS_CHILD | WS_VISIBLE | SS_LEFT, 0, 0, 0, 0, hMainWnd, (HMENU)ID_STATIC_STATUS, hInst, nullptr); HWND hEdit = CreateWindowExW(0, L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE | ES_AUTOVSCROLL, 0, 0, 0, 0, hMainWnd, (HMENU)ID_EDIT_TEXT, hInst, nullptr); SetWindowTheme(hEdit, L"DarkMode_Explorer", nullptr); SendMessage(hEdit, WM_SETFONT, (WPARAM)g_fText, TRUE); int editMargin = (int)(10 * g_dpiScale); SendMessage(hEdit, EM_SETMARGINS, EC_LEFTMARGIN | EC_RIGHTMARGIN, MAKELONG(editMargin, editMargin)); CreateWindowW(L"STATIC", L"Your transcription will appear here\u2026", WS_CHILD | WS_VISIBLE | SS_LEFT, 0, 0, 0, 0, hMainWnd, (HMENU)ID_STATIC_PLACEHOLDER, hInst, nullptr); HWND hSelAudio = CreateWindow(L"BUTTON", L"", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_OWNERDRAW, 0, 0, 0, 0, hMainWnd, (HMENU)ID_SEL_AUDIO, hInst, nullptr); SetWindowTheme(hSelAudio, L"", L""); HWND hSelModel = CreateWindow(L"BUTTON", L"", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_OWNERDRAW, 0, 0, 0, 0, hMainWnd, (HMENU)ID_SEL_MODEL, hInst, nullptr); SetWindowTheme(hSelModel, L"", L""); HWND hCopy = CreateWindow(L"BUTTON", L"", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_OWNERDRAW, 0, 0, 0, 0, hMainWnd, (HMENU)ID_BTN_COPY, hInst, nullptr); SetWindowTheme(hCopy, L"", L""); HWND hPaste = CreateWindow(L"BUTTON", L"", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_OWNERDRAW, 0, 0, 0, 0, hMainWnd, (HMENU)ID_BTN_PASTE, hInst, nullptr); SetWindowTheme(hPaste, L"", L""); HWND hClear = CreateWindow(L"BUTTON", L"", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_OWNERDRAW, 0, 0, 0, 0, hMainWnd, (HMENU)ID_BTN_CLEAR, hInst, nullptr); SetWindowTheme(hClear, L"", L""); } ShowWindow(GetDlgItem(hMainWnd, ID_BTN_RECORD), SW_HIDE); ShowWindow(GetDlgItem(hMainWnd, ID_BTN_PIN), SW_HIDE); ShowWindow(GetDlgItem(hMainWnd, ID_STATIC_STATUS), SW_HIDE); ShowWindow(GetDlgItem(hMainWnd, ID_STATIC_PLACEHOLDER), SW_HIDE); ShowWindow(GetDlgItem(hMainWnd, ID_SEL_AUDIO), SW_HIDE); ShowWindow(GetDlgItem(hMainWnd, ID_SEL_MODEL), SW_HIDE); ShowWindow(GetDlgItem(hMainWnd, ID_BTN_COPY), SW_HIDE); ShowWindow(GetDlgItem(hMainWnd, ID_BTN_PASTE), SW_HIDE); ShowWindow(GetDlgItem(hMainWnd, ID_BTN_CLEAR), SW_HIDE); SendMessageW(hMainWnd, WM_CHANGEUISTATE, MAKEWPARAM(UIS_SET, UISF_HIDEFOCUS), 0); SDL_Init(SDL_INIT_AUDIO); RefreshAudioDevices(hMainWnd); RefreshModelList(hMainWnd); CreateDirectoryA((exe_dir() + "\\models").c_str(), nullptr); { WIN32_FIND_DATAA fd; HANDLE h = FindFirstFileA((exe_dir() + "\\models\\*.part").c_str(), &fd); if (h != INVALID_HANDLE_VALUE) { do { DeleteFileA((exe_dir() + "\\models\\" + fd.cFileName).c_str()); } while (FindNextFileA(h, &fd)); FindClose(h); } } bool has_gpu = DetectGPUAvailability(); std::string model = SelectOptimalModel(has_gpu); if (!g_set.modelFile.empty()) { std::string mf; { int len = WideCharToMultiByte(CP_UTF8, 0, g_set.modelFile.c_str(), -1, nullptr, 0, nullptr, nullptr); if (len > 0) { mf.resize(len - 1); WideCharToMultiByte(CP_UTF8, 0, g_set.modelFile.c_str(), -1, &mf[0], len, nullptr, nullptr); } } if (GetFileAttributesA(mf.c_str()) != INVALID_FILE_ATTRIBUTES) { g_config.model_path = mf; for (int i = 0; i < (int)g_modelComboPaths.size(); ++i) { std::string p = exe_dir() + "\\" + g_modelComboPaths[i]; if (p == mf) { g_modelSel = i; break; } } } } if (g_config.model_path.empty()) { if (!g_modelComboPaths.empty()) g_config.model_path = exe_dir() + "\\" + g_modelComboPaths[0]; else g_config.model_path = exe_dir() + "\\" + model; } g_config.n_threads = 0; g_config.capture_id = g_set.captureId; SeedDefaults(g_timing, g_config.model_path); LoadTiming(g_timing, g_config.model_path); LoadStats(g_stats); g_history = LoadHistoryIndex(); { char log[256]; snprintf(log, sizeof(log), "Startup model=%s threads=%d capture_id=%d", g_config.model_path.c_str(), g_config.n_threads, g_config.capture_id); LogLine(log); } 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"Dictation"); Shell_NotifyIcon(NIM_ADD, &nid); bool hkOk1 = RegisterHotKey(hMainWnd, HK_TOGGLE, g_set.hkMods, g_set.hkVk) != FALSE; bool hkOk2 = RegisterHotKey(hMainWnd, HK_HIDE, MOD_CONTROL | MOD_SHIFT, 'H') != FALSE; if (!hkOk1) SetStatus(hMainWnd, L"Hotkey in use \u2014 edit win-dictation.ini"); g_tx.set_result_callback([](const std::string& t) { PostMessage(hMainWnd, WM_APP_RESULT, (WPARAM)new std::string(t), 0); }); g_tx.set_progress_callback([](int p) { PostMessage(hMainWnd, WM_APP_PROGRESS, (WPARAM)p, 0); }); std::thread([] { bool ok = g_tx.preload(g_config); g_modelOk = ok; g_modelLoaded = true; LogLine(ok ? "Model loaded successfully" : "Model load FAILED"); }).detach(); SetTimer(hMainWnd, ID_TIMER_UPDATE, 50, nullptr); SetStatus(hMainWnd, L"Loading model\u2026"); ShowWindow(hMainWnd, nCmdShow); { RECT rc; GetClientRect(hMainWnd, &rc); LayoutControls(hMainWnd, rc.right, rc.bottom); LayoutWidgets(rc.right, rc.bottom); } InvalidateRect(hMainWnd, nullptr, FALSE); g_initializing = false; MSG msg; while (GetMessage(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } KillTimer(hMainWnd, ID_TIMER_UPDATE); UnregisterHotKey(hMainWnd, HK_TOGGLE); UnregisterHotKey(hMainWnd, HK_HIDE); g_tx.cancel(); g_dl.requestCancel(); g_dl.join(); Shell_NotifyIcon(NIM_DELETE, &nid); SDL_Quit(); ReleaseMutex(g_hMutex); CloseHandle(g_hMutex); delete g_gpUI; delete g_gpUISemi; delete g_gpSmall; delete g_gpText; delete g_gpIcon; g_gpUI = g_gpUISemi = g_gpSmall = g_gpText = g_gpIcon = nullptr; GdiplusShutdown(g_gdipToken); DeleteObject(g_fUI); DeleteObject(g_fUISemi); DeleteObject(g_fSmall); DeleteObject(g_fText); DeleteObject(g_brBg); DeleteObject(g_brSurface); return (int)msg.wParam; } void DrawVU(Graphics& g, const RECT& r, float level) { const int N = 14, gap = 3; int w = r.right - r.left; if (w <= 0) return; int segW = (w - gap * (N - 1)) / N; if (segW < 1) return; int h = r.bottom - r.top; int lit = (int)(level * N + 0.5f); for (int i = 0; i < N; ++i) { int x = r.left + i * (segW + gap); Rect seg(x, r.top, segW, h); FillRound(g, (i < lit) ? C_GOOD : C_SURFACEHI, seg, 2); } } void DrawProgress(Graphics& g, const RECT& r, float frac) { Rect track(r.left, r.top, r.right - r.left, r.bottom - r.top); FillRound(g, C_SURFACEHI, track, 4); frac = frac < 0.0f ? 0.0f : (frac > 1.0f ? 1.0f : frac); int fullW = r.right - r.left; int w = (int)(fullW * frac); if (w > 0) { Rect fill(r.left, r.top, w, r.bottom - r.top); FillRound(g, C_ACCENT, fill, 4); } if (frac >= 0.949f && frac < 1.0f) { double ph = (GetTickCount() % 1100) / 1100.0; BYTE a = (BYTE)(70 + 150 * (0.5 + 0.5 * sin(ph * 6.2831853))); Rect tail(r.left + w, r.top, fullW - w, r.bottom - r.top); FillRound(g, Color(a, 0x6E, 0x8B, 0xFF), tail, 4); } } LRESULT CALLBACK PopupProc(HWND h, UINT m, WPARAM w, LPARAM l) { switch (m) { case WM_MOUSEMOVE: { int row = GET_Y_LPARAM(l) / 30; if (row != g_pop.hot) { g_pop.hot = row; InvalidateRect(h, nullptr, FALSE); } TRACKMOUSEEVENT t{ sizeof(t), TME_LEAVE, h, 0 }; TrackMouseEvent(&t); return 0; } case WM_MOUSELEAVE: g_pop.hot = -1; InvalidateRect(h, nullptr, FALSE); return 0; case WM_LBUTTONUP: { int row = GET_Y_LPARAM(l) / 30; if (row >= 0 && row < (int)g_pop.items.size()) PostMessageW(g_pop.owner, WM_APP_SELECT, (WPARAM)g_pop.ctrlId, (LPARAM)row); DestroyWindow(h); return 0; } case WM_KILLFOCUS: DestroyWindow(h); return 0; case WM_ERASEBKGND: return 1; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(h, &ps); RECT rc; GetClientRect(h, &rc); HDC mem = CreateCompatibleDC(hdc); HBITMAP bmp = CreateCompatibleBitmap(hdc, rc.right, rc.bottom); HBITMAP old = (HBITMAP)SelectObject(mem, bmp); { Graphics g(mem); g.SetSmoothingMode(SmoothingModeAntiAlias); Rect all(0, 0, rc.right, rc.bottom); FillRound(g, T_CARD, all, 10); StrokeRound(g, T_FAINT, all, 10, 1.0f); Font f(mem, g_fUI); for (int i = 0; i < (int)g_pop.items.size(); ++i) { Rect row(3, i * 30 + 3, rc.right - 6, 28); if (i == g_pop.hot) FillRound(g, T_CARD_HI, row, 7); RectF tb((REAL)row.X + 9, (REAL)row.Y, (REAL)row.Width - 12, (REAL)row.Height); DrawTextC(g, g_pop.items[i].c_str(), f, (i == g_pop.sel) ? T_ACCENT : T_TEXT, tb, StringAlignmentNear, StringAlignmentCenter); } } BitBlt(hdc, 0, 0, rc.right, rc.bottom, mem, 0, 0, SRCCOPY); SelectObject(mem, old); DeleteObject(bmp); DeleteDC(mem); EndPaint(h, &ps); return 0; } } return DefWindowProc(h, m, w, l); } void ShowSelectPopup(HWND owner, int ctrlId, const std::vector& items, int sel, const RectF& anchor) { static bool reg = false; if (!reg) { WNDCLASSEXW wc{ sizeof(wc) }; wc.lpfnWndProc = PopupProc; wc.hInstance = hInst; wc.hCursor = LoadCursor(nullptr, IDC_ARROW); wc.hbrBackground = CreateSolidBrush(CR_SURFACE); wc.lpszClassName = L"DictPopup"; RegisterClassExW(&wc); reg = true; } g_pop = { items, sel, -1, owner, ctrlId }; POINT tl{ (LONG)anchor.X, (LONG)(anchor.Y + anchor.Height) }; ClientToScreen(owner, &tl); int h = (int)items.size() * 30 + 6, wdt = (int)anchor.Width; HWND p = CreateWindowExW(WS_EX_TOOLWINDOW | WS_EX_TOPMOST, L"DictPopup", L"", WS_POPUP, tl.x, tl.y + 2, wdt, h, owner, nullptr, hInst, nullptr); int corner = DWMWCP_ROUND; DwmSetWindowAttribute(p, DWMWA_WINDOW_CORNER_PREFERENCE, &corner, sizeof(corner)); ShowWindow(p, SW_SHOWNA); SetFocus(p); } void LayoutControls(HWND h, int W, int H) { float s = g_dpiScale; const int M = (int)(16 * s), row = (int)(36 * s), gap = (int)(10 * s); int x = M, y = M, innerW = W - 2 * M; int pinW = (int)(78 * s), recW = innerW - pinW - gap; MoveWindow(GetDlgItem(h, ID_BTN_RECORD), x, y, recW, row, TRUE); MoveWindow(GetDlgItem(h, ID_BTN_PIN), x + recW + gap, y, pinW, row, TRUE); y += row + gap; int vuH = (int)(8 * s); g_vuRect = { x, y, x + innerW, y + vuH }; y += vuH + gap; MoveWindow(GetDlgItem(h, ID_STATIC_STATUS), x, y, innerW, (int)(18 * s), TRUE); y += (int)(18 * s) + gap; int bottom = (row + gap) * 2; int panelTop = y; int textH = H - y - M - bottom; if (textH < (int)(70 * s)) textH = (int)(70 * s); int editPad = (int)(10 * s); MoveWindow(GetDlgItem(h, ID_EDIT_TEXT), x + editPad, panelTop + editPad, innerW - 2 * editPad, textH - 2 * editPad, TRUE); MoveWindow(GetDlgItem(h, ID_STATIC_PLACEHOLDER), x + (int)(16 * s), panelTop + (int)(16 * s), innerW - (int)(32 * s), (int)(22 * s), TRUE); y += textH + gap; int halfW = (innerW - gap) / 2; MoveWindow(GetDlgItem(h, ID_SEL_AUDIO), x, y, halfW, row, TRUE); MoveWindow(GetDlgItem(h, ID_SEL_MODEL), x + halfW + gap, y, halfW, row, TRUE); y += row + gap; int thirdW = (innerW - gap * 2) / 3; MoveWindow(GetDlgItem(h, ID_BTN_COPY), x, y, thirdW, row, TRUE); MoveWindow(GetDlgItem(h, ID_BTN_PASTE), x + thirdW + gap, y, thirdW, row, TRUE); MoveWindow(GetDlgItem(h, ID_BTN_CLEAR), x + (thirdW + gap) * 2, y, thirdW, row, TRUE); g_panelRect = { x, panelTop, x + innerW, panelTop + textH }; InvalidateRect(h, nullptr, FALSE); } void LayoutWidgets(int W, int H) { for (int i = 0; i < (int)std::size(g_w); ++i) g_w[i].kind = (WK)i; for (auto& w : g_w) w.r = RectF(0, 0, 0, 0); float s = g_dpiScale; const REAL M = 16 * s, row = 36 * s, gap = 10 * s; REAL x = M, y = M, innerW = W - 2 * M; REAL iconW = 44 * s; REAL recW = innerW - 2 * (iconW + gap); g_w[(int)WK::RecordHero].r = RectF(x, y, recW, row); g_w[(int)WK::Pin].r = RectF(x + recW + gap, y, iconW, row); g_w[(int)WK::SettingsCog].r = RectF(x + recW + gap + iconW + gap, y, iconW, row); y += row + gap; REAL vuH = 8 * s; g_vuRect = { (int)x, (int)y, (int)(x + innerW), (int)(y + vuH) }; y += vuH + gap; REAL statusH = 18 * s; y += statusH + gap; // status strip space (drawn by PaintSurface) int bottom = (int)((row + gap) * 2); REAL panelTop = y; int textH = H - (int)y - (int)M - bottom; if (textH < (int)(70 * s)) textH = (int)(70 * s); int editPad = (int)(10 * s); MoveWindow(GetDlgItem(hMainWnd, ID_EDIT_TEXT), (int)(x + editPad), (int)(panelTop + editPad), (int)(innerW - 2 * editPad), textH - 2 * editPad, TRUE); g_w[(int)WK::Transcript].r = RectF(x + editPad, panelTop + editPad, innerW - 2 * editPad, (REAL)(textH - 2 * editPad)); y += textH + gap; REAL halfW = (innerW - gap) / 2; g_w[(int)WK::SelAudio].r = RectF(x, y, halfW, row); g_w[(int)WK::History].r = RectF(x + halfW + gap, y, halfW, row); y += row + gap; REAL thirdW = (innerW - gap * 2) / 3; g_w[(int)WK::Copy].r = RectF(x, y, thirdW, row); g_w[(int)WK::Paste].r = RectF(x + thirdW + gap, y, thirdW, row); g_w[(int)WK::Clear].r = RectF(x + (thirdW + gap) * 2, y, thirdW, row); g_panelRect = { (int)x, (int)panelTop, (int)(x + innerW), (int)(panelTop + textH) }; } void DrawCard(Graphics& g, const Rect& cardRect) { FillRound(g, T_CARD, cardRect, (int)(16 * g_dpiScale)); int r = (int)(16 * g_dpiScale); int d = r * 2; if (d < 1) return; GraphicsPath p; p.AddArc(cardRect.X, cardRect.Y, d, d, 180, 90); p.AddArc(cardRect.GetRight() - d, cardRect.Y, d, d, 270, 90); p.AddArc(cardRect.GetRight() - d, cardRect.GetBottom() - d, d, d, 0, 90); p.AddArc(cardRect.X, cardRect.GetBottom() - d, d, d, 90, 90); p.CloseFigure(); Pen topLight(T_TOPLIGHT, 1.0f); g.DrawPath(&topLight, &p); } void DrawHero(Graphics& g, const Widget& w) { Rect rc((int)w.r.X, (int)w.r.Y, (int)w.r.Width, (int)w.r.Height); bool rec = g_tx.is_recording(); bool hover = w.hover; bool pressed = w.pressed; Color fill = rec ? T_DANGER : (hover ? T_ACCENT_HI : T_ACCENT); if (rec) { double ph = (GetTickCount() % 1400) / 1400.0; int add = (int)(18 * (0.5 + 0.5 * sin(ph * 6.2831853))); fill = Color(255, (BYTE)std::min(255, 0xFF), (BYTE)std::min(255, 0x5C + add), (BYTE)std::min(255, 0x5C + add)); } if (pressed) fill = Color(255, (BYTE)(fill.GetR() * 0.85f), (BYTE)(fill.GetG() * 0.85f), (BYTE)(fill.GetB() * 0.85f)); Rect pill = rc; pill.Inflate(-1, -1); FillRound(g, fill, pill, pill.Height / 2); int cx = pill.X + (int)(22 * g_dpiScale), cy = pill.Y + pill.Height / 2; SolidBrush white(Color(255, 255, 255, 255)); if (rec) { Rect sq(cx - 7, cy - 7, 14, 14); FillRound(g, Color(255, 255, 255, 255), sq, 3); } else { g.FillEllipse(&white, cx - 7, cy - 7, 14, 14); } RectF tb((REAL)(pill.X + (int)(36 * g_dpiScale)), (REAL)pill.Y, (REAL)(pill.Width - (int)(44 * g_dpiScale)), (REAL)pill.Height); DrawTextC(g, rec ? L"Stop" : L"Record", *g_gpUISemi, Color(255, 255, 255, 255), tb, StringAlignmentNear, StringAlignmentCenter); } void DrawGhost(Graphics& g, const Widget& w, const wchar_t* label, bool active) { Rect rc((int)w.r.X, (int)w.r.Y, (int)w.r.Width, (int)w.r.Height); Rect chip = rc; chip.Inflate(-1, -1); float a = w.anim; if (a > 0.001f) { BYTE bgAlpha = (BYTE)(std::min(255, (int)(255 * a))); Color bg(T_CARD_HI.GetA(), T_CARD_HI.GetR(), T_CARD_HI.GetG(), T_CARD_HI.GetB()); if (w.pressed) bg = Color(bgAlpha, T_CARD_LO.GetR(), T_CARD_LO.GetG(), T_CARD_LO.GetB()); else bg = Color(bgAlpha, T_CARD_HI.GetR(), T_CARD_HI.GetG(), T_CARD_HI.GetB()); FillRound(g, bg, chip, (int)(10 * g_dpiScale)); } BYTE tr = (BYTE)(T_DIM.GetR() + (T_TEXT.GetR() - T_DIM.GetR()) * a); BYTE tg = (BYTE)(T_DIM.GetG() + (T_TEXT.GetG() - T_DIM.GetG()) * a); BYTE tb = (BYTE)(T_DIM.GetB() + (T_TEXT.GetB() - T_DIM.GetB()) * a); Color tc(255, tr, tg, tb); if (active && a < 0.01f) tc = T_ACCENT; RectF textBox((REAL)chip.X, (REAL)chip.Y, (REAL)chip.Width, (REAL)chip.Height); DrawTextC(g, label, *g_gpUI, tc, textBox, StringAlignmentCenter, StringAlignmentCenter); } void DrawPinSurface(Graphics& g, const Widget& w) { Rect rc((int)w.r.X, (int)w.r.Y, (int)w.r.Width, (int)w.r.Height); Rect chip = rc; chip.Inflate(-1, -1); float a = w.anim; if (a > 0.001f) { BYTE bgAlpha = (BYTE)(std::min(255, (int)(255 * a))); Color bg = w.pressed ? Color(bgAlpha, T_CARD_LO.GetR(), T_CARD_LO.GetG(), T_CARD_LO.GetB()) : Color(bgAlpha, T_CARD_HI.GetR(), T_CARD_HI.GetG(), T_CARD_HI.GetB()); FillRound(g, bg, chip, (int)(10 * g_dpiScale)); } Color tc = g_pinned ? T_ACCENT : (a > 0.01f ? T_TEXT : T_FAINT); RectF tb((REAL)chip.X, (REAL)chip.Y, (REAL)chip.Width, (REAL)chip.Height); DrawTextC(g, g_pinned ? L"Pinned" : L"Pin", *g_gpUI, tc, tb, StringAlignmentCenter, StringAlignmentCenter); } void DrawIconChip(Graphics& g, const Widget& w, const wchar_t* glyph, const wchar_t* fallback, bool active) { Rect chip((int)w.r.X, (int)w.r.Y, (int)w.r.Width, (int)w.r.Height); chip.Inflate(-1, -1); float a = w.anim; if (a > 0.001f) { BYTE al = (BYTE)std::min(255, (int)(255 * a)); FillRound(g, Color(al, T_CARD_HI.GetR(), T_CARD_HI.GetG(), T_CARD_HI.GetB()), chip, (int)(10 * g_dpiScale)); } Color tc = active ? T_ACCENT : (a > 0.01f ? T_TEXT : T_FAINT); RectF tb((REAL)chip.X, (REAL)chip.Y, (REAL)chip.Width, (REAL)chip.Height); if (g_gpIcon) DrawTextC(g, glyph, *g_gpIcon, tc, tb, StringAlignmentCenter, StringAlignmentCenter); else DrawTextC(g, fallback, *g_gpUI, tc, tb, StringAlignmentCenter, StringAlignmentCenter); } void DrawSelectSurface(Graphics& g, const Widget& w, const std::wstring& text) { Rect rc((int)w.r.X, (int)w.r.Y, (int)w.r.Width, (int)w.r.Height); Rect field = rc; field.Inflate(-1, -1); float a = w.anim; if (a > 0.001f) { BYTE bgAlpha = (BYTE)(std::min(255, (int)(255 * a))); FillRound(g, Color(bgAlpha, T_CARD_HI.GetR(), T_CARD_HI.GetG(), T_CARD_HI.GetB()), field, (int)(9 * g_dpiScale)); } else { FillRound(g, T_CARD, field, (int)(9 * g_dpiScale)); } Pen border(T_FAINT, 1.0f); Rect brd = field; brd.Inflate(-1, -1); StrokeRound(g, T_FAINT, brd, (int)(9 * g_dpiScale), 1.0f); RectF tb((REAL)field.X + (10 * g_dpiScale), (REAL)field.Y, (REAL)(field.Width - 28 * g_dpiScale), (REAL)field.Height); DrawTextC(g, text.c_str(), *g_gpUI, T_TEXT, tb, StringAlignmentNear, StringAlignmentCenter); int cx = field.X + field.Width - (int)(16 * g_dpiScale), cy = field.Y + field.Height / 2; Pen pen(T_DIM, 1.6f); g.DrawLine(&pen, cx - 4, cy - 2, cx, cy + 2); g.DrawLine(&pen, cx, cy + 2, cx + 4, cy - 2); } void DrawStatusStrip(Graphics& g, const RectF& strip) { RECT r = { (int)strip.X, (int)strip.Y, (int)(strip.X + strip.Width), (int)(strip.Y + strip.Height) }; if (g_tx.is_busy()) { DrawProgress(g, r, g_progressFrac); } else if (g_tx.is_recording()) { DrawVU(g, r, g_energy); } } void PaintSurface(HWND hwnd) { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); RECT rcCli; GetClientRect(hwnd, &rcCli); int W = rcCli.right - rcCli.left, H = rcCli.bottom - rcCli.top; if (W <= 0 || H <= 0) { EndPaint(hwnd, &ps); return; } HDC mem = CreateCompatibleDC(hdc); HBITMAP bmp = CreateCompatibleBitmap(hdc, W, H); HBITMAP old = (HBITMAP)SelectObject(mem, bmp); { Graphics g(mem); g.SetSmoothingMode(SmoothingModeAntiAlias); g.SetTextRenderingHint(TextRenderingHintClearTypeGridFit); SolidBrush bg(T_BG); g.FillRectangle(&bg, 0, 0, W, H); if (g_view == View::Settings) { PaintSettings(g, hwnd, W, H); } else { Rect panel(g_panelRect.left, g_panelRect.top, g_panelRect.right - g_panelRect.left, g_panelRect.bottom - g_panelRect.top); DrawCard(g, panel); for (auto& w : g_w) { switch (w.kind) { case WK::RecordHero: DrawHero(g, w); break; case WK::Copy: DrawGhost(g, w, L"Copy", false); break; case WK::Paste: DrawGhost(g, w, L"Paste", false); break; case WK::Clear: DrawGhost(g, w, L"Clear", false); break; case WK::Pin: DrawIconChip(g, w, L"\uE718", L"P", g_pinned); break; case WK::SettingsCog: DrawIconChip(g, w, L"\uE713", L"\u2699", false); break; case WK::History: DrawSelectSurface(g, w, L"History"); break; case WK::SelAudio: DrawSelectSurface(g, w, g_audioItems.empty() ? L"No devices" : g_audioItems[g_audioSel]); break; case WK::Transcript: break; } } RECT vr = g_vuRect; RectF stripRect((REAL)vr.left, (REAL)vr.top, (REAL)(vr.right - vr.left), (REAL)(vr.bottom - vr.top)); DrawStatusStrip(g, stripRect); wchar_t statusBuf[256]; if (g_tx.is_recording()) { int secs = g_recordingSecs.load(); swprintf_s(statusBuf, L"Recording %d:%02d", secs / 60, secs % 60); } else if (g_tx.is_busy()) { int pct = (int)(g_progressFrac*100.0f + 0.5f); float total = g_tx.audio_seconds(); int mm = (int)total / 60; int ss = (int)total % 60; int rem = (int)(g_progressRemain + 0.5f); if (g_progressFrac >= 0.949f) swprintf_s(statusBuf, L"Transcribing %d:%02d \u2022 95%% \u2022 finishing\u2026", mm, ss); else swprintf_s(statusBuf, L"Transcribing %d:%02d \u2022 %d%% \u2022 %ds left", mm, ss, pct, rem); } else if (g_modelLoaded.load()) { if (!g_statusOverride.empty() && GetTickCount() < g_statusOverrideUntil) { wcscpy_s(statusBuf, g_statusOverride.c_str()); } else if (!g_modelOk.load()) { swprintf_s(statusBuf, L"Model not found \u2014 check models folder"); } else { swprintf_s(statusBuf, L"Ready \u2022 %d threads", g_tx.threads()); } } else { swprintf_s(statusBuf, L"Loading model\u2026"); } REAL vy = g_vuRect.top + g_vuRect.bottom - g_vuRect.top + (REAL)(10 * g_dpiScale); RectF statusBox((REAL)g_vuRect.left, vy, (REAL)(g_vuRect.right - g_vuRect.left), (REAL)(18 * g_dpiScale)); DrawTextC(g, statusBuf, *g_gpSmall, T_DIM, statusBox, StringAlignmentNear, StringAlignmentNear); bool empty = GetWindowTextLengthW(GetDlgItem(hwnd, ID_EDIT_TEXT)) == 0; if (empty && !g_tx.is_busy()) { RectF placeholderBox(g_w[(int)WK::Transcript].r.X + (6 * g_dpiScale), g_w[(int)WK::Transcript].r.Y + (6 * g_dpiScale), g_w[(int)WK::Transcript].r.Width, (REAL)(22 * g_dpiScale)); DrawTextC(g, L"Your transcription will appear here\u2026", *g_gpText, T_DIM, placeholderBox, StringAlignmentNear, StringAlignmentNear); } } // else (view == Main) } BitBlt(hdc, 0, 0, W, H, mem, 0, 0, SRCCOPY); SelectObject(mem, old); DeleteObject(bmp); DeleteDC(mem); EndPaint(hwnd, &ps); } int HitTest(POINT p) { for (int i = 0; i < (int)std::size(g_w); ++i) if (g_w[i].kind != WK::Transcript && g_w[i].r.Contains((REAL)p.x, (REAL)p.y)) return i; return -1; } void OnClick(HWND hWnd, WK kind) { if (!g_modelLoaded.load() && kind != WK::RecordHero && kind != WK::Clear && kind != WK::Copy && kind != WK::Paste && kind != WK::Pin && kind != WK::History && kind != WK::SettingsCog && kind != WK::SelAudio) return; switch (kind) { case WK::RecordHero: PostMessage(hWnd, WM_HOTKEY, HK_TOGGLE, 0); break; case WK::Copy: { int len = GetWindowTextLengthW(GetDlgItem(hWnd, ID_EDIT_TEXT)); if (len > 0) { std::vector buf(len + 1); GetDlgItemTextW(hWnd, ID_EDIT_TEXT, buf.data(), (int)buf.size()); std::wstring w(buf.data()); int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, nullptr, 0, nullptr, nullptr); std::string u8(u8len ? u8len - 1 : 0, '\0'); if (u8len) WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &u8[0], u8len, nullptr, nullptr); SetClipboardTextUtf8(hWnd, u8); SetStatus(hWnd, L"Copied"); } break; } case WK::Paste: if (g_prevForeground) { HWND target = g_prevForeground; ShowWindow(hWnd, SW_HIDE); Sleep(60); PasteIntoWindow(target); } break; case WK::Clear: { std::wstring cur = GetEditText(hWnd); if (!cur.empty() && cur != g_lastLoadedText) ArchiveSession(cur); g_history = LoadHistoryIndex(); g_lastLoadedText.clear(); g_editDirty = false; SetDlgItemText(hWnd, ID_EDIT_TEXT, L""); UpdatePlaceholder(hWnd); SetStatus(hWnd, L"Saved to history"); break; } case WK::Pin: g_pinned = !g_pinned; SetWindowPos(hWnd, g_pinned ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); InvalidateRect(hWnd, nullptr, FALSE); PersistNow(); break; case WK::SelAudio: ShowSelectPopup(hWnd, ID_SEL_AUDIO, g_audioItems, g_audioSel, g_w[(int)WK::SelAudio].r); break; case WK::History: { wchar_t dbg[64]; swprintf_s(dbg, L"History: %d entries", (int)g_history.size()); SetStatus(hWnd, dbg); std::vector items; for (auto& e : g_history) items.push_back(e.label); if (items.empty()) items.push_back(L"No history yet"); ShowSelectPopup(hWnd, ID_SEL_HISTORY, items, -1, g_w[(int)WK::History].r); break; } case WK::SettingsCog: SwitchView(hWnd, View::Settings); break; default: break; } } void RefreshCatalogStates() { std::string dir = exe_dir(); for (int i = 0; i < kCatalogCount; ++i) { if (g_dl.active.load() && g_dl.itemIndex == i) { g_cat[i].state = DlState::Downloading; continue; } std::string full = dir + "\\models\\" + kCatalog[i].fileName; g_cat[i].state = (GetFileAttributesA(full.c_str()) != INVALID_FILE_ATTRIBUTES) ? DlState::Installed : DlState::NotInstalled; g_cat[i].pct = 0; } } void SwitchView(HWND hwnd, View v) { g_view = v; ShowWindow(GetDlgItem(hwnd, ID_EDIT_TEXT), v == View::Main ? SW_SHOW : SW_HIDE); if (v == View::Settings) { g_pendingModelPath = g_config.model_path; g_setScroll = 0; RefreshCatalogStates(); } RECT rc; GetClientRect(hwnd, &rc); LayoutWidgets(rc.right, rc.bottom); if (v == View::Settings) LayoutSettings(rc.right, rc.bottom); g_setHot = -1; InvalidateRect(hwnd, nullptr, FALSE); } void LayoutSettings(int W, int H) { float s = g_dpiScale; const REAL M = 16 * s, row = 36 * s, gap = 10 * s; g_sBack = RectF(M, M, 90 * s, row); g_sSave = RectF(M, H - M - row, (W - 2*M - gap) / 2, row); g_sCancel = RectF(M + (W - 2*M - gap)/2 + gap, H - M - row, (W - 2*M - gap)/2, row); g_sContent = { (int)M, (int)(M + row + gap), (int)(W - M), (int)(H - M - row - gap) }; REAL cy = (REAL)g_sContent.top - g_setScroll; REAL cw = (REAL)(g_sContent.right - g_sContent.left); cy += 22 * s; for (int i = 0; i < kCatalogCount; ++i) { g_catRect[i].row = RectF(M, cy, cw, 34 * s); REAL bw = 112 * s; g_catRect[i].btn = RectF(M + cw - bw, cy + 3 * s, bw, 28 * s); cy += 40 * s; } cy += 26 * s; REAL statsTop = cy; cy += (REAL)BuildStatsLines().size() * 20 * s; g_statsTopY = statsTop; int contentH = (int)(cy + g_setScroll) - g_sContent.top + (int)(8 * s); int viewH = g_sContent.bottom - g_sContent.top; g_setScrollMax = std::max(0, contentH - viewH); if (g_setScroll > g_setScrollMax) g_setScroll = g_setScrollMax; } void DrawCatalogButton(Graphics& g, int i, bool installed) { const RectF& b = g_catRect[i].btn; Rect br((int)b.X, (int)b.Y, (int)b.Width, (int)b.Height); bool hov = (g_setHot == 100 + i); wchar_t label[32]; Color tc = T_DIM; switch (g_cat[i].state) { case DlState::Installed: wcscpy_s(label, L"Installed"); tc = T_GOOD; break; case DlState::Downloading: swprintf_s(label, 32, L"%d%% \u2715", g_cat[i].pct); tc = T_ACCENT; break; default: wcscpy_s(label, L"Download"); tc = hov ? T_TEXT : T_DIM; break; } if (hov && g_cat[i].state != DlState::Installed) FillRound(g, T_CARD_HI, br, (int)(8 * g_dpiScale)); DrawTextC(g, label, *g_gpSmall, tc, b, StringAlignmentCenter, StringAlignmentCenter); } void OnCatalogButton(HWND hwnd, int i) { switch (g_cat[i].state) { case DlState::Installed: { std::string full = exe_dir() + "\\models\\" + kCatalog[i].fileName; g_pendingModelPath = full; break; } case DlState::Downloading: g_dl.requestCancel(); break; default: { if (g_dl.active.load()) { SetStatus(hwnd, L"One download at a time"); return; } std::wstring url = L"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/" + to_w(kCatalog[i].fileName); std::wstring dest = to_w(exe_dir()) + L"\\models\\" + to_w(kCatalog[i].fileName); g_cat[i].state = DlState::Downloading; g_cat[i].pct = 0; g_dl.start(hwnd, i, url, dest); break; } } InvalidateRect(hwnd, nullptr, FALSE); } void PaintSettings(Graphics& g, HWND hwnd, int W, int H) { float s = g_dpiScale; bool hb = (g_setHot == 200); if (hb) FillRound(g, T_CARD_HI, Rect((int)g_sBack.X,(int)g_sBack.Y,(int)g_sBack.Width,(int)g_sBack.Height), (int)(10*s)); DrawTextC(g, L"\u2190 Back", *g_gpUI, hb ? T_TEXT : T_DIM, g_sBack, StringAlignmentCenter, StringAlignmentCenter); RectF title((REAL)g_sContent.left, g_sBack.Y, (REAL)(g_sContent.right - g_sContent.left), g_sBack.Height); DrawTextC(g, L"Settings", *g_gpUISemi, T_TEXT, title, StringAlignmentCenter, StringAlignmentCenter); g.SetClip(Rect(g_sContent.left, g_sContent.top, g_sContent.right - g_sContent.left, g_sContent.bottom - g_sContent.top)); RectF cap(g_catRect[0].row.X, g_catRect[0].row.Y - 20*s, 300*s, 18*s); DrawTextC(g, L"MODEL", *g_gpSmall, T_FAINT, cap, StringAlignmentNear, StringAlignmentNear); std::string dir = exe_dir(); for (int i = 0; i < kCatalogCount; ++i) { const RectF& r = g_catRect[i].row; std::string rel = std::string("models\\") + kCatalog[i].fileName; bool installed = GetFileAttributesA((dir + "\\" + rel).c_str()) != INVALID_FILE_ATTRIBUTES; bool selected = (g_pendingModelPath == dir + "\\" + rel); if (g_setHot == i && installed) FillRound(g, T_CARD_HI, Rect((int)r.X,(int)r.Y,(int)r.Width,(int)r.Height), (int)(9*s)); int rx = (int)(r.X + 12*s), ry = (int)(r.Y + r.Height/2); Pen ring(installed ? T_DIM : T_FAINT, 1.4f); g.DrawEllipse(&ring, rx-6, ry-6, 12, 12); if (selected) { SolidBrush dot(T_ACCENT); g.FillEllipse(&dot, rx-3, ry-3, 6, 6); } RectF nameBox(r.X + 30*s, r.Y, 150*s, r.Height); DrawTextC(g, kCatalog[i].display, *g_gpUI, installed ? T_TEXT : T_DIM, nameBox, StringAlignmentNear, StringAlignmentCenter); wchar_t meta[96]; swprintf_s(meta, L"%s \u00b7 %s", kCatalog[i].sizeLabel, kCatalog[i].hint); RectF metaBox(r.X + 30*s + 150*s, r.Y, r.Width - 30*s - 150*s - 120*s, r.Height); DrawTextC(g, meta, *g_gpSmall, T_FAINT, metaBox, StringAlignmentNear, StringAlignmentCenter); DrawCatalogButton(g, i, installed); } RectF scap(g_catRect[0].row.X, g_statsTopY - 20*s, 300*s, 18*s); DrawTextC(g, L"STATISTICS", *g_gpSmall, T_FAINT, scap, StringAlignmentNear, StringAlignmentNear); auto lines = BuildStatsLines(); for (size_t i = 0; i < lines.size(); ++i) { RectF lr(g_catRect[0].row.X, g_statsTopY + (REAL)i * 20*s, (REAL)(g_sContent.right - g_sContent.left), 18*s); DrawTextC(g, lines[i].c_str(), *g_gpUI, T_DIM, lr, StringAlignmentNear, StringAlignmentNear); } g.ResetClip(); Rect sv((int)g_sSave.X,(int)g_sSave.Y,(int)g_sSave.Width,(int)g_sSave.Height); FillRound(g, g_setHot == 201 ? T_ACCENT_HI : T_ACCENT, sv, (int)(10*s)); DrawTextC(g, L"Save", *g_gpUISemi, Color(255,255,255,255), g_sSave, StringAlignmentCenter, StringAlignmentCenter); if (g_setHot == 202) FillRound(g, T_CARD_HI, Rect((int)g_sCancel.X,(int)g_sCancel.Y,(int)g_sCancel.Width,(int)g_sCancel.Height), (int)(10*s)); DrawTextC(g, L"Cancel", *g_gpUI, g_setHot == 202 ? T_TEXT : T_DIM, g_sCancel, StringAlignmentCenter, StringAlignmentCenter); } static bool PtIn(const RectF& r, POINT p) { return r.Contains((REAL)p.x, (REAL)p.y); } int SettingsHitTest(POINT p) { if (PtIn(g_sBack, p)) return 200; if (PtIn(g_sSave, p)) return 201; if (PtIn(g_sCancel, p)) return 202; if (!PtInRect(&g_sContent, p)) return -1; for (int i = 0; i < kCatalogCount; ++i) { if (PtIn(g_catRect[i].btn, p)) return 100 + i; if (PtIn(g_catRect[i].row, p)) return i; } return -1; } void OnSettingsClick(HWND hwnd, POINT p) { int hit = SettingsHitTest(p); if (hit == 200 || hit == 202) { SwitchView(hwnd, View::Main); return; } if (hit == 201) { ApplySettings(hwnd); return; } if (hit >= 100 && hit < 100 + kCatalogCount) { OnCatalogButton(hwnd, hit - 100); return; } if (hit >= 0 && hit < kCatalogCount) { std::string full = exe_dir() + "\\models\\" + kCatalog[hit].fileName; if (GetFileAttributesA(full.c_str()) != INVALID_FILE_ATTRIBUTES) { g_pendingModelPath = full; InvalidateRect(hwnd, nullptr, FALSE); } } } void ApplySettings(HWND hwnd) { if (!g_pendingModelPath.empty() && g_pendingModelPath != g_config.model_path) { g_config.model_path = g_pendingModelPath; SeedDefaults(g_timing, g_config.model_path); LoadTiming(g_timing, g_config.model_path); g_modelLoaded = false; g_modelOk = false; std::thread([] { bool ok = g_tx.reload(g_config); g_modelOk = ok; g_modelLoaded = true; }).detach(); PersistNow(); SetStatus(hwnd, L"Settings saved \u2014 loading model\u2026"); } SwitchView(hwnd, View::Main); } void UpdatePlaceholder(HWND hwnd) { bool empty = GetWindowTextLengthW(GetDlgItem(hwnd, ID_EDIT_TEXT)) == 0; ShowWindow(GetDlgItem(hwnd, ID_STATIC_PLACEHOLDER), empty ? SW_SHOW : SW_HIDE); } void RefreshAudioDevices(HWND hwnd) { g_audioItems.clear(); std::vector devices = Transcriber::get_audio_devices(); for (const auto& device : devices) { int len = MultiByteToWideChar(CP_UTF8, 0, device.c_str(), -1, nullptr, 0); if (len > 0) { std::vector wbuf(len); MultiByteToWideChar(CP_UTF8, 0, device.c_str(), -1, wbuf.data(), len); g_audioItems.push_back(wbuf.data()); } } if (g_audioItems.empty()) g_audioItems.push_back(L"No devices"); g_audioSel = 0; g_config.capture_id = 0; InvalidateRect(GetDlgItem(hwnd, ID_SEL_AUDIO), nullptr, FALSE); } void RefreshModelList(HWND hwnd) { g_modelItems.clear(); g_modelComboPaths.clear(); std::string dir = exe_dir(); for (int i = 0; i < kCatalogCount; ++i) { std::string rel = std::string("models\\") + kCatalog[i].fileName; if (GetFileAttributesA((dir + "\\" + rel).c_str()) != INVALID_FILE_ATTRIBUTES) { g_modelItems.push_back(kCatalog[i].display); g_modelComboPaths.push_back(rel); } } g_modelSel = 0; InvalidateRect(GetDlgItem(hwnd, ID_SEL_MODEL), nullptr, FALSE); } void SetStatus(HWND hwnd, const wchar_t* text) { g_statusOverride = text; g_statusOverrideUntil = GetTickCount() + 2500; InvalidateRect(hwnd, nullptr, FALSE); } LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_CTLCOLORSTATIC: { HDC hdc = (HDC)wParam; if (GetDlgCtrlID((HWND)lParam) == ID_STATIC_PLACEHOLDER) { SetTextColor(hdc, RGB(0x9A, 0xA0, 0xAB)); SetBkColor(hdc, CR_SURFACE); return (LRESULT)g_brSurface; } SetTextColor(hdc, CR_TEXT); SetBkColor(hdc, CR_BG); return (LRESULT)g_brBg; } case WM_CTLCOLOREDIT: { HDC hdc = (HDC)wParam; SetTextColor(hdc, CR_TEXT); SetBkColor(hdc, CR_SURFACE); return (LRESULT)g_brSurface; } case WM_ERASEBKGND: return 1; case WM_PAINT: PaintSurface(hWnd); return 0; case WM_MOUSEMOVE: { if (g_view == View::Settings) { POINT p{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; int hot = SettingsHitTest(p); if (hot != g_setHot) { g_setHot = hot; InvalidateRect(hWnd, nullptr, FALSE); } TRACKMOUSEEVENT t{ sizeof(t), TME_LEAVE, hWnd, 0 }; TrackMouseEvent(&t); return 0; } POINT p{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; int hot = HitTest(p); if (hot != g_hot) { if (g_hot >= 0) g_w[g_hot].hover = false; g_hot = hot; if (g_hot >= 0) g_w[g_hot].hover = true; TRACKMOUSEEVENT t{ sizeof(t), TME_LEAVE, hWnd, 0 }; TrackMouseEvent(&t); EnsureAnimating(hWnd); } return 0; } case WM_MOUSELEAVE: if (g_hot >= 0) { g_w[g_hot].hover = false; g_hot = -1; EnsureAnimating(hWnd); } return 0; case WM_LBUTTONDOWN: if (g_view == View::Settings) return 0; g_active = g_hot; if (g_active >= 0) { g_w[g_active].pressed = true; SetCapture(hWnd); EnsureAnimating(hWnd); } return 0; case WM_LBUTTONUP: { if (g_view == View::Settings) { POINT p{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; OnSettingsClick(hWnd, p); return 0; } ReleaseCapture(); POINT p{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; if (g_active >= 0 && HitTest(p) == g_active) OnClick(hWnd, g_w[g_active].kind); if (g_active >= 0) g_w[g_active].pressed = false; g_active = -1; EnsureAnimating(hWnd); return 0; } case WM_MOUSEWHEEL: if (g_view == View::Settings) { g_setScroll -= GET_WHEEL_DELTA_WPARAM(wParam) / 2; g_setScroll = std::max(0, std::min(g_setScroll, g_setScrollMax)); RECT rc; GetClientRect(hWnd, &rc); LayoutSettings(rc.right, rc.bottom); InvalidateRect(hWnd, nullptr, FALSE); } return 0; case WM_DPICHANGED: { g_dpiScale = LOWORD(wParam) / 96.0f; if (g_dpiScale <= 0.0f) g_dpiScale = 1.0f; RecreateFonts(g_dpiScale); RECT* sug = (RECT*)lParam; SetWindowPos(hWnd, nullptr, sug->left, sug->top, sug->right - sug->left, sug->bottom - sug->top, SWP_NOZORDER | SWP_NOACTIVATE); HWND hEdit = GetDlgItem(hWnd, ID_EDIT_TEXT); if (hEdit) SendMessage(hEdit, WM_SETFONT, (WPARAM)g_fText, TRUE); LayoutControls(hWnd, sug->right - sug->left, sug->bottom - sug->top); LayoutWidgets(sug->right - sug->left, sug->bottom - sug->top); if (g_view == View::Settings) LayoutSettings(sug->right - sug->left, sug->bottom - sug->top); InvalidateRect(hWnd, nullptr, TRUE); return 0; } case WM_SIZE: LayoutControls(hWnd, LOWORD(lParam), HIWORD(lParam)); LayoutWidgets(LOWORD(lParam), HIWORD(lParam)); if (g_view == View::Settings) LayoutSettings(LOWORD(lParam), HIWORD(lParam)); return 0; case WM_EXITSIZEMOVE: PersistNow(); return 0; case WM_GETMINMAXINFO: ((MINMAXINFO*)lParam)->ptMinTrackSize.x = (LONG)(340 * g_dpiScale); ((MINMAXINFO*)lParam)->ptMinTrackSize.y = (LONG)(280 * g_dpiScale); return 0; case WM_TIMER: if (wParam == 3) { DWORD now = GetTickCount(); float dt = (now - g_lastFrame) / 1000.0f; g_lastFrame = now; if (dt <= 0.0f) dt = 0.016f; StepAnimations(dt); if (!AnyAnimating()) { StopAnimating(hWnd); } InvalidateRect(hWnd, nullptr, FALSE); } if (wParam == ID_TIMER_UPDATE) { if (g_tx.is_recording()) { g_energy = g_tx.get_audio_energy(); InvalidateRect(hWnd, nullptr, FALSE); static DWORD lastTick = 0; DWORD now = GetTickCount(); if (now - lastTick >= 1000) { g_recordingSecs.store(g_recordingSecs.load() + 1); lastTick = now; } if (g_tx.recorded_seconds() >= kMaxRecordSeconds) { g_lastAudioLen = g_tx.recorded_seconds(); g_busyStart = GetTickCount(); g_lastTick = g_busyStart; g_progress = 0; g_cancelRequested = false; g_est.begin(g_timing.predict(g_lastAudioLen)); g_tx.stop_and_transcribe(); SetStatus(hWnd, L"Max length reached \u2014 transcribing\u2026"); } } else { g_energy = 0.0f; if (g_tx.is_busy()) { DWORD now = GetTickCount(); float dt = (now - g_lastTick) / 1000.0f; g_lastTick = now; if (dt <= 0.0f) dt = 0.05f; g_est.tick(dt, g_progressFrac, g_progressRemain); InvalidateRect(hWnd, nullptr, FALSE); } } } break; case WM_HOTKEY: if (wParam == HK_TOGGLE) { if (g_tx.is_busy()) { g_tx.request_cancel(); g_cancelRequested = true; g_est.reset_busy(); g_progressFrac = 0.0f; g_progressRemain = 0.0f; SetStatus(hWnd, L"Cancelling\u2026"); break; } if (!g_tx.is_recording()) { if (g_view == View::Settings) SwitchView(hWnd, View::Main); if (!g_modelLoaded.load()) { SetStatus(hWnd, L"Loading model\u2026"); break; } if (!g_modelOk.load()) { std::wstring m = L"Model not found:\n" + to_w(g_config.model_path) + L"\n\nPut the .bin there and restart."; MessageBoxW(hWnd, m.c_str(), L"Dictation", MB_OK | MB_ICONWARNING); break; } g_prevForeground = GetForegroundWindow(); ShowWindow(hWnd, SW_SHOWNA); g_recordingSecs = 0; { DWORD s = 0, e = 0; SendMessageW(GetDlgItem(hWnd, ID_EDIT_TEXT), EM_GETSEL, (WPARAM)&s, (LPARAM)&e); g_insStart = s; g_insEnd = e; } if (g_tx.start_recording()) { SetStatus(hWnd, L"Recording\u2026"); } else { SetStatus(hWnd, L"Microphone error"); } } else { g_lastAudioLen = g_tx.recorded_seconds(); g_busyStart = GetTickCount(); g_lastTick = g_busyStart; g_progress = 0; g_cancelRequested = false; g_est.begin(g_timing.predict(g_lastAudioLen)); g_tx.stop_and_transcribe(); SetStatus(hWnd, L"Transcribing\u2026"); } } else if (wParam == HK_HIDE) { if (g_tx.is_recording()) g_tx.cancel(); ShowWindow(hWnd, SW_HIDE); } break; case WM_APP_SHOW: ShowWindow(hWnd, SW_SHOW); SetForegroundWindow(hWnd); break; case WM_APP_RESULT: { float frac, remain; g_est.finish(frac, remain); g_progressFrac = 1.0f; g_progressRemain = 0.0f; double actual = (GetTickCount() - g_busyStart) / 1000.0; if (g_lastAudioLen > 0.5 && actual > 0.2 && !g_cancelRequested) { g_timing.add_sample(g_lastAudioLen, actual); SaveTiming(g_timing, g_config.model_path); } std::string* res = (std::string*)wParam; if (res && !res->empty()) { RecordUsage(g_stats, g_lastAudioLen, actual, to_w(*res)); char log[128]; float secs = g_tx.audio_seconds(); snprintf(log, sizeof(log), "Transcribed %.1fs %d chars", secs, (int)res->size()); LogLine(log); HWND hEdit = GetDlgItem(hWnd, ID_EDIT_TEXT); std::wstring full = GetEditText(hWnd); DWORD s = std::min(g_insStart, (DWORD)full.size()); DWORD e = std::min(g_insEnd, (DWORD)full.size()); std::wstring add = to_w(*res); bool needLead = (s > 0) && !iswspace(full[s - 1]); bool needTrail = (e < full.size()) && !iswspace(full[e]); std::wstring ins = (needLead ? L" " : L"") + add + (needTrail ? L" " : L""); SendMessageW(hEdit, EM_SETSEL, s, e); SendMessageW(hEdit, EM_REPLACESEL, TRUE, (LPARAM)ins.c_str()); SendMessageW(hEdit, EM_SCROLLCARET, 0, 0); UpdatePlaceholder(hWnd); g_lastLoadedText = GetEditText(hWnd); SetClipboardTextUtf8(hWnd, *res); bool pasted = false; if (g_autoPaste && g_prevForeground && g_prevForeground != hWnd && IsWindow(g_prevForeground)) { PasteIntoWindow(g_prevForeground); pasted = true; } if (pasted && g_autoHide) { ShowWindow(hWnd, SW_HIDE); } SetStatus(hWnd, pasted ? L"Pasted" : L"Copied"); } else { SetStatus(hWnd, g_cancelRequested ? L"Cancelled" : L"No speech detected"); } delete res; } break; case WM_APP_PROGRESS: { double t_now = (GetTickCount() - g_busyStart) / 1000.0; g_est.on_whisper(t_now, (int)wParam); InvalidateRect(hWnd, &g_vuRect, FALSE); return 0; } case WM_APP_DLPROGRESS: { int idx = (int)wParam, code = (int)lParam; if (idx >= 0 && idx < kCatalogCount) { if (code >= 0 && code <= 100) { g_cat[idx].state = DlState::Downloading; g_cat[idx].pct = code; } else { g_dl.join(); g_dl.active = false; if (code == 101) { RefreshModelList(hWnd); SetStatus(hWnd, L"Model downloaded"); } else { SetStatus(hWnd, code == -2 ? L"Download cancelled" : L"Download failed"); } RefreshCatalogStates(); } } InvalidateRect(hWnd, nullptr, FALSE); return 0; } case WM_APP_SELECT: { int ctrlId = (int)wParam, idx = (int)lParam; if (ctrlId == ID_SEL_AUDIO) { g_audioSel = idx; g_config.capture_id = idx; InvalidateRect(GetDlgItem(hWnd, ID_SEL_AUDIO), nullptr, FALSE); PersistNow(); } else if (ctrlId == ID_SEL_MODEL && idx >= 0 && idx < (int)g_modelComboPaths.size()) { g_modelSel = idx; g_config.model_path = exe_dir() + "\\" + g_modelComboPaths[idx]; SeedDefaults(g_timing, g_config.model_path); LoadTiming(g_timing, g_config.model_path); g_modelLoaded = false; g_modelOk = false; SetStatus(hWnd, L"Loading model\u2026"); std::thread([] { bool ok = g_tx.reload(g_config); g_modelOk = ok; g_modelLoaded = true; }).detach(); InvalidateRect(GetDlgItem(hWnd, ID_SEL_MODEL), nullptr, FALSE); PersistNow(); } else if (ctrlId == ID_SEL_HISTORY && idx >= 0 && idx < (int)g_history.size()) { std::wstring cur = GetEditText(hWnd); if (!cur.empty() && cur != g_lastLoadedText) ArchiveSession(cur); std::wstring text = ReadFileUtf8(g_history[idx].path); SetWindowTextW(GetDlgItem(hWnd, ID_EDIT_TEXT), text.c_str()); g_lastLoadedText = text; g_editDirty = false; g_history = LoadHistoryIndex(); UpdatePlaceholder(hWnd); SetStatus(hWnd, L"Loaded from history"); } return 0; } case WM_COMMAND: if (LOWORD(wParam) == ID_EDIT_TEXT && HIWORD(wParam) == EN_CHANGE) { g_editDirty = true; break; } switch (LOWORD(wParam)) { case ID_TRAY_EXIT: DestroyWindow(hWnd); break; case ID_TRAY_SHOW: ShowWindow(hWnd, SW_SHOW); SetForegroundWindow(hWnd); break; case ID_TRAY_AUTOPASTE: g_autoPaste = !g_autoPaste; PersistNow(); break; case ID_TRAY_TOPMOST: g_pinned = !g_pinned; SetWindowPos(hWnd, g_pinned ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); InvalidateRect(GetDlgItem(hWnd, ID_BTN_PIN), nullptr, FALSE); PersistNow(); break; case ID_TRAY_AUTOHIDE: g_autoHide = !g_autoHide; PersistNow(); break; case ID_BTN_RECORD: PostMessage(hWnd, WM_HOTKEY, HK_TOGGLE, 0); break; case ID_BTN_CLEAR: { std::wstring cur = GetEditText(hWnd); if (!cur.empty() && cur != g_lastLoadedText) ArchiveSession(cur); g_history = LoadHistoryIndex(); g_lastLoadedText.clear(); g_editDirty = false; SetDlgItemText(hWnd, ID_EDIT_TEXT, L""); UpdatePlaceholder(hWnd); SetStatus(hWnd, L"Saved to history"); break; } case ID_BTN_PIN: g_pinned = !g_pinned; SetWindowPos(hWnd, g_pinned ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); InvalidateRect(GetDlgItem(hWnd, ID_BTN_PIN), nullptr, FALSE); PersistNow(); break; case ID_BTN_COPY: { int len = GetWindowTextLengthW(GetDlgItem(hWnd, ID_EDIT_TEXT)); if (len > 0) { std::vector buf(len + 1); GetDlgItemTextW(hWnd, ID_EDIT_TEXT, buf.data(), (int)buf.size()); std::wstring w(buf.data()); int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, nullptr, 0, nullptr, nullptr); std::string u8(u8len ? u8len - 1 : 0, '\0'); if (u8len) WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &u8[0], u8len, nullptr, nullptr); SetClipboardTextUtf8(hWnd, u8); SetStatus(hWnd, L"Copied"); } } break; case ID_BTN_PASTE: if (g_prevForeground) { HWND target = g_prevForeground; ShowWindow(hWnd, SW_HIDE); Sleep(60); PasteIntoWindow(target); } break; case ID_SEL_AUDIO: ShowSelectPopup(hWnd, ID_SEL_AUDIO, g_audioItems, g_audioSel, g_w[(int)WK::SelAudio].r); break; } 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_CLOSE: ShowWindow(hWnd, SW_HIDE); return 0; case WM_DESTROY: { std::wstring cur = GetEditText(hWnd); if (!cur.empty() && cur != g_lastLoadedText) ArchiveSession(cur); PersistNow(); 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, nullptr); InsertMenu(hMenu, 2, MF_BYPOSITION | MF_STRING | (g_autoPaste ? MF_CHECKED : 0), ID_TRAY_AUTOPASTE, L"Auto-paste"); InsertMenu(hMenu, 3, MF_BYPOSITION | MF_STRING | (g_pinned ? MF_CHECKED : 0), ID_TRAY_TOPMOST, L"Always on top"); InsertMenu(hMenu, 4, MF_BYPOSITION | MF_STRING | (g_autoHide ? MF_CHECKED : 0), ID_TRAY_AUTOHIDE, L"Auto-hide"); InsertMenu(hMenu, 5, MF_BYPOSITION | MF_SEPARATOR, 0, nullptr); InsertMenu(hMenu, 6, MF_BYPOSITION | MF_STRING, ID_TRAY_EXIT, L"Exit"); SetForegroundWindow(hwnd); TrackPopupMenu(hMenu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, hwnd, nullptr); DestroyMenu(hMenu); } std::string exe_dir() { char buf[MAX_PATH]; GetModuleFileNameA(nullptr, buf, MAX_PATH); std::string p(buf); return p.substr(0, p.find_last_of("\\/")); } std::wstring to_w(const std::string& s) { if (s.empty()) return L""; int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, nullptr, 0); std::wstring w(n ? n - 1 : 0, L'\0'); if (n) MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, &w[0], n); return w; } std::wstring GetEditText(HWND hwnd) { HWND e = GetDlgItem(hwnd, ID_EDIT_TEXT); int len = GetWindowTextLengthW(e); std::wstring s; if (len > 0) { s.resize(len + 1); int got = GetWindowTextW(e, &s[0], len + 1); s.resize(got); } return s; } std::vector BuildStatsLines() { std::vector out; wchar_t b[160]; swprintf_s(b, L"Dictated %s of audio across %d clips", FormatHMS(g_stats.totalAudioSec).c_str(), (int)g_stats.totalClips); out.push_back(b); double mins = g_stats.totalAudioSec / 60.0; swprintf_s(b, L"Words %d (%d wpm speaking)", (int)g_stats.totalWords, mins > 0.05 ? (int)(g_stats.totalWords / mins + 0.5) : 0); out.push_back(b); swprintf_s(b, L"Processing %s total (%.1fx real-time on this machine)", FormatHMS(g_stats.totalProcSec).c_str(), g_stats.totalProcSec > 0.5 ? g_stats.totalAudioSec / g_stats.totalProcSec : 0.0); out.push_back(b); swprintf_s(b, L"Longest clip %s", FormatHMS(g_stats.longestClipSec).c_str()); out.push_back(b); double typingSec = (g_stats.totalWords / 40.0) * 60.0; double savedSec = typingSec - g_stats.totalAudioSec; if (savedSec > 60) { swprintf_s(b, L"Time saved ~%s vs typing at 40 wpm", FormatHMS(savedSec).c_str()); out.push_back(b); } return out; } bool SetClipboardTextUtf8(HWND owner, const std::string& utf8) { std::wstring w = to_w(utf8); if (!OpenClipboard(owner)) return false; EmptyClipboard(); size_t bytes = (w.size() + 1) * sizeof(wchar_t); HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, bytes); if (h) { void* p = GlobalLock(h); memcpy(p, w.c_str(), bytes); GlobalUnlock(h); SetClipboardData(CF_UNICODETEXT, h); } CloseClipboard(); return h != nullptr; } void send_ctrl_v() { INPUT in[4] = {}; in[0].type = INPUT_KEYBOARD; in[0].ki.wVk = VK_CONTROL; in[1].type = INPUT_KEYBOARD; in[1].ki.wVk = 'V'; in[2].type = INPUT_KEYBOARD; in[2].ki.wVk = 'V'; in[2].ki.dwFlags = KEYEVENTF_KEYUP; in[3].type = INPUT_KEYBOARD; in[3].ki.wVk = VK_CONTROL; in[3].ki.dwFlags = KEYEVENTF_KEYUP; SendInput(4, in, sizeof(INPUT)); } void PasteIntoWindow(HWND target) { if (!target || !IsWindow(target)) return; DWORD me = GetCurrentThreadId(); DWORD other = GetWindowThreadProcessId(target, nullptr); AttachThreadInput(me, other, TRUE); SetForegroundWindow(target); SetFocus(target); AttachThreadInput(me, other, FALSE); Sleep(40); send_ctrl_v(); } bool DetectGPUAvailability() { const char* info = whisper_print_system_info(); if (!info) return false; return (strstr(info, "CUDA") != nullptr || strstr(info, "Metal") != nullptr || strstr(info, "HIP") != nullptr || strstr(info, "Vulkan") != nullptr); } std::string SelectOptimalModel(bool has_gpu) { std::string dir = exe_dir(); if (has_gpu) { if (GetFileAttributesA((dir + "\\models\\ggml-base.en.bin").c_str()) != INVALID_FILE_ATTRIBUTES) return "models\\ggml-base.en.bin"; if (GetFileAttributesA((dir + "\\models\\ggml-tiny.en.bin").c_str()) != INVALID_FILE_ATTRIBUTES) return "models\\ggml-tiny.en.bin"; } else { if (GetFileAttributesA((dir + "\\models\\ggml-tiny.en.bin").c_str()) != INVALID_FILE_ATTRIBUTES) return "models\\ggml-tiny.en.bin"; if (GetFileAttributesA((dir + "\\models\\ggml-base.en.bin").c_str()) != INVALID_FILE_ATTRIBUTES) return "models\\ggml-base.en.bin"; } return "models\\ggml-base.en.bin"; }