Feature Pack 01: settings view, model downloader, progress pulse, editable transcript, usage stats, persistent history

This commit is contained in:
Win Dictation Dev
2026-06-11 15:51:44 +12:00
parent 04db37599f
commit d56260fdb9
4 changed files with 684 additions and 71 deletions
+447 -71
View File
@@ -28,6 +28,9 @@ using namespace Gdiplus;
#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
@@ -135,6 +138,24 @@ 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<HistoryEntry> 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() {
@@ -161,12 +182,25 @@ 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;
static const wchar_t* kModelNames[] = { L"tiny.en", L"tiny.en-q8_0", L"base.en-q5_1", L"base.en" };
static const char* kModelFiles[] = {
"models\\ggml-tiny.en.bin", "models\\ggml-tiny.en-q8_0.bin",
"models\\ggml-base.en-q5_1.bin", "models\\ggml-base.en.bin",
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<std::string> g_modelComboPaths;
std::vector<std::wstring> g_audioItems; int g_audioSel = 0;
std::vector<std::wstring> g_modelItems; int g_modelSel = 0;
@@ -175,7 +209,7 @@ bool g_initializing = true;
struct PopupState { std::vector<std::wstring> items; int sel; int hot; HWND owner; int ctrlId; };
static PopupState g_pop;
enum class WK { RecordHero, Pin, Copy, Paste, Clear, SelAudio, SelModel, Transcript };
enum class WK { RecordHero, Pin, SettingsCog, Copy, Paste, Clear, SelAudio, History, Transcript };
struct Widget {
WK kind;
RectF r;
@@ -226,7 +260,6 @@ void ShowContextMenu(HWND, POINT);
void RefreshAudioDevices(HWND);
void RefreshModelList(HWND);
void SetStatus(HWND, const wchar_t*);
void UpdateStatus(HWND);
void UpdatePlaceholder(HWND);
void LayoutControls(HWND, int, int);
void LayoutWidgets(int W, int H);
@@ -236,19 +269,28 @@ 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<std::wstring>& items, int sel);
void ShowSelectPopup(HWND owner, int ctrlId, const std::vector<std::wstring>& 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<std::wstring> 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,
@@ -279,11 +321,13 @@ static Gdiplus::Font* GdipFontFromHFont(HFONT hf) {
}
static void RebuildGdipFonts() {
delete g_gpUI; delete g_gpUISemi; delete g_gpSmall; delete g_gpText;
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);
@@ -394,7 +438,7 @@ int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
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 | ES_READONLY,
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);
@@ -447,6 +491,16 @@ int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
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);
@@ -475,6 +529,8 @@ int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
SeedDefaults(g_timing, g_config.model_path);
LoadTiming(g_timing, g_config.model_path);
LoadStats(g_stats);
g_history = LoadHistoryIndex();
{
char log[256];
@@ -532,12 +588,14 @@ int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
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;
g_gpUI = g_gpUISemi = g_gpSmall = g_gpText = nullptr;
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);
@@ -566,8 +624,16 @@ 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 w = (int)((r.right - r.left) * 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) {
@@ -614,7 +680,7 @@ LRESULT CALLBACK PopupProc(HWND h, UINT m, WPARAM w, LPARAM l) {
return DefWindowProc(h, m, w, l);
}
void ShowSelectPopup(HWND owner, int ctrlId, const std::vector<std::wstring>& items, int sel) {
void ShowSelectPopup(HWND owner, int ctrlId, const std::vector<std::wstring>& items, int sel, const RectF& anchor) {
static bool reg = false;
if (!reg) {
WNDCLASSEXW wc{ sizeof(wc) };
@@ -627,10 +693,11 @@ void ShowSelectPopup(HWND owner, int ctrlId, const std::vector<std::wstring>& it
reg = true;
}
g_pop = { items, sel, -1, owner, ctrlId };
RECT rc; GetWindowRect(GetDlgItem(owner, ctrlId), &rc);
int h = (int)items.size() * 30 + 6, wdt = rc.right - rc.left;
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, rc.left, rc.bottom + 2, wdt, h, owner, nullptr, hInst, nullptr);
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);
}
@@ -684,9 +751,11 @@ void LayoutWidgets(int W, int H) {
const REAL M = 16 * s, row = 36 * s, gap = 10 * s;
REAL x = M, y = M, innerW = W - 2 * M;
REAL pinW = 78 * s, recW = innerW - pinW - gap;
g_w[(int)WK::RecordHero].r = RectF(x, y, recW, row);
g_w[(int)WK::Pin].r = RectF(x + recW + gap, y, pinW, row);
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;
@@ -708,7 +777,7 @@ void LayoutWidgets(int W, int H) {
y += textH + gap;
REAL halfW = (innerW - gap) / 2;
g_w[(int)WK::SelAudio].r = RectF(x, y, halfW, row);
g_w[(int)WK::SelModel].r = RectF(x + halfW + gap, 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;
@@ -803,6 +872,22 @@ void DrawPinSurface(Graphics& g, const Widget& w) {
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);
@@ -855,6 +940,9 @@ void PaintSurface(HWND hwnd) {
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);
@@ -866,9 +954,10 @@ void PaintSurface(HWND hwnd) {
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: DrawPinSurface(g, w); 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::SelModel: DrawSelectSurface(g, w, g_modelItems.empty() ? L"\u2014" : g_modelItems[g_modelSel]); break;
case WK::Transcript: break;
}
}
@@ -887,7 +976,10 @@ void PaintSurface(HWND hwnd) {
int mm = (int)total / 60;
int ss = (int)total % 60;
int rem = (int)(g_progressRemain + 0.5f);
swprintf_s(statusBuf, L"Transcribing %d:%02d \u2022 %d%% \u2022 %ds left", mm, ss, pct, rem);
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());
@@ -913,6 +1005,7 @@ void PaintSurface(HWND hwnd) {
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);
@@ -953,10 +1046,18 @@ void OnClick(HWND hWnd, WK kind) {
PasteIntoWindow(target);
}
break;
case WK::Clear:
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,
@@ -965,15 +1066,207 @@ void OnClick(HWND hWnd, WK kind) {
PersistNow();
break;
case WK::SelAudio:
ShowSelectPopup(hWnd, ID_SEL_AUDIO, g_audioItems, g_audioSel);
ShowSelectPopup(hWnd, ID_SEL_AUDIO, g_audioItems, g_audioSel, g_w[(int)WK::SelAudio].r);
break;
case WK::SelModel:
ShowSelectPopup(hWnd, ID_SEL_MODEL, g_modelItems, g_modelSel);
case WK::History: {
std::vector<std::wstring> 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);
@@ -1001,11 +1294,11 @@ void RefreshModelList(HWND hwnd) {
g_modelItems.clear();
g_modelComboPaths.clear();
std::string dir = exe_dir();
for (int i = 0; i < 4; ++i) {
std::string full = dir + "\\" + kModelFiles[i];
if (GetFileAttributesA(full.c_str()) != INVALID_FILE_ATTRIBUTES) {
g_modelItems.push_back(kModelNames[i]);
g_modelComboPaths.push_back(kModelFiles[i]);
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;
@@ -1018,31 +1311,6 @@ void SetStatus(HWND hwnd, const wchar_t* text) {
InvalidateRect(hwnd, nullptr, FALSE);
}
void UpdateStatus(HWND hwnd) {
wchar_t buf[256];
if (g_tx.is_recording()) {
int secs = g_recordingSecs.load();
swprintf_s(buf, L"Recording %d:%02d", secs / 60, secs % 60);
SetStatus(hwnd, buf);
} 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);
swprintf_s(buf, L"Transcribing %d:%02d \u2022 %d%% \u2022 %ds left", mm, ss, pct, rem);
SetStatus(hwnd, buf);
} else if (g_modelLoaded.load()) {
if (!g_modelOk.load()) {
SetStatus(hwnd, L"Model not found \u2014 check models folder");
return;
}
swprintf_s(buf, L"Ready \u2022 %d threads", g_tx.threads());
SetStatus(hwnd, buf);
} else {
SetStatus(hwnd, L"Loading model\u2026");
}
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
@@ -1074,6 +1342,13 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
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) {
@@ -1089,10 +1364,16 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
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);
@@ -1101,6 +1382,16 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
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;
@@ -1113,6 +1404,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
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;
}
@@ -1120,6 +1412,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
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:
@@ -1169,10 +1462,9 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
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, &g_vuRect, FALSE);
InvalidateRect(hWnd, nullptr, FALSE);
}
}
UpdateStatus(hWnd);
}
break;
@@ -1188,6 +1480,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
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)
@@ -1198,6 +1491,11 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
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 {
@@ -1236,17 +1534,21 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
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);
int len = GetWindowTextLengthW(hEdit);
std::wstring cur;
if (len > 0) { cur.resize(len + 1); int g = GetWindowTextW(hEdit, &cur[0], len + 1); cur.resize(g); }
std::wstring combined = append_transcript(cur, to_w(*res));
SetWindowTextW(hEdit, combined.c_str());
SendMessageW(hEdit, EM_SETSEL, (WPARAM)combined.size(), (LPARAM)combined.size());
std::wstring full = GetEditText(hWnd);
DWORD s = std::min<DWORD>(g_insStart, (DWORD)full.size());
DWORD e = std::min<DWORD>(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);
@@ -1276,6 +1578,26 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
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) {
@@ -1296,11 +1618,26 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
}).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);
@@ -1327,10 +1664,18 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
case ID_BTN_RECORD:
PostMessage(hWnd, WM_HOTKEY, HK_TOGGLE, 0);
break;
case ID_BTN_CLEAR:
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,
@@ -1362,10 +1707,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
}
break;
case ID_SEL_AUDIO:
ShowSelectPopup(hWnd, ID_SEL_AUDIO, g_audioItems, g_audioSel);
break;
case ID_SEL_MODEL:
ShowSelectPopup(hWnd, ID_SEL_MODEL, g_modelItems, g_modelSel);
ShowSelectPopup(hWnd, ID_SEL_AUDIO, g_audioItems, g_audioSel, g_w[(int)WK::SelAudio].r);
break;
}
break;
@@ -1384,10 +1726,13 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
ShowWindow(hWnd, SW_HIDE);
return 0;
case WM_DESTROY:
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);
@@ -1424,6 +1769,37 @@ std::wstring to_w(const std::string& s) {
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<std::wstring> BuildStatsLines() {
std::vector<std::wstring> 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;