Feature chore bug 007 #21

Merged
iamdoubz merged 68 commits from feature_chore_bug_007 into main 2026-07-12 12:59:04 -05:00
Showing only changes of commit c98c14bbc7 - Show all commits
+82 -68
View File
@@ -752,14 +752,18 @@
</section> </section>
{:else if section === "hardware"} {:else if section === "hardware"}
<section> <section>
<h3>Hardware</h3> <h3>{t("settings.hardware.title")}</h3>
{#if settings.hardware} {#if settings.hardware}
<div class="row"> <div class="row">
Active backend<code>{settings.hardware.active}</code> {t("settings.hardware.active_backend")}<code>{settings.hardware.active}</code>
<span class="muted">· model {settings.hardware.modelSize}</span> <span class="muted"
>{t("settings.hardware.model_meta", {
size: settings.hardware.modelSize,
})}</span
>
</div> </div>
<label <label
>Preferred backend >{t("settings.hardware.preferred_backend")}
<select <select
value={settings.settings.preferred_backend} value={settings.settings.preferred_backend}
onchange={(e) => onchange={(e) =>
@@ -767,40 +771,34 @@
(e.target as HTMLSelectElement).value as BackendId | "auto", (e.target as HTMLSelectElement).value as BackendId | "auto",
)} )}
> >
<option value="auto">Auto (best available)</option> <option value="auto">{t("settings.hardware.auto_backend")}</option>
{#each settings.hardware.backends as b (b.id)} {#each settings.hardware.backends as b (b.id)}
<option value={b.id} disabled={!b.available}> <option value={b.id} disabled={!b.available}>
{b.name} ({b.id}){b.available ? "" : " — not available"} {b.name} ({b.id}){b.available ? "" : ` — ${t("settings.hardware.not_available")}`}
</option> </option>
{/each} {/each}
</select> </select>
</label> </label>
<p class="muted"> <p class="muted">{t("settings.hardware.fallback_hint")}</p>
Falls back automatically (NPU → NVIDIA → AMD → Intel → CPU) if the chosen backend fails
to load.
</p>
<h4>Audio Devices</h4> <h4>{t("settings.hardware.audio_devices")}</h4>
<label <label
>Recording device >{t("settings.hardware.recording_device")}
<select <select
value={settings.settings.audio_output_device ?? ""} value={settings.settings.audio_output_device ?? ""}
onchange={(e) => onchange={(e) =>
settings.setAudioOutputDevice((e.target as HTMLSelectElement).value || null)} settings.setAudioOutputDevice((e.target as HTMLSelectElement).value || null)}
> >
<option value="">Default system audio</option> <option value="">{t("settings.hardware.default_system_audio")}</option>
{#each settings.audioDevices as d (d.id)} {#each settings.audioDevices as d (d.id)}
<option value={d.id}>{d.name}</option> <option value={d.id}>{d.name}</option>
{/each} {/each}
</select> </select>
</label> </label>
<p class="muted"> <p class="muted">{t("settings.hardware.recording_device_hint")}</p>
WhispAssist records whatever this device plays (loopback) — the other side of the call.
Pick a specific output if you don't want it following Windows' system default.
</p>
<label <label
>Microphone >{t("settings.hardware.microphone")}
<select <select
value={!settings.settings.microphone_enabled value={!settings.settings.microphone_enabled
? "off" ? "off"
@@ -811,18 +809,14 @@
else settings.setMicrophone(true, v || null); else settings.setMicrophone(true, v || null);
}} }}
> >
<option value="off">Off — don't capture my microphone</option> <option value="off">{t("settings.hardware.mic_off")}</option>
<option value="">Default microphone</option> <option value="">{t("settings.hardware.default_mic")}</option>
{#each settings.inputDevices as d (d.id)} {#each settings.inputDevices as d (d.id)}
<option value={d.id}>{d.name}</option> <option value={d.id}>{d.name}</option>
{/each} {/each}
</select> </select>
</label> </label>
<p class="muted"> <p class="muted">{t("settings.hardware.mic_hint")}</p>
Adds your own voice to the live transcript so both sides of the meeting are captured.
Stays on your device — nothing is uploaded. Choose “Off” to transcribe only the system
audio above.
</p>
{#if settings.hardware.npu?.present} {#if settings.hardware.npu?.present}
{@const npu = settings.hardware.npu} {@const npu = settings.hardware.npu}
@@ -830,24 +824,27 @@
<div class="npu-package"> <div class="npu-package">
<div class="row"> <div class="row">
<Cpu size={14} aria-hidden="true" /> <Cpu size={14} aria-hidden="true" />
<strong>NPU accelerator detected</strong> <strong>{t("settings.hardware.npu_detected")}</strong>
{#if ready} {#if ready}
<span class="badge ok"><Check size={12} aria-hidden="true" /> Ready</span> <span class="badge ok"
><Check size={12} aria-hidden="true" /> {t("settings.hardware.ready")}</span
>
{:else if npuBusy} {:else if npuBusy}
<span class="badge busy">Downloading…</span> <span class="badge busy">{t("settings.hardware.downloading")}</span>
{:else} {:else}
<span class="badge">Package needed</span> <span class="badge">{t("settings.hardware.package_needed")}</span>
{/if} {/if}
</div> </div>
{#if !ready} {#if !ready}
<p class="muted"> <p class="muted">
Your NPU needs a one-time acceleration package (Whisper ONNX model{npu.runtimeReady {t("settings.hardware.npu_hint", {
? "" extra: npu.runtimeReady ? "" : t("settings.hardware.plus_openvino"),
: " + OpenVINO runtime"}). It downloads in the background; you can also start it })}
now.
</p> </p>
<button onclick={downloadNpuPackage} disabled={npuBusy}> <button onclick={downloadNpuPackage} disabled={npuBusy}>
{npuBusy ? "Downloading…" : "Download NPU package"} {npuBusy
? t("settings.hardware.downloading")
: t("settings.hardware.download_npu")}
</button> </button>
{/if} {/if}
{#if npuMsg}<p class="muted npu-status">{npuMsg}</p>{/if} {#if npuMsg}<p class="muted npu-status">{npuMsg}</p>{/if}
@@ -861,29 +858,34 @@
<div class="npu-package"> <div class="npu-package">
<div class="row"> <div class="row">
<Cpu size={14} aria-hidden="true" /> <Cpu size={14} aria-hidden="true" />
<strong>GPU acceleration (DirectML)</strong> <strong>{t("settings.hardware.dml_title")}</strong>
{#if dmlReady} {#if dmlReady}
<span class="badge ok"><Check size={12} aria-hidden="true" /> Ready</span> <span class="badge ok"
><Check size={12} aria-hidden="true" /> {t("settings.hardware.ready")}</span
>
{:else if dmlBusy} {:else if dmlBusy}
<span class="badge busy">Downloading…</span> <span class="badge busy">{t("settings.hardware.downloading")}</span>
{:else} {:else}
<span class="badge">Package needed</span> <span class="badge">{t("settings.hardware.package_needed")}</span>
{/if} {/if}
</div> </div>
{#if !dmlReady} {#if !dmlReady}
<p class="muted"> <p class="muted">
Your GPU can be accelerated without Vulkan via DirectML — a one-time package {t("settings.hardware.dml_hint", {
(Whisper ONNX model{dml.runtimeReady ? "" : " + DirectML runtime"}). extra: dml.runtimeReady ? "" : t("settings.hardware.plus_directml"),
})}
</p> </p>
<button onclick={downloadDirectmlPackage} disabled={dmlBusy}> <button onclick={downloadDirectmlPackage} disabled={dmlBusy}>
{dmlBusy ? "Downloading…" : "Download DirectML package"} {dmlBusy
? t("settings.hardware.downloading")
: t("settings.hardware.download_dml")}
</button> </button>
{/if} {/if}
{#if dmlMsg}<p class="muted npu-status">{dmlMsg}</p>{/if} {#if dmlMsg}<p class="muted npu-status">{dmlMsg}</p>{/if}
</div> </div>
{/if} {/if}
{:else} {:else}
<p class="muted">Hardware detection unavailable.</p> <p class="muted">{t("settings.hardware.detection_unavailable")}</p>
{/if} {/if}
<label class="row"> <label class="row">
@@ -893,11 +895,12 @@
onchange={(e) => settings.setLowOverhead((e.target as HTMLInputElement).checked)} onchange={(e) => settings.setLowOverhead((e.target as HTMLInputElement).checked)}
/> />
<span <span
>Low overhead preset <em>(CPU + smallest model, for battery/background use)</em></span >{t("settings.hardware.low_overhead")}
<em>({t("settings.hardware.low_overhead_note")})</em></span
> >
</label> </label>
<h4>Models</h4> <h4>{t("settings.hardware.models")}</h4>
<ul class="models"> <ul class="models">
{#each settings.models as m (m.id)} {#each settings.models as m (m.id)}
{@const downloading = m.id in settings.downloadProgress} {@const downloading = m.id in settings.downloadProgress}
@@ -908,16 +911,19 @@
<span class="meta"> <span class="meta">
{fmtBytes(m.size_mb)} {fmtBytes(m.size_mb)}
{#if m.active} {#if m.active}
<span class="badge ok"><Check size={11} aria-hidden="true" /> Active</span> <span class="badge ok"
><Check size={11} aria-hidden="true" />
{t("settings.hardware.active_badge")}</span
>
{:else if m.installed} {:else if m.installed}
<span class="badge">Installed</span> <span class="badge">{t("settings.hardware.installed")}</span>
{/if} {/if}
</span> </span>
{#if downloading} {#if downloading}
<div <div
class="progress" class="progress"
role="progressbar" role="progressbar"
aria-label="Downloading {m.label}" aria-label={t("settings.hardware.downloading_model", { name: m.label })}
aria-valuenow={pct ?? undefined} aria-valuenow={pct ?? undefined}
aria-valuemin={0} aria-valuemin={0}
aria-valuemax={100} aria-valuemax={100}
@@ -929,7 +935,7 @@
></div> ></div>
</div> </div>
<span class="muted progress-label" <span class="muted progress-label"
>Downloading… {pct !== null ? `${pct}%` : ""}</span >{t("settings.hardware.downloading")} {pct !== null ? `${pct}%` : ""}</span
> >
{/if} {/if}
</div> </div>
@@ -942,19 +948,24 @@
disabled={m.active} disabled={m.active}
onclick={() => settings.setActiveModel(m.id)} onclick={() => settings.setActiveModel(m.id)}
> >
<Check size={13} aria-hidden="true" /> Use <Check size={13} aria-hidden="true" />
{t("settings.hardware.use")}
</button> </button>
<button <button
class="icon-btn danger" class="icon-btn danger"
disabled={m.active} disabled={m.active}
title={m.active ? "Can't remove the active model" : "Remove"} title={m.active
? t("settings.hardware.cant_remove_active")
: t("settings.hardware.remove")}
onclick={() => settings.removeModel(m.id)} onclick={() => settings.removeModel(m.id)}
> >
<Trash2 size={13} aria-hidden="true" /> Remove <Trash2 size={13} aria-hidden="true" />
{t("settings.hardware.remove")}
</button> </button>
{:else} {:else}
<button class="icon-btn" onclick={() => settings.downloadModel(m.id)}> <button class="icon-btn" onclick={() => settings.downloadModel(m.id)}>
<Download size={13} aria-hidden="true" /> Download <Download size={13} aria-hidden="true" />
{t("settings.hardware.download")}
</button> </button>
{/if} {/if}
</div> </div>
@@ -962,12 +973,8 @@
{/each} {/each}
</ul> </ul>
<h4>Speaker diarization</h4> <h4>{t("settings.hardware.diarization")}</h4>
<p class="muted"> <p class="muted">{t("settings.hardware.diarization_hint")}</p>
Install both models to have finished recordings separated by speaker (Speaker 1, Speaker
2, …) instead of one running transcript. Runs fully offline, once per meeting after it
stops. Without them, every line is attributed to a single speaker.
</p>
<ul class="models"> <ul class="models">
{#each settings.diarizationModels as m (m.id)} {#each settings.diarizationModels as m (m.id)}
{@const downloading = m.id in settings.downloadProgress} {@const downloading = m.id in settings.downloadProgress}
@@ -978,14 +985,17 @@
<span class="meta"> <span class="meta">
{fmtBytes(m.size_mb)} {fmtBytes(m.size_mb)}
{#if m.installed} {#if m.installed}
<span class="badge ok"><Check size={11} aria-hidden="true" /> Installed</span> <span class="badge ok"
><Check size={11} aria-hidden="true" />
{t("settings.hardware.installed")}</span
>
{/if} {/if}
</span> </span>
{#if downloading} {#if downloading}
<div <div
class="progress" class="progress"
role="progressbar" role="progressbar"
aria-label="Downloading {m.label}" aria-label={t("settings.hardware.downloading_model", { name: m.label })}
aria-valuenow={pct ?? undefined} aria-valuenow={pct ?? undefined}
aria-valuemin={0} aria-valuemin={0}
aria-valuemax={100} aria-valuemax={100}
@@ -997,7 +1007,7 @@
></div> ></div>
</div> </div>
<span class="muted progress-label" <span class="muted progress-label"
>Downloading… {pct !== null ? `${pct}%` : ""}</span >{t("settings.hardware.downloading")} {pct !== null ? `${pct}%` : ""}</span
> >
{/if} {/if}
</div> </div>
@@ -1007,14 +1017,16 @@
{:else if m.installed} {:else if m.installed}
<button <button
class="icon-btn danger" class="icon-btn danger"
title="Remove" title={t("settings.hardware.remove")}
onclick={() => settings.removeDiarizationModel(m.id)} onclick={() => settings.removeDiarizationModel(m.id)}
> >
<Trash2 size={13} aria-hidden="true" /> Remove <Trash2 size={13} aria-hidden="true" />
{t("settings.hardware.remove")}
</button> </button>
{:else} {:else}
<button class="icon-btn" onclick={() => settings.downloadDiarizationModel(m.id)}> <button class="icon-btn" onclick={() => settings.downloadDiarizationModel(m.id)}>
<Download size={13} aria-hidden="true" /> Download <Download size={13} aria-hidden="true" />
{t("settings.hardware.download")}
</button> </button>
{/if} {/if}
</div> </div>
@@ -1042,14 +1054,16 @@
<p class="muted">{t("settings.transcription.applies_hint")}</p> <p class="muted">{t("settings.transcription.applies_hint")}</p>
{:else} {:else}
<label <label
>Language >{t("settings.transcription.label")}
<select disabled aria-describedby="language-disabled-hint"> <select disabled aria-describedby="language-disabled-hint">
<option>English only</option> <option>{t("settings.transcription.english_only")}</option>
</select> </select>
</label> </label>
<p class="muted" id="language-disabled-hint"> <p class="muted" id="language-disabled-hint">
{activeModel ? `"${activeModel.label}" is English-only.` : "No model selected."} Switch to {activeModel
a multilingual model above to choose a language. ? t("settings.transcription.model_english_only", { model: activeModel.label })
: t("settings.transcription.no_model")}
{t("settings.transcription.switch_multilingual")}
</p> </p>
{/if} {/if}
</section> </section>