Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21f3d76ed0 | ||
|
|
19bd5815c0 | ||
|
|
a552ddb1fa | ||
|
|
de25ac70e7 | ||
|
|
28d9b2988a | ||
|
|
f24adc3a59 | ||
|
|
94425d456c | ||
|
|
49cdbd6a54 | ||
|
|
b4d72622da | ||
|
|
c1ccdce497 | ||
|
|
a319dd9e97 | ||
|
|
6a01afbba6 | ||
|
|
8c6a0ea25c | ||
|
|
4a000f07a2 | ||
|
|
0e2a3bb110 | ||
|
|
95b3d34713 | ||
|
|
588354f488 | ||
|
|
ee08f2bc88 | ||
|
|
a71c17965f | ||
|
|
3c73ec1cd4 | ||
|
|
8f805b8ef5 | ||
|
|
f83234bde1 | ||
|
|
d4b1578478 | ||
|
|
185e4ec803 | ||
|
|
30f4a4d3ec | ||
|
|
86c3587d19 | ||
|
|
d736b78ecb | ||
|
|
98f6fcdc16 | ||
|
|
683838bef1 | ||
|
|
c63cabaf88 | ||
|
|
fe355f034d | ||
|
|
e0e9377273 | ||
|
|
a95eafe952 | ||
|
|
5b9e0ed1f7 | ||
|
|
6c343c4204 | ||
|
|
7d16c864e8 | ||
|
|
e461d4eb22 | ||
|
|
b14728e1c8 | ||
|
|
b7e6f54d91 | ||
|
|
cf919f1499 | ||
|
|
c616e0bfec | ||
|
|
ecbe3e464c | ||
|
|
7f6c0fac81 | ||
|
|
6fa9916f7e | ||
|
|
d78a426a7e | ||
|
|
1b36df020a | ||
|
|
4af008da0f | ||
|
|
223dc5f34b | ||
|
|
27773a03e5 | ||
|
|
203e7b1bf6 | ||
|
|
a6e1728346 | ||
|
|
26808970df | ||
|
|
4a63b38ee0 | ||
|
|
5d6563231c | ||
|
|
2edd0382d3 | ||
|
|
1530bca976 | ||
|
|
55c5143de0 | ||
|
|
47071b5861 | ||
|
|
ded5e00750 | ||
|
|
2fa3ea6824 | ||
|
|
14300fccee | ||
|
|
2c8958da01 | ||
|
|
f2c945838c | ||
|
|
0e0f3a09f1 | ||
|
|
3d183fdd7c | ||
|
|
32bb4057ec | ||
|
|
90a9cbd476 | ||
|
|
5bd76b80e8 | ||
|
|
a1d71d1c73 | ||
|
|
e287fa5ebe | ||
|
|
534f5ca350 | ||
|
|
1e99c4c747 | ||
|
|
5e5f61f3b7 | ||
|
|
dec9251e7b | ||
|
|
d4e2ab494c | ||
|
|
845903a772 | ||
|
|
998de62df2 | ||
|
|
e449c194ef | ||
|
|
6cea767dec | ||
|
|
60ae97c771 | ||
|
|
d8587b8e93 | ||
|
|
695a12dc2d | ||
|
|
6385e38b33 | ||
|
|
0b5a85f461 | ||
|
|
fd9311c482 | ||
|
|
39294adcbc | ||
|
|
fee18ebc59 | ||
|
|
51f5ff085c | ||
|
|
6d4204fb22 | ||
|
|
185489fa18 | ||
|
|
41c2c79797 | ||
|
|
6dd6c3c69f | ||
|
|
9468781218 | ||
|
|
b39352dbff | ||
|
|
0d263637df | ||
|
|
bb6e222101 | ||
|
|
85ed3b5587 | ||
|
|
68d692fbe8 | ||
|
|
fa664e5147 | ||
|
|
b5c99621b5 | ||
|
|
db52b9e812 | ||
|
|
319e7a31cc | ||
|
|
25011d0f27 | ||
|
|
7733e86cfd | ||
|
|
a481f5dcbf | ||
|
|
bd6519b65e | ||
|
|
4f3af98bfa | ||
|
|
77f9a8b518 | ||
|
|
2a7e6229f4 | ||
|
|
64e7baad66 | ||
|
|
804d451bfa | ||
|
|
a29e60b47d | ||
|
|
969ed6022e | ||
|
|
226593b49d | ||
|
|
dc1d352e09 | ||
|
|
05e003ab19 | ||
|
|
d636eec848 | ||
|
|
1aeaae3550 | ||
|
|
84774c75cb | ||
|
|
bca790571c | ||
|
|
bfe7005368 | ||
|
|
e01d61ae0c | ||
|
|
8947eb2f39 | ||
|
|
d2ef12269b | ||
|
|
bab1eb57b1 | ||
|
|
76bb38cbe6 | ||
|
|
7570d810e4 | ||
|
|
a090cc4acd | ||
|
|
7f0a718210 | ||
|
|
96e8723cb9 | ||
|
|
fce3a6b96c | ||
|
|
0f8288f90f | ||
|
|
8ffc015e29 | ||
|
|
b8c58d6ab8 | ||
|
|
51a03b0ec4 | ||
|
|
96851157dd | ||
|
|
f05cec879f | ||
|
|
bf67be8764 | ||
|
|
c1de97938e | ||
|
|
5f300ee177 | ||
|
|
52d3fcbf3b | ||
|
|
2181aa5b0e | ||
|
|
4e87cab3ad | ||
|
|
84ab88b565 | ||
|
|
ae13944563 | ||
|
|
8fc5d72576 | ||
|
|
681933c8cf | ||
|
|
cfe73e7d80 | ||
|
|
505387ba49 | ||
|
|
fa8ce62f63 | ||
|
|
7527da92c5 | ||
|
|
ef933c3786 | ||
|
|
f6f336d79f | ||
|
|
1f25833e74 | ||
|
|
0d8bcf3b17 | ||
|
|
27a1f98051 | ||
|
|
0ecd6db12e | ||
|
|
d2f35c64b1 | ||
|
|
5967fd6997 | ||
|
|
a0ad1564a8 | ||
|
|
377a91d1b8 | ||
|
|
7796c5b62d | ||
|
|
698fb26b0a | ||
|
|
802e30e9c6 | ||
|
|
f1683af01c | ||
|
|
12388b8bd9 | ||
|
|
b295e1ee83 | ||
|
|
c98c14bbc7 | ||
|
|
c0b7587298 | ||
|
|
d5f0d028ac | ||
|
|
2f925de9a5 | ||
|
|
486767587e | ||
|
|
88969019b3 | ||
|
|
2b13de4ef2 | ||
|
|
a112b81d6e | ||
|
|
1da127a61c | ||
|
|
57d3aca386 | ||
|
|
aac88bfbc5 | ||
|
|
e5fdb2baea | ||
|
|
fc89f76ac8 | ||
|
|
7b8a651730 | ||
|
|
e1e2808d9c | ||
|
|
77234e697d | ||
|
|
f5c521df0d | ||
|
|
3f04ec6167 | ||
|
|
030b06329a | ||
|
|
a7af623753 | ||
|
|
b98c258c13 | ||
|
|
8ca6cf4f89 | ||
|
|
dbe845e923 | ||
|
|
d5aef2966f | ||
|
|
2389fb05a5 | ||
|
|
96d02cc220 | ||
|
|
c3da6cf0f3 | ||
|
|
5564c9c9a6 | ||
|
|
d25c297257 | ||
|
|
23cfca7ea2 | ||
|
|
6129866ac5 | ||
|
|
2e57ccfab5 | ||
|
|
4dff7a58b5 | ||
|
|
25204c0235 | ||
|
|
2b2e120f12 | ||
|
|
70eb182eb4 | ||
|
|
836547596b | ||
|
|
58215076cb | ||
|
|
862ab86860 | ||
|
|
a0faaa94b4 | ||
|
|
9c74f84c5b |
@@ -21,6 +21,13 @@ Every memory operation in this session goes through MEMANTO. There is no excepti
|
|||||||
|
|
||||||
These are not suggestions. Follow each one on every turn.
|
These are not suggestions. Follow each one on every turn.
|
||||||
|
|
||||||
|
0. **Activate the `whispassist` agent at the start of every session, before any memory op.** Run
|
||||||
|
`memanto agent activate whispassist` first thing. This machine hosts multiple projects and the
|
||||||
|
session-start sync may activate a *different* project's agent (e.g. `whispassist`), so the
|
||||||
|
auto-synced `MEMORY.md` can belong to the wrong project — do not trust it as LastERP context
|
||||||
|
until you've activated `whispassist` and re-synced. Confirm with `memanto agent list` (the
|
||||||
|
active one is marked). All `recall`/`remember`/`answer` calls read and write the *active*
|
||||||
|
agent's store, so getting this wrong silently pollutes or mis-reads another project's memory.
|
||||||
1. **Read `MEMORY.md` before doing anything.** It is auto-synced at session start and holds
|
1. **Read `MEMORY.md` before doing anything.** It is auto-synced at session start and holds
|
||||||
the user's preferences, facts, goals, instructions, decisions, and commitments from every
|
the user's preferences, facts, goals, instructions, decisions, and commitments from every
|
||||||
prior session. You MUST honor what is written there. If you act against it, you are
|
prior session. You MUST honor what is written there. If you act against it, you are
|
||||||
@@ -179,6 +186,56 @@ A task is done when: code compiles with no clippy warnings; the relevant require
|
|||||||
acceptance criteria are demonstrably met; tests named in `docs/06-test-strategy.md` for that
|
acceptance criteria are demonstrably met; tests named in `docs/06-test-strategy.md` for that
|
||||||
phase pass; and docs are updated if behavior or contracts changed.
|
phase pass; and docs are updated if behavior or contracts changed.
|
||||||
|
|
||||||
|
## Build & release (the unified installer)
|
||||||
|
|
||||||
|
Release builds produce **one universal installer** (as **two bundles**: MSI + NSIS) that
|
||||||
|
covers every backend from a single command. The Vulkan whisper DLLs are compiled in and
|
||||||
|
`vulkan-1.dll` is bundled as a resource; the NPU (OpenVINO) and DirectML runtimes are
|
||||||
|
**downloaded on demand** at first use, so they don't need to be built or bundled. There is no
|
||||||
|
separate "DirectML build" to merge — the one Vulkan-feature build *is* the universal installer.
|
||||||
|
|
||||||
|
**Version bump (4 files, keep them in lockstep):** `package.json`, `src-tauri/Cargo.toml`,
|
||||||
|
`src-tauri/tauri.conf.json`, and the `whispassist` entry in `src-tauri/Cargo.lock`. Do **not**
|
||||||
|
bump `package-lock.json` — its app version is intentionally `0.0.0` (its only version-looking
|
||||||
|
`0.x` string is an unrelated dependency).
|
||||||
|
|
||||||
|
**Build command** — run from the repo root with the MSVC environment loaded, targeting the
|
||||||
|
dedicated Vulkan build dir so the compile stays incremental (a clean build is much slower):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
call "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
|
||||||
|
set CARGO_TARGET_DIR=C:\wt
|
||||||
|
npm run tauri -- build --features vulkan --config src-tauri/tauri.vulkan.conf.json
|
||||||
|
```
|
||||||
|
|
||||||
|
- `CARGO_TARGET_DIR=C:\wt` is a **persistent Vulkan-build cache**, kept separate from
|
||||||
|
`src-tauri/target` so toggling the `vulkan` feature doesn't force a full rebuild. Reuse it;
|
||||||
|
don't `cargo clean` unless you actually need a from-scratch build.
|
||||||
|
- `tauri.vulkan.conf.json` only adds `vulkan-1.dll` to the bundle resources (that DLL lives at
|
||||||
|
`src-tauri/vulkan-1.dll`); everything else comes from the base `tauri.conf.json`.
|
||||||
|
- `vcvars64.bat` is required for the native deps (see the build-toolchain memory).
|
||||||
|
|
||||||
|
**Output** (under the `C:\wt` target dir), two bundles:
|
||||||
|
|
||||||
|
```
|
||||||
|
C:\wt\release\bundle\msi\WhispAssist_<ver>_x64_en-US.msi (~44 MB)
|
||||||
|
C:\wt\release\bundle\nsis\WhispAssist_<ver>_x64-setup.exe (~9 MB)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Checksums:** generate and verify SHA-256 sums for both bundles, and embed them in
|
||||||
|
`RELEASE_NOTES_<ver>.md` (see the 0.4.0/0.5.0 notes for the format):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd C:\wt\release\bundle
|
||||||
|
sha256sum msi/WhispAssist_<ver>_x64_en-US.msi nsis/WhispAssist_<ver>_x64-setup.exe | sed 's| \*| |' > SHA256SUMS_<ver>.txt
|
||||||
|
sha256sum -c SHA256SUMS_<ver>.txt # must print OK for both
|
||||||
|
```
|
||||||
|
|
||||||
|
The bundles and `SHA256SUMS` live in the `C:\wt` build dir, **outside the repo** — only
|
||||||
|
`RELEASE_NOTES_<ver>.md` is committed. Bundles are currently **unsigned** (no signing cert in
|
||||||
|
`tauri.conf.json`); the release notes carry the SmartScreen "More info → Run anyway" guidance.
|
||||||
|
Tagging/pushing a release is outward-facing — do it only when explicitly asked.
|
||||||
|
|
||||||
## Guardrails
|
## Guardrails
|
||||||
|
|
||||||
- Do not add telemetry, analytics, crash reporting, or auto-update phone-home without an ADR
|
- Do not add telemetry, analytics, crash reporting, or auto-update phone-home without an ADR
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="bonus/clippy-says-hej.png" alt="WhispAssist logo with friendly Clippy saying hi there showing open source, private, note taker, and meeting advisor">
|
||||||
|
</p>
|
||||||
|
|
||||||
# WhispAssist (WA)
|
# WhispAssist (WA)
|
||||||
|
|
||||||
**A privacy-first, Windows-native meeting assistant that runs entirely on-device.**
|
**A privacy-first, Windows-native meeting assistant that runs entirely on-device.**
|
||||||
@@ -7,7 +11,7 @@ on-device acceleration (**NPU → GPU → CPU**), labels speakers, structures th
|
|||||||
Markdown notes, and optionally augments them with a locally hosted LLM (Ollama). Audio and
|
Markdown notes, and optionally augments them with a locally hosted LLM (Ollama). Audio and
|
||||||
transcripts **never leave the machine** unless you explicitly configure a destination.
|
transcripts **never leave the machine** unless you explicitly configure a destination.
|
||||||
|
|
||||||
> **Status: working application (v0.4.0).** Capture (system audio **+ your microphone**, with a
|
> **Status: working application (v0.5.2).** Capture (system audio **+ your microphone**, with a
|
||||||
> live **dual level meter**), transcription (CPU / Intel NPU / Vulkan GPU) with a **fluid live
|
> live **dual level meter**), transcription (CPU / Intel NPU / Vulkan GPU) with a **fluid live
|
||||||
> transcript**, speaker diarization, storage + crash recovery, local-LLM summaries, AI tags,
|
> transcript**, speaker diarization, storage + crash recovery, local-LLM summaries, AI tags,
|
||||||
> opt-in recording with **in-app playback**, at-rest encryption, self-hosted sync, **Outlook
|
> opt-in recording with **in-app playback**, at-rest encryption, self-hosted sync, **Outlook
|
||||||
@@ -27,6 +31,7 @@ transcripts **never leave the machine** unless you explicitly configure a destin
|
|||||||
| **NPU / GPU auto-acceleration** | n/a (cloud) | ⚠️ CPU/GPU, manual | ✅ **NPU→NVIDIA→AMD→Intel→CPU** ladder, one binary |
|
| **NPU / GPU auto-acceleration** | n/a (cloud) | ⚠️ CPU/GPU, manual | ✅ **NPU→NVIDIA→AMD→Intel→CPU** ladder, one binary |
|
||||||
| **Speaker diarization** | ✅ cloud | ⚠️ limited | ✅ offline (sherpa-onnx) |
|
| **Speaker diarization** | ✅ cloud | ⚠️ limited | ✅ offline (sherpa-onnx) |
|
||||||
| **Summaries / notes AI** | ☁️ cloud LLM | ✅ local (Ollama) / BYO | ✅ local (Ollama, localhost **or LAN**) + optional hosted |
|
| **Summaries / notes AI** | ☁️ cloud LLM | ✅ local (Ollama) / BYO | ✅ local (Ollama, localhost **or LAN**) + optional hosted |
|
||||||
|
| **Interface localization (UI language)** | ⚠️ English-only UI (multilingual transcription) | ⚠️ English-only UI (more planned) | ✅ **20 languages** + RTL (Arabic/Urdu) |
|
||||||
| **Default data egress** | ☁️ audio + notes to cloud | 🔒 local (cloud optional) | 🔒 **none** — everything off by default, allowlist-enforced |
|
| **Default data egress** | ☁️ audio + notes to cloud | 🔒 local (cloud optional) | 🔒 **none** — everything off by default, allowlist-enforced |
|
||||||
| **Calendar / Outlook `.pst` context** | ✅ cloud calendar | ❌ | ✅ local `.pst` import + **auto-record on events** |
|
| **Calendar / Outlook `.pst` context** | ✅ cloud calendar | ❌ | ✅ local `.pst` import + **auto-record on events** |
|
||||||
| **Import an existing recording (file/URL)** | ❌ live capture only | ❌ | ✅ any file or URL (ffmpeg + yt-dlp) |
|
| **Import an existing recording (file/URL)** | ❌ live capture only | ❌ | ✅ any file or URL (ffmpeg + yt-dlp) |
|
||||||
@@ -51,7 +56,7 @@ accelerated, zero-egress-by-default** option: it exploits the NPU/GPU in modern
|
|||||||
everything on the device unless you opt in, and adds Windows-specific context (Outlook) and a
|
everything on the device unless you opt in, and adds Windows-specific context (Outlook) and a
|
||||||
coding-agent handoff.
|
coding-agent handoff.
|
||||||
|
|
||||||
## What's built (v0.4.0)
|
## What's built (v0.5.2)
|
||||||
|
|
||||||
- **Bot-free capture — now both sides, with a live dual meter** — WASAPI loopback records the
|
- **Bot-free capture — now both sides, with a live dual meter** — WASAPI loopback records the
|
||||||
system mix (all participants), and an optional **microphone** path captures your own voice,
|
system mix (all participants), and an optional **microphone** path captures your own voice,
|
||||||
@@ -103,6 +108,14 @@ coding-agent handoff.
|
|||||||
(Claude, Codex, Copilot, OpenCode) over a **loopback-only, token-gated** MCP server that is off
|
(Claude, Codex, Copilot, OpenCode) over a **loopback-only, token-gated** MCP server that is off
|
||||||
by default, scope-limited, audited, and **adds no egress** — data leaves only via the agent's
|
by default, scope-limited, audited, and **adds no egress** — data leaves only via the agent's
|
||||||
own provider, which WA discloses.
|
own provider, which WA discloses.
|
||||||
|
- **20 interface languages, with RTL** — the whole UI (shell, meetings list, transcript/notes,
|
||||||
|
summary, and Settings) is available in **20 languages**, chosen under **Settings ▸ Language**:
|
||||||
|
Arabic (العربية), Bengali (বাংলা), Simplified Chinese (中文简体), English, Finnish (Suomi),
|
||||||
|
French — France & Canada (Français), German (Deutsch), Hindi (हिन्दी), Korean (한국어),
|
||||||
|
Norwegian Bokmål (Norsk bokmål), Portuguese — Brazil & Portugal (Português), Russian (Русский),
|
||||||
|
Sinhala (සිංහල), Spanish — Spain & Mexico (Español), Swedish (Svenska), Tamil (தமிழ்), and
|
||||||
|
Urdu (اردو). Selecting Arabic or Urdu flips the interface to **right-to-left**; any untranslated
|
||||||
|
string falls back to English rather than showing a raw key. Adding a language is a one-file job.
|
||||||
- **One universal installer** — a single signed MSI and NSIS `-setup.exe` that covers every
|
- **One universal installer** — a single signed MSI and NSIS `-setup.exe` that covers every
|
||||||
machine: Vulkan for all GPUs, the Intel NPU path (+ DirectML fallback), and CPU. The Vulkan
|
machine: Vulkan for all GPUs, the Intel NPU path (+ DirectML fallback), and CPU. The Vulkan
|
||||||
loader is bundled so it launches even on machines without a GPU driver.
|
loader is bundled so it launches even on machines without a GPU driver.
|
||||||
@@ -125,10 +138,15 @@ during setup).
|
|||||||
On first run WA detects your hardware and picks the best transcription backend
|
On first run WA detects your hardware and picks the best transcription backend
|
||||||
(**NPU → NVIDIA → AMD → Intel → CPU**). It works on your **CPU or GPU** (GPU via Vulkan) out of the
|
(**NPU → NVIDIA → AMD → Intel → CPU**). It works on your **CPU or GPU** (GPU via Vulkan) out of the
|
||||||
box; to use an Intel **NPU**, open **Settings ▸ Hardware** and download the one-time NPU
|
box; to use an Intel **NPU**, open **Settings ▸ Hardware** and download the one-time NPU
|
||||||
acceleration package. The app runs without admin rights, never adds itself to startup, and keeps all
|
acceleration package. The app runs without admin rights, does not add itself to startup unless you
|
||||||
data under `%LOCALAPPDATA%\WhispAssist`.
|
opt in (**Settings ▸ Recording ▸ Launch at login**), and keeps all data under
|
||||||
|
`%LOCALAPPDATA%\WhispAssist`.
|
||||||
|
|
||||||
Prefer the NSIS installer? Grab **`WhispAssist_<version>_x64-setup.exe`** from the same page.
|
Prefer the NSIS installer? Grab **`WhispAssist_<version>_x64-setup.exe`** from the same page — it
|
||||||
|
lets you choose a **current-user** (no admin) or **all-users** install.
|
||||||
|
|
||||||
|
**Deploying to many machines?** See [`docs/enterprise-deployment.md`](docs/enterprise-deployment.md)
|
||||||
|
for silent install, custom install location, and presetting defaults with a `wa-defaults.ini` file.
|
||||||
|
|
||||||
## Optional dependencies
|
## Optional dependencies
|
||||||
|
|
||||||
@@ -142,7 +160,7 @@ If you do not have these installed, WhispAssist will still work, but some featur
|
|||||||
|
|
||||||
WhispAssist is a **Tauri 2** application: a small Rust core with a compiled **Svelte + TypeScript**
|
WhispAssist is a **Tauri 2** application: a small Rust core with a compiled **Svelte + TypeScript**
|
||||||
frontend rendered through the OS WebView2 (no bundled browser → low idle memory). Every decision
|
frontend rendered through the OS WebView2 (no bundled browser → low idle memory). Every decision
|
||||||
and its alternatives are recorded in [`docs/adr/`](docs/adr/) (ADR-0001–0011).
|
and its alternatives are recorded in [`docs/adr/`](docs/adr/) (ADR-0001–0012).
|
||||||
|
|
||||||
- **Shell / IPC:** Tauri 2 (Rust ⇄ WebView2)
|
- **Shell / IPC:** Tauri 2 (Rust ⇄ WebView2)
|
||||||
- **Audio capture:** WASAPI loopback
|
- **Audio capture:** WASAPI loopback
|
||||||
@@ -163,7 +181,7 @@ and its alternatives are recorded in [`docs/adr/`](docs/adr/) (ADR-0001–0011).
|
|||||||
WhispAssist/
|
WhispAssist/
|
||||||
├── docs/ # The engineering plan (read this first)
|
├── docs/ # The engineering plan (read this first)
|
||||||
│ ├── 00-overview.md … 07-research-findings.md
|
│ ├── 00-overview.md … 07-research-findings.md
|
||||||
│ └── adr/ Architecture Decision Records (0001–0011)
|
│ └── adr/ Architecture Decision Records (0001–0012)
|
||||||
├── src-tauri/ # Rust core — implemented service modules:
|
├── src-tauri/ # Rust core — implemented service modules:
|
||||||
│ └── src/{audio,transcription,diarization,storage,llm,calendar,
|
│ └── src/{audio,transcription,diarization,storage,llm,calendar,
|
||||||
│ hardware,notes,sync,mcp,vault}
|
│ hardware,notes,sync,mcp,vault}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# WhispAssist v0.7.0
|
||||||
|
|
||||||
|
**Privacy-first, Windows-native meeting assistant — everything on-device, nothing leaves unless you say so.**
|
||||||
|
|
||||||
|
This release is about **control over install and startup** — for a single user and for admins rolling
|
||||||
|
WhispAssist out across a fleet. You can now launch WhispAssist at login, choose a per-user (no-admin)
|
||||||
|
or all-users install, preset every default with a deployment file, and re-scan your hardware without
|
||||||
|
restarting. No feature here changes the privacy posture: everything optional stays off-by-default and
|
||||||
|
local-first.
|
||||||
|
|
||||||
|
One universal installer (**MSI** and **NSIS**) covers every machine: **Vulkan** for all GPUs
|
||||||
|
(NVIDIA/AMD/Intel), the Intel **NPU** (OpenVINO), a **DirectML** fallback, and **CPU**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ New
|
||||||
|
|
||||||
|
### Launch at login (opt-in)
|
||||||
|
A new **Settings ▸ Recording ▸ Launch WhispAssist at login** toggle starts WhispAssist when you sign
|
||||||
|
in to Windows. It's **off by default**, needs **no admin** (a per-user startup entry), and does **not**
|
||||||
|
begin recording on its own.
|
||||||
|
|
||||||
|
### Choose a per-user or all-users install
|
||||||
|
The NSIS setup (`WhispAssist_0.7.0_x64-setup.exe`) now lets you install for **just yourself**
|
||||||
|
(no admin rights required) or for **all users**. The MSI remains the per-machine, all-users installer.
|
||||||
|
|
||||||
|
### Customize deployments with a file (`wa-defaults.ini`)
|
||||||
|
Admins can preset WhispAssist's defaults for every machine using native Windows tooling (Group Policy,
|
||||||
|
SCCM, Intune, `msiexec`) — no management console. Drop a `wa-defaults.ini` next to the installer or in
|
||||||
|
`%PROGRAMDATA%\WhispAssist\`, and each machine's **first launch** seeds its settings from it: record-
|
||||||
|
by-default, preferred backend, a model to auto-download, retention, storage location, AI provider, and
|
||||||
|
more. You can also set a **custom install location** with standard `msiexec INSTALLDIR=…` / NSIS `/D=`
|
||||||
|
flags. **Secrets are never read from this file** — API keys and tokens stay in the OS credential store.
|
||||||
|
Full key reference and silent-install examples in
|
||||||
|
[`docs/enterprise-deployment.md`](docs/enterprise-deployment.md).
|
||||||
|
|
||||||
|
### Refresh hardware
|
||||||
|
A **Refresh** button in **Settings ▸ Hardware** re-detects your GPU/NPU on the spot — handy after
|
||||||
|
installing a driver or plugging in an eGPU — no restart needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Install
|
||||||
|
|
||||||
|
**Requirements:** Windows 10 or 11 (x64). WhispAssist needs **WebView2** (preinstalled on Windows 11;
|
||||||
|
the installer fetches it on Windows 10). Importing from a file/URL additionally needs **`ffmpeg`**
|
||||||
|
(and **`yt-dlp`** for URLs) on your PATH — install those separately.
|
||||||
|
|
||||||
|
1. Download **`WhispAssist_0.7.0_x64_en-US.msi`** (or the NSIS **`WhispAssist_0.7.0_x64-setup.exe`**).
|
||||||
|
2. Run it and accept the UAC prompt. If SmartScreen appears, choose **More info → Run anyway**.
|
||||||
|
3. Launch **WhispAssist** from the Start menu.
|
||||||
|
|
||||||
|
On first run WA picks the best transcription backend (**NPU → NVIDIA → AMD → Intel → CPU**). It runs
|
||||||
|
without admin rights, does **not** add itself to startup unless you opt in, and keeps all data under
|
||||||
|
`%LOCALAPPDATA%\WhispAssist`.
|
||||||
|
|
||||||
|
Deploying to many machines? See [`docs/enterprise-deployment.md`](docs/enterprise-deployment.md).
|
||||||
|
|
||||||
|
## 🔐 Checksums (SHA-256)
|
||||||
|
|
||||||
|
```
|
||||||
|
730676580dbef5c4bb2c155f46a73a5791722139ab144a3fc0286e3a4449c432 WhispAssist_0.7.0_x64_en-US.msi
|
||||||
|
6a34c16eb6f876fc92c7a79d37414b4fdd2c09f4f40873008149db36cd0a30aa WhispAssist_0.7.0_x64-setup.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify after download:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-FileHash .\WhispAssist_0.7.0_x64_en-US.msi -Algorithm SHA256
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Privacy, unchanged
|
||||||
|
Everything optional is **off by default**. With nothing configured, WhispAssist makes **no content
|
||||||
|
egress at all**. Recording is opt-in; sync/AI credentials live only in the OS credential store; the
|
||||||
|
MCP server is loopback-only and adds no egress; the deployment file never carries secrets. The
|
||||||
|
reachable-host allowlist is derived from your settings and enforced in the core.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# WhispAssist v0.7.1
|
||||||
|
|
||||||
|
**Privacy-first, Windows-native meeting assistant — everything on-device, nothing leaves unless you say so.**
|
||||||
|
|
||||||
|
A quality-of-life release focused on **importing meetings, taking notes, and living in the
|
||||||
|
background**. Adding a meeting from a file or link now runs without freezing the app and shows you
|
||||||
|
exactly where it's up to; notes gained AI cleanup and quick formatting; and WhispAssist can now mute
|
||||||
|
your mic mid-meeting and tuck itself into the system tray. No feature here changes the privacy
|
||||||
|
posture: everything optional stays off-by-default and local-first.
|
||||||
|
|
||||||
|
One universal installer (**MSI** and **NSIS**) covers every machine: **Vulkan** for all GPUs
|
||||||
|
(NVIDIA/AMD/Intel), the Intel **NPU** (OpenVINO), a **DirectML** fallback, and **CPU**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ New
|
||||||
|
|
||||||
|
### Add a meeting — now in the background, with a progress tracker
|
||||||
|
Importing a recording (a local audio/video file or a YouTube/streaming/direct URL) no longer blocks
|
||||||
|
the app while it works. Click **Import** and the meeting appears in your list immediately with a
|
||||||
|
**four-step progress tracker** — *Transcode → Transcribe → Identify speakers → Finalize* — where the
|
||||||
|
current step pulses and finished steps show how long they took. A 25-minute video that used to lock
|
||||||
|
the window for ~13 minutes now transcribes quietly in the background.
|
||||||
|
|
||||||
|
- **Pick the transcription model** right in the dialog, and see **"Transcribed with …"** on the
|
||||||
|
finished meeting so you always know how it was produced.
|
||||||
|
- **One-click links** to download `ffmpeg` and `yt-dlp` (still external, not bundled).
|
||||||
|
- A failed import stays in your list marked **error** instead of vanishing.
|
||||||
|
|
||||||
|
### Better notes
|
||||||
|
- **AI-enhance** (✨): turn rough notes into clean, structured notes using your local LLM, with a
|
||||||
|
one-step **Undo**. Off unless you have a local model configured.
|
||||||
|
- **Slash commands & a formatting toolbar**: type `/todo`, `/h1`, `/quote`, … or use the toolbar for
|
||||||
|
headings, lists, checkboxes, quotes, and dividers.
|
||||||
|
- **Fix:** notes no longer show stale text after re-transcribing a meeting — the pane updates in
|
||||||
|
place, no restart needed.
|
||||||
|
|
||||||
|
### Mute your microphone — press **M**
|
||||||
|
Mute/unmute the mic mid-meeting with the **M** key or the new mic button by the level meter. The mic
|
||||||
|
channel goes silent (recording, live transcript, and meter) while system/loopback audio keeps
|
||||||
|
capturing.
|
||||||
|
|
||||||
|
### Close to system tray
|
||||||
|
Closing the window now **keeps WhispAssist running in the background** instead of quitting. Reopen it
|
||||||
|
from the tray icon; the tray's **Quit** exits fully. On by default — toggle it in
|
||||||
|
**Settings ▸ Recording ▸ Close to system tray**. (This release also fixes a bug that showed **two**
|
||||||
|
WhispAssist icons in the tray — there's now just one.)
|
||||||
|
|
||||||
|
### Privacy & hardware odds and ends
|
||||||
|
- **Vault lock card** in **Settings ▸ Privacy**: lock/unlock the encrypted store and change its
|
||||||
|
password at a glance.
|
||||||
|
- **Test your audio devices**: a live level meter for your mic and system audio, plus a test tone.
|
||||||
|
- **Quick hardware stress test**: benchmark the available backends against your installed models and
|
||||||
|
apply the fastest real-time combination.
|
||||||
|
- Tidier recording header.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Install
|
||||||
|
|
||||||
|
**Requirements:** Windows 10 or 11 (x64). WhispAssist needs **WebView2** (preinstalled on Windows 11;
|
||||||
|
the installer fetches it on Windows 10). Importing from a file/URL additionally needs **`ffmpeg`**
|
||||||
|
(and **`yt-dlp`** for URLs) on your PATH — the Import dialog now links to both.
|
||||||
|
|
||||||
|
1. Download **`WhispAssist_0.7.1_x64_en-US.msi`** (or the NSIS **`WhispAssist_0.7.1_x64-setup.exe`**).
|
||||||
|
2. Run it and accept the UAC prompt. If SmartScreen appears, choose **More info → Run anyway**.
|
||||||
|
3. Launch **WhispAssist** from the Start menu.
|
||||||
|
|
||||||
|
On first run WA picks the best transcription backend (**NPU → NVIDIA → AMD → Intel → CPU**). It runs
|
||||||
|
without admin rights, does **not** add itself to startup unless you opt in, and keeps all data under
|
||||||
|
`%LOCALAPPDATA%\WhispAssist`.
|
||||||
|
|
||||||
|
Deploying to many machines? See [`docs/enterprise-deployment.md`](docs/enterprise-deployment.md).
|
||||||
|
|
||||||
|
## 🔐 Checksums (SHA-256)
|
||||||
|
|
||||||
|
```
|
||||||
|
bd96a059db3658a9bee81161edc709cfc4741ed99d6c66a12194403b339f9369 WhispAssist_0.7.1_x64_en-US.msi
|
||||||
|
711fa7df618c8fc3f03c18d543abad2288d56f750b532288204e0ec0f9426595 WhispAssist_0.7.1_x64-setup.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify after download:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-FileHash .\WhispAssist_0.7.1_x64_en-US.msi -Algorithm SHA256
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Privacy, unchanged
|
||||||
|
Everything optional is **off by default**. With nothing configured, WhispAssist makes **no content
|
||||||
|
egress at all**. Recording is opt-in; sync/AI credentials live only in the OS credential store; the
|
||||||
|
MCP server is loopback-only and adds no egress; the deployment file never carries secrets. The
|
||||||
|
reachable-host allowlist is derived from your settings and enforced in the core.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 728 KiB |
@@ -84,10 +84,14 @@ issues from feature briefs.
|
|||||||
4. Provisional speaker turns (from cheap segmentation) tag segments live; the user may name a
|
4. Provisional speaker turns (from cheap segmentation) tag segments live; the user may name a
|
||||||
speaker, applied immediately to past/future segments (FR-SPK-2).
|
speaker, applied immediately to past/future segments (FR-SPK-2).
|
||||||
5. The notes view renders Markdown that updates as segments arrive (FR-NOTE-1, FR-NOTE-6).
|
5. The notes view renders Markdown that updates as segments arrive (FR-NOTE-1, FR-NOTE-6).
|
||||||
6. On `stop_recording`: finalize the WAV; run full `diarization`, align speaker IDs to segments,
|
6. On `stop_recording`: finalize the WAV; run `diarization`, align speaker IDs to segments,
|
||||||
apply name mappings; persist transcript JSON + metadata via `storage` (FR-STORE-1). If "Record
|
apply name mappings; persist transcript JSON + metadata via `storage` (FR-STORE-1). When the mic
|
||||||
this meeting" is **off**, delete the working WAV **after** the transcript is finalized; if **on**,
|
is on, `audio.wav` is a **dual-channel split** (left = mic, right = loopback; ADR-0005 Phase 3.5,
|
||||||
keep it as `audio.wav` (ADR-0009, FR-REC-1/4).
|
FR-SPK), so attribution is **per-stream**: sherpa clusters the **right channel only** →
|
||||||
|
`Speaker N`, and "You" comes from **left-channel** voice activity. Blind whole-signal clustering
|
||||||
|
+ voiceprint is the mic-off (`summed`) fallback. If "Record this meeting" is **off**, delete the
|
||||||
|
working WAV **after** the transcript is finalized; if **on**, keep it as `audio.wav` (ADR-0009,
|
||||||
|
FR-REC-1/4).
|
||||||
7. If an LLM provider is configured: `llm` builds a prompt (transcript + metadata + template),
|
7. If an LLM provider is configured: `llm` builds a prompt (transcript + metadata + template),
|
||||||
streams a summary/decisions/action-items into the summary panel (FR-LLM-2/4).
|
streams a summary/decisions/action-items into the summary panel (FR-LLM-2/4).
|
||||||
8. Action items are parsed and presented for confirmation; confirmed ones persist and may raise
|
8. Action items are parsed and presented for confirmation; confirmed ones persist and may raise
|
||||||
|
|||||||
+31
-1
@@ -14,7 +14,8 @@ Default root: `%LOCALAPPDATA%\WhispAssist\` (user-configurable, FR-STORE-2).
|
|||||||
│ └── spk-eres2net.onnx
|
│ └── spk-eres2net.onnx
|
||||||
└── meetings\
|
└── meetings\
|
||||||
└── <meeting_id>\ # one folder per meeting (uuid)
|
└── <meeting_id>\ # one folder per meeting (uuid)
|
||||||
├── audio.wav # canonical recording — present ONLY if "Record" was on (ADR-0009)
|
├── audio.wav # canonical recording — present ONLY if "Record" was on (ADR-0009). Mic on → stereo split (L=mic/"You", R=loopback/"Speaker", `audio_layout=split`); mic off → summed. Playback folds to mono; export folds to dual-mono (ADR-0005 Phase 3.5)
|
||||||
|
├── voiceprint.wav # mic-only 16kHz sample for "You" re-matching on reprocess of a SUMMED recording (FR-SPK); lives/dies with audio.wav
|
||||||
├── transcript.json # canonical transcript (segments+speakers+timings)
|
├── transcript.json # canonical transcript (segments+speakers+timings)
|
||||||
├── manual_notes.json # raw user-authored notes captured live during recording
|
├── manual_notes.json # raw user-authored notes captured live during recording
|
||||||
├── notes.md # the final notes document: manual notes + transcript, merged at finalize
|
├── notes.md # the final notes document: manual notes + transcript, merged at finalize
|
||||||
@@ -23,6 +24,13 @@ Default root: `%LOCALAPPDATA%\WhispAssist\` (user-configurable, FR-STORE-2).
|
|||||||
└── <brief_id>.json # agent-ready spec served via the MCP `get_feature_brief` tool
|
└── <brief_id>.json # agent-ready spec served via the MCP `get_feature_brief` tool
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A **bundle export** (`export_meeting` / `bulk_export_meetings` with `format: "bundle"`, FR-STORE-4)
|
||||||
|
copies a meeting's `audio.wav`, `transcript.json`, `notes.md`, and `summary.json` (all decrypted)
|
||||||
|
into a destination folder plus a `meeting.json` manifest (the `MeetingBundle`: title, timestamps,
|
||||||
|
duration, language/backend/model, tags, speakers, and confirmed action items). `import_meeting_bundle`
|
||||||
|
reconstructs each such folder under a fresh meeting id — the portable format for moving recordings
|
||||||
|
between computers.
|
||||||
|
|
||||||
Rule: while a meeting is in progress a working WAV is the source of truth for crash recovery. On
|
Rule: while a meeting is in progress a working WAV is the source of truth for crash recovery. On
|
||||||
finalize, it is **kept** as `audio.wav` if "Record this meeting" was on, or **deleted** if not
|
finalize, it is **kept** as `audio.wav` if "Record this meeting" was on, or **deleted** if not
|
||||||
(FR-REC-1/4) — deletion happens only after `transcript.json` is finalized. `transcript.json` and
|
(FR-REC-1/4) — deletion happens only after `transcript.json` is finalized. `transcript.json` and
|
||||||
@@ -79,6 +87,7 @@ CREATE TABLE meetings (
|
|||||||
language TEXT, -- detected/selected language code
|
language TEXT, -- detected/selected language code
|
||||||
backend_used TEXT, -- npu|nvidia|amd|intel|cpu
|
backend_used TEXT, -- npu|nvidia|amd|intel|cpu
|
||||||
model_used TEXT, -- e.g. whisper-base
|
model_used TEXT, -- e.g. whisper-base
|
||||||
|
audio_layout TEXT, -- split|summed|NULL; how audio.wav's channels are laid out (0009, ADR-0005 Phase 3.5)
|
||||||
calendar_event_id TEXT, -- FK -> calendar_events.id (nullable)
|
calendar_event_id TEXT, -- FK -> calendar_events.id (nullable)
|
||||||
template_id TEXT, -- note-template id (T8.1, FR-NOTE-5); catalog is
|
template_id TEXT, -- note-template id (T8.1, FR-NOTE-5); catalog is
|
||||||
-- a built-in Rust list (notes::templates), not a table
|
-- a built-in Rust list (notes::templates), not a table
|
||||||
@@ -373,6 +382,13 @@ segment (the M1 grounding invariant, asserted by the golden-transcript test).
|
|||||||
"expose_recordings": false, // never serve .wav unless explicitly true (FR-MCP-3)
|
"expose_recordings": false, // never serve .wav unless explicitly true (FR-MCP-3)
|
||||||
},
|
},
|
||||||
"privacy": { "encrypt_at_rest": false },
|
"privacy": { "encrypt_at_rest": false },
|
||||||
|
// Launch WhispAssist at login (NFR-RES-4). Opt-in, OFF by default. Toggling this
|
||||||
|
// via `set_auto_start` also writes a per-user `HKCU\...\Run` entry (no admin);
|
||||||
|
// startup reconciles the OS entry to this flag (e.g. after a reinstall).
|
||||||
|
"auto_start": false,
|
||||||
|
// Closing the window hides WhispAssist to the system tray (keep running in background) instead of
|
||||||
|
// quitting; ON by default. Tray "Quit" is the real exit. Enforced in the Rust on_window_event handler.
|
||||||
|
"close_to_tray": true,
|
||||||
// Optional MS Graph calendar source (M4.4, T8.9, FR-CAL-6). Opt-in, explicit consent via OAuth
|
// Optional MS Graph calendar source (M4.4, T8.9, FR-CAL-6). Opt-in, explicit consent via OAuth
|
||||||
// PKCE — OFF by default. `credential_ref` points into the OS credential store; the token itself
|
// PKCE — OFF by default. `credential_ref` points into the OS credential store; the token itself
|
||||||
// is never written here (same invariant as sync credentials, FR-SYNC-6).
|
// is never written here (same invariant as sync credentials, FR-SYNC-6).
|
||||||
@@ -380,6 +396,20 @@ segment (the M1 grounding invariant, asserted by the golden-transcript test).
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## First-run deploy seeding (`wa-defaults.ini`)
|
||||||
|
|
||||||
|
For enterprise mass-deployment, the **first** launch on a machine (before `settings.json` exists)
|
||||||
|
optionally seeds its defaults from an admin-supplied INI. First file found wins:
|
||||||
|
|
||||||
|
1. `%PROGRAMDATA%\WhispAssist\wa-defaults.ini` — machine-wide (GPO / SCCM / Intune file copy).
|
||||||
|
2. `<install dir>\wa-defaults.ini` — the bundled template (shipped fully commented → no-op).
|
||||||
|
|
||||||
|
Keys are flat `key = value` matching `settings.json` field names (bools/ints coerced), plus the
|
||||||
|
special `auto_download_model = true` which fetches the configured `whisper_model` in the background.
|
||||||
|
**No secrets** — any key containing `key`/`token`/`secret`/`credential`/`password` is ignored; those
|
||||||
|
live only in the OS credential store. After first run the file is never read again. See
|
||||||
|
`docs/enterprise-deployment.md` and `src-tauri/src/deploy.rs`.
|
||||||
|
|
||||||
## Retention & recovery semantics
|
## Retention & recovery semantics
|
||||||
|
|
||||||
- **Retention** (FR-STORE-2): a background job deletes whole meeting folders + rows once a meeting
|
- **Retention** (FR-STORE-2): a background job deletes whole meeting folders + rows once a meeting
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ start_recording(input: { meetingTitle?: string; calendarEventId?: string; record
|
|||||||
stop_recording(input: { meetingId: MeetingId }): MeetingSummaryRef
|
stop_recording(input: { meetingId: MeetingId }): MeetingSummaryRef
|
||||||
pause_recording(input: { meetingId: MeetingId }): void
|
pause_recording(input: { meetingId: MeetingId }): void
|
||||||
resume_recording(input: { meetingId: MeetingId }): void
|
resume_recording(input: { meetingId: MeetingId }): void
|
||||||
|
// Mute/unmute the mic mid-meeting (FR-CAP-7): mic channel goes silent (recording + transcript + meter),
|
||||||
|
// loopback keeps capturing. Returns the new muted state; emits recording://mic. Errs if the mic is off.
|
||||||
|
toggle_microphone_mute(input: { meetingId: MeetingId }): boolean
|
||||||
set_recording_retention(input: { meetingId: MeetingId; record: boolean }): void // toggle mid-meeting (FR-REC-1)
|
set_recording_retention(input: { meetingId: MeetingId; record: boolean }): void // toggle mid-meeting (FR-REC-1)
|
||||||
acknowledge_recording_consent(): void // one-time (FR-REC-2)
|
acknowledge_recording_consent(): void // one-time (FR-REC-2)
|
||||||
|
|
||||||
@@ -36,8 +39,14 @@ update_live_notes(input: { meetingId: MeetingId; markdown: string }): void
|
|||||||
set_segment_note(input: { meetingId: MeetingId; anchorMs: number; text: string }): void
|
set_segment_note(input: { meetingId: MeetingId; anchorMs: number; text: string }): void
|
||||||
|
|
||||||
// ---- Hardware ----
|
// ---- Hardware ----
|
||||||
hardware_status(): { backends: BackendInfo[]; active: BackendId; modelSize: string; estRtf: number }
|
hardware_status(): { backends: BackendInfo[]; active: BackendId; modelSize: string; estRtf: number } // re-detects fresh each call — backs the Settings ▸ Hardware "Refresh" button
|
||||||
set_preferred_backend(input: { backend: BackendId | "auto" }): void
|
set_preferred_backend(input: { backend: BackendId | "auto" }): void
|
||||||
|
// Launch-at-login (NFR-RES-4). Writes/removes a per-user OS Run entry (no admin) and persists auto_start. Opt-in, off by default.
|
||||||
|
set_auto_start(input: { enabled: boolean }): void
|
||||||
|
// Device test: opens a mic ("input") or the render device in loopback ("loopback") for a few seconds and streams device://level (no recording, no retained audio). Refused while recording.
|
||||||
|
monitor_audio_level(input: { kind: "input" | "loopback"; deviceId?: string; durationMs?: number }): void
|
||||||
|
// Quick stress test: benchmarks each available backend × installed model (≤3 sizes) on a fixed sample, returns per-pair real-time factor + the most-accurate real-time-capable recommendation. Emits stress://progress. Refused while recording.
|
||||||
|
stress_test_hardware(): { results: { backend: string; model: string; rtf: number; realtime: boolean }[]; recommended: { backend: string; model: string } | null }
|
||||||
|
|
||||||
// ---- Transcription / models ----
|
// ---- Transcription / models ----
|
||||||
// language (T8.7, M4.2): omitted reuses the meeting's current language rather than resetting it.
|
// language (T8.7, M4.2): omitted reuses the meeting's current language rather than resetting it.
|
||||||
@@ -63,10 +72,24 @@ map_speaker_to_participant(input: { meetingId: MeetingId; label: string; partici
|
|||||||
// yet at local-desktop meeting counts) and gained from/to date filters, per
|
// yet at local-desktop meeting counts) and gained from/to date filters, per
|
||||||
// FR-SEARCH-2's "filter by date, tag, or participant".
|
// FR-SEARCH-2's "filter by date, tag, or participant".
|
||||||
list_meetings(input: { query?: string; tag?: string; participantId?: string; from?: number; to?: number }): MeetingListItem[]
|
list_meetings(input: { query?: string; tag?: string; participantId?: string; from?: number; to?: number }): MeetingListItem[]
|
||||||
|
// Meeting also carries `action_items: ActionItem[]` — table-backed (the confirmed/edited source
|
||||||
|
// of truth), falling back to summary.json drafts until any are saved (FR-LLM-3). TranscriptSegment
|
||||||
|
// keeps start_ms/end_ms; the UI renders these as per-line timestamps.
|
||||||
get_meeting(input: { meetingId: MeetingId }): Meeting // includes transcript + speakers + summary (null until generated)
|
get_meeting(input: { meetingId: MeetingId }): Meeting // includes transcript + speakers + summary (null until generated)
|
||||||
delete_meeting(input: { meetingId: MeetingId }): void
|
delete_meeting(input: { meetingId: MeetingId }): void
|
||||||
export_meeting(input: { meetingId: MeetingId; dest: string; format: "md" | "pdf" | "docx" | "bundle" }): string
|
// format "bundle" writes a portable folder: audio.wav + transcript.json + notes.md + summary.json
|
||||||
|
// (all decrypted) + meeting.json (the MeetingBundle manifest), re-importable on another machine.
|
||||||
|
// format "obsidian" writes one self-contained vault note (dest is a .md file path, no audio):
|
||||||
|
// YAML frontmatter (title/date/duration/participants/tags/source) + notes + summary + decisions
|
||||||
|
// + action items + timestamped transcript. For dropping a meeting into an Obsidian vault.
|
||||||
|
export_meeting(input: { meetingId: MeetingId; dest: string; format: "md" | "pdf" | "docx" | "bundle" | "obsidian" }): string
|
||||||
|
// Edit commands that change an uploaded artifact — update_notes, set_tags, generate_summary,
|
||||||
|
// reprocess_transcript, confirm_action_items — auto-resync when sync is enabled (FR-SYNC-5):
|
||||||
|
// they enqueue for finalize-trigger targets and pump in the background. SHA-256 dedup means an
|
||||||
|
// edit that didn't alter a file uploads nothing.
|
||||||
update_notes(input: { meetingId: MeetingId; markdown: string }): void
|
update_notes(input: { meetingId: MeetingId; markdown: string }): void
|
||||||
|
// AI-enhance rough notes into structured Markdown grounded in the transcript (Granola-style), via the configured LlmProvider (no new egress). Takes the live buffer, returns the enhanced text WITHOUT persisting — the UI keeps or undoes it. Refused while recording; errors with no provider.
|
||||||
|
enhance_notes(input: { meetingId: MeetingId; notes: string }): string
|
||||||
// SearchHit = MeetingListItem fields (id, title, started_at, duration_secs, status, tags) + snippet: string
|
// SearchHit = MeetingListItem fields (id, title, started_at, duration_secs, status, tags) + snippet: string
|
||||||
search(input: { query: string }): SearchHit[] // FTS (FR-SEARCH-1)
|
search(input: { query: string }): SearchHit[] // FTS (FR-SEARCH-1)
|
||||||
set_tags(input: { meetingId: MeetingId; tags: string[] }): void
|
set_tags(input: { meetingId: MeetingId; tags: string[] }): void
|
||||||
@@ -77,6 +100,17 @@ list_note_templates(): NoteTemplate[]
|
|||||||
// Bulk export (T8.5, FR-STORE-4): every meeting matching tag/from/to, one file (or bundle
|
// Bulk export (T8.5, FR-STORE-4): every meeting matching tag/from/to, one file (or bundle
|
||||||
// folder) per meeting under destDir. Returns the count actually exported.
|
// folder) per meeting under destDir. Returns the count actually exported.
|
||||||
bulk_export_meetings(input: { destDir: string; format: "md" | "pdf" | "docx" | "bundle"; tag?: string; from?: number; to?: number }): number
|
bulk_export_meetings(input: { destDir: string; format: "md" | "pdf" | "docx" | "bundle"; tag?: string; from?: number; to?: number }): number
|
||||||
|
// Import bundle(s) (FR-STORE-4): `dir` is a single bundle folder (has meeting.json) or a parent
|
||||||
|
// folder of them (from a bulk export). Each is reconstructed under a fresh meeting id (original
|
||||||
|
// title/date/duration/speakers/tags/action items preserved). Returns the count imported.
|
||||||
|
import_meeting_bundle(input: { dir: string }): number
|
||||||
|
// Add a meeting from an existing recording: a local audio/video file path or a URL (YouTube/
|
||||||
|
// streaming page or direct media URL). Needs ffmpeg (+ yt-dlp for URLs) on PATH; neither bundled.
|
||||||
|
// `model` overrides the Settings whisper model for this import (recorded as meeting.model_used).
|
||||||
|
// Returns the new meeting id IMMEDIATELY (status "transcribing"); transcode→transcribe→diarize→
|
||||||
|
// finalize run in the background, streaming import://progress and ending with transcript://finalized.
|
||||||
|
// A failed import is left in the list with status "error" (not deleted).
|
||||||
|
import_media(input: { source: string; title?: string; model?: string }): MeetingId
|
||||||
|
|
||||||
// ---- LLM / AI provider (ADR-0007/0011) ----
|
// ---- LLM / AI provider (ADR-0007/0011) ----
|
||||||
// provider ∈ ollama | custom | anthropic | openai | off. Hosted-provider API keys are passed to
|
// provider ∈ ollama | custom | anthropic | openai | off. Hosted-provider API keys are passed to
|
||||||
@@ -167,9 +201,11 @@ privacy_self_check(): {
|
|||||||
"recording://state" { meetingId, state: "recording"|"paused"|"stopped"|"cancelled", elapsedMs }
|
"recording://state" { meetingId, state: "recording"|"paused"|"stopped"|"cancelled", elapsedMs }
|
||||||
"recording://level" { meetingId, rms: number, peak: number } // waveform (FR-CAP-5)
|
"recording://level" { meetingId, rms: number, peak: number } // waveform (FR-CAP-5)
|
||||||
"recording://device" { meetingId, recovered: boolean, message: string } // capture device change (FR-CAP-6)
|
"recording://device" { meetingId, recovered: boolean, message: string } // capture device change (FR-CAP-6)
|
||||||
"transcript://segment" { meetingId, segment: TranscriptSegment } // live segments (FR-TRX-2)
|
"recording://mic" { meetingId, muted: boolean } // mic mute toggled (FR-CAP-7)
|
||||||
|
"transcript://segment" { meetingId, segment: TranscriptSegment } // live segments (FR-TRX-2); may re-emit a committed segment with a refined `speaker` — replace by `segment.id`
|
||||||
"transcript://finalized" { meetingId, segmentCount }
|
"transcript://finalized" { meetingId, segmentCount }
|
||||||
"diarization://updated" { meetingId, speakers: SpeakerInfo[] } // after post-pass (FR-SPK)
|
"import://progress" { meetingId, phase: "prepare"|"transcribe"|"diarize"|"finalize", state: "active"|"done"|"error", elapsedMs: number|null, error: string|null } // background import_media tracker
|
||||||
|
"diarization://updated" { meetingId, speakers: SpeakerInfo[] } // post-pass AND live 15s provisional passes (FR-SPK); carries "You" once the mic voiceprint matches
|
||||||
"llm://token" { meetingId, text } // streamed summary (FR-LLM-4)
|
"llm://token" { meetingId, text } // streamed summary (FR-LLM-4)
|
||||||
"llm://done" { meetingId, summary: SummaryFile } // full summary.json contents, not just a pointer
|
"llm://done" { meetingId, summary: SummaryFile } // full summary.json contents, not just a pointer
|
||||||
"model://progress" { id, receivedBytes, totalBytes }
|
"model://progress" { id, receivedBytes, totalBytes }
|
||||||
@@ -177,6 +213,8 @@ privacy_self_check(): {
|
|||||||
"calendar://linked" { ok: boolean, error?: string } // MS Graph OAuth handshake settled (M4.4)
|
"calendar://linked" { ok: boolean, error?: string } // MS Graph OAuth handshake settled (M4.4)
|
||||||
"calendar://progress" { processed, total } // MS Graph import (M4.4)
|
"calendar://progress" { processed, total } // MS Graph import (M4.4)
|
||||||
"hardware://changed" { active: BackendId, reason: string } // fallback occurred (FR-HW-4)
|
"hardware://changed" { active: BackendId, reason: string } // fallback occurred (FR-HW-4)
|
||||||
|
"device://level" { kind: "input"|"loopback", rms?, peak?, done?: boolean } // Settings device test meter; done=window ended
|
||||||
|
"stress://progress" { backend: string, model: string } // quick stress test, per pairing benchmarked
|
||||||
"recording://retention" { meetingId, record: boolean } // retention toggled (FR-REC-1/3)
|
"recording://retention" { meetingId, record: boolean } // retention toggled (FR-REC-1/3)
|
||||||
"sync://job" { jobId, meetingId, targetId, artifact, status, bytesSent, bytesTotal } // FR-SYNC-5
|
"sync://job" { jobId, meetingId, targetId, artifact, status, bytesSent, bytesTotal } // FR-SYNC-5
|
||||||
"sync://done" { meetingId, targetId, uploaded: number, failed: number }
|
"sync://done" { meetingId, targetId, uploaded: number, failed: number }
|
||||||
|
|||||||
@@ -32,6 +32,54 @@ labels in the post-meeting pass; this keeps latency low while improving final ac
|
|||||||
- Speaker IDs (`S1`, `S2`, …) are internal and stable per meeting; name mappings live in the DB
|
- Speaker IDs (`S1`, `S2`, …) are internal and stable per meeting; name mappings live in the DB
|
||||||
and are applied at render/export time, never destructively rewritten onto segments.
|
and are applied at render/export time, never destructively rewritten onto segments.
|
||||||
|
|
||||||
|
### Phase 3 refinement — per-stream "You" attribution (FR-SPK, 2026-07-14)
|
||||||
|
|
||||||
|
The single-pass-over-the-whole-recording model above blind-clusters a **summed mono** signal
|
||||||
|
(mic + loopback), then guesses which cluster is the user via a voiceprint match. In practice that
|
||||||
|
clustering is unreliable in both directions on the summed signal — it over-split a 2-speaker call
|
||||||
|
into 83 clusters at one threshold and merged two clearly distinct voices into one at another — and
|
||||||
|
the voiceprint can only *label* a cluster, never *create* the separation.
|
||||||
|
|
||||||
|
So when the **microphone is enabled**, WA no longer relies on clustering to find the user:
|
||||||
|
|
||||||
|
- A live **mic-activity timeline** (`audio::MicActivity`) records, per 100 ms of `audio.wav` frame
|
||||||
|
time, when the mic was speech-level — captured in the loopback writer, the one place the mic and
|
||||||
|
loopback exist separately in the recording's own timebase. Those ranges become **"You"** spans
|
||||||
|
directly (no clustering, no embedding).
|
||||||
|
- sherpa clustering is then run over the recording with the "You" ranges **masked out** (zeroed),
|
||||||
|
so it only ever splits the **far side** into `Speaker N`. The mic can never pollute or merge into
|
||||||
|
a far-side cluster.
|
||||||
|
- The timeline is persisted as `mic_activity.json` (retained meetings) so `reprocess_transcript`
|
||||||
|
re-attributes identically without the live capture.
|
||||||
|
|
||||||
|
The original blind-clustering + voiceprint pass **remains the fallback** for mic-off recordings and
|
||||||
|
imports (no timeline). Naming stays uniform (`build_name_map`: "You", then `Speaker 2…`). Segment
|
||||||
|
IDs and the names-in-DB rule are unchanged; only the *source* of the spans changes.
|
||||||
|
|
||||||
|
### Phase 3.5 refinement — dual-channel capture (supersedes Phase 3's timeline, 2026-07-14)
|
||||||
|
|
||||||
|
The Phase 3 timeline above reconstructed the mic/far-side split *after the fact* from a summed-mono
|
||||||
|
`audio.wav` plus a `mic_activity.json` sidecar, masking the mic ranges before clustering. That was
|
||||||
|
reliable at stop but **fragile on reprocess** (re-aligning a sidecar against a mono mix — it
|
||||||
|
collapsed to a single speaker) and carried a parallel persistence path.
|
||||||
|
|
||||||
|
**Decision:** when the mic is enabled, record `audio.wav` as **stereo with the streams separated —
|
||||||
|
left = microphone ("You"), right = system/loopback ("Speaker")** instead of summing them. The
|
||||||
|
separation is then intrinsic to the recording:
|
||||||
|
|
||||||
|
- Diarization runs on the **right channel only** → `Speaker N`; "You" comes from **left-channel**
|
||||||
|
voice activity (`vad_spans`). The mic is never clustered, by construction.
|
||||||
|
- Reprocess recomputes both from the file — no sidecar, no masking; stop and reprocess agree.
|
||||||
|
- Transcription still downmixes (L+R) to the same summed mono; playback folds to mono and bundle
|
||||||
|
export folds to dual-mono so shared/played audio is normal.
|
||||||
|
- A `meetings.audio_layout` flag (`summed` | `split`, carried in the bundle manifest) distinguishes
|
||||||
|
recordings; existing `summed` recordings keep the blind-clustering + voiceprint path. On a stereo
|
||||||
|
render endpoint this is **size-neutral** (the file was already 2ch with the mic summed into both).
|
||||||
|
|
||||||
|
This **retires** `MicActivity` / `mic_activity.json` / masked diarization from Phase 3 (see the
|
||||||
|
retirement note at `attribute_split` in `commands.rs`); the capture-silence handling,
|
||||||
|
`diarize_samples`, and `build_name_map` naming are retained. Segment IDs and names-in-DB unchanged.
|
||||||
|
|
||||||
## Revisit if
|
## Revisit if
|
||||||
A single model gives joint ASR + diarization with better accuracy, or whisper.cpp gains
|
A single model gives joint ASR + diarization with better accuracy, or whisper.cpp gains
|
||||||
production diarization.
|
production diarization.
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# ADR-0012 — Launch-at-login & enterprise deployment defaults
|
||||||
|
|
||||||
|
- **Status:** Accepted
|
||||||
|
- **Date:** 2026-07-14
|
||||||
|
- **Context source:** User request (2026-07-14) — auto-start on boot; customize an installation
|
||||||
|
(install location, per-user/all-users, default settings) with native Windows tooling for
|
||||||
|
mass-deployment.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Two related needs. (1) Users want WhispAssist to **launch automatically at login**. NFR-RES-4
|
||||||
|
forbids adding WA to OS startup without explicit consent, so this must be opt-in. (2) An admin
|
||||||
|
mass-deploying WA to many machines wants to **customize the deployment** — install location, whether
|
||||||
|
it installs per-user (no admin) or all-users, and the app's default settings (record-by-default,
|
||||||
|
preferred backend, a model to pre-download, retention, AI provider) — using **native Windows tools**
|
||||||
|
(GPO / SCCM / Intune / `msiexec` / silent NSIS), not a bespoke management console.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. **Launch-at-login is opt-in, off by default.** A `set_auto_start` command uses
|
||||||
|
`tauri-plugin-autostart` to write a **per-user** `HKCU\...\Run` entry (no admin) and persists an
|
||||||
|
`auto_start` setting. Startup reconciles the OS entry to that flag (restores it after a reinstall).
|
||||||
|
Nothing runs on a timer — this is a registry entry, not a background process (NFR-RES-1).
|
||||||
|
2. **Install location & scope are native, no app code.**
|
||||||
|
- Location: `msiexec INSTALLDIR=…` (MSI) / NSIS `/D=…` (silent).
|
||||||
|
- Scope: NSIS `installMode: "both"` — the `.exe` setup lets the user choose **current-user
|
||||||
|
(no admin)** or **all-users (admin)**. The MSI stays per-machine as the enterprise all-users
|
||||||
|
artifact.
|
||||||
|
3. **Default settings via a first-run `wa-defaults.ini`.** On a machine's **first** launch (before
|
||||||
|
`settings.json` exists) WA reads an admin-supplied INI — `%PROGRAMDATA%\WhispAssist\wa-defaults.ini`
|
||||||
|
first, else the bundled `<install dir>\wa-defaults.ini` — and seeds `settings.json` from it, with an
|
||||||
|
optional `auto_download_model` to pre-fetch the model. The shipped template is fully commented, so
|
||||||
|
a normal install is unaffected. This is deployable purely by copying a file with existing Windows
|
||||||
|
management tooling; no WiX custom actions.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Positive:** opt-in startup honors NFR-RES-4 with zero idle cost; install location/scope reuse the
|
||||||
|
installers' native behavior (no custom code to maintain); one small INI + a first-run guard covers
|
||||||
|
the whole deployment-customization surface and works for MSI, NSIS, and portable copies alike.
|
||||||
|
- **Guardrail — no secrets in the deploy file (CLAUDE.md):** the INI must never carry credentials.
|
||||||
|
`deploy.rs` drops any key containing `key`/`token`/`secret`/`credential`/`password` as defense in
|
||||||
|
depth; API keys, OAuth tokens and sync passwords remain in the OS credential store only. Seeding a
|
||||||
|
provider (e.g. `llm_provider=anthropic`) still requires the admin/user to provision its key
|
||||||
|
separately — no new egress path is created by the file.
|
||||||
|
- **Negative / care:** the seed runs only when `settings.json` is absent (truly first run); it does
|
||||||
|
**not** re-apply on upgrade, matching "the user's own settings win thereafter." Array config merges
|
||||||
|
in Tauri **replace** rather than append, so `wa-defaults.ini` must be listed in both
|
||||||
|
`tauri.conf.json` and `tauri.vulkan.conf.json` bundle resources (the shipped build uses the latter).
|
||||||
|
|
||||||
|
## Revisit if
|
||||||
|
|
||||||
|
Admins need per-machine policy that **overrides** user settings on every launch (not just seeds
|
||||||
|
first-run), or a signed/locked-down enterprise policy channel beyond a plain INI.
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Enterprise deployment
|
||||||
|
|
||||||
|
How to mass-deploy WhispAssist and preset its defaults with native Windows tooling (Group Policy,
|
||||||
|
SCCM, Intune, `msiexec`, silent NSIS). No management console, no phone-home. See ADR-0012.
|
||||||
|
|
||||||
|
WhispAssist ships two bundles:
|
||||||
|
|
||||||
|
| Bundle | Scope | Admin? |
|
||||||
|
|---|---|---|
|
||||||
|
| `WhispAssist_<ver>_x64_en-US.msi` | Per-machine (all users) | Yes |
|
||||||
|
| `WhispAssist_<ver>_x64-setup.exe` (NSIS) | Current-user **or** all-users (prompts) | Only for all-users |
|
||||||
|
|
||||||
|
## Install location
|
||||||
|
|
||||||
|
- **MSI:** `msiexec /i WhispAssist_<ver>_x64_en-US.msi INSTALLDIR="D:\Apps\WhispAssist" /qn`
|
||||||
|
(`INSTALLDIR` is Tauri's WiX install-dir property; confirm against the generated `.wxs` if a build
|
||||||
|
changes it.)
|
||||||
|
- **NSIS:** `WhispAssist_<ver>_x64-setup.exe /S /D=D:\Apps\WhispAssist`
|
||||||
|
(`/S` = silent, `/D=` = install dir; `/D=` must be **last** and unquoted per NSIS.)
|
||||||
|
|
||||||
|
## Install scope (per-user vs all-users)
|
||||||
|
|
||||||
|
The NSIS `.exe` shows a "current user / all users" page. **Current user needs no admin** and installs
|
||||||
|
under the user profile; **all users** requires elevation. Silent all-users:
|
||||||
|
`WhispAssist_<ver>_x64-setup.exe /S`. The MSI is always per-machine (all-users) and requires admin.
|
||||||
|
|
||||||
|
## Auto-start at login
|
||||||
|
|
||||||
|
Off by default (NFR-RES-4). Turn it on for the user either in-app (Settings ▸ Recording ▸ *Launch
|
||||||
|
WhispAssist at login*) or by presetting `auto_start = true` in `wa-defaults.ini` (below). It installs
|
||||||
|
a **per-user** `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` entry — no admin, and it does
|
||||||
|
**not** start recording on its own.
|
||||||
|
|
||||||
|
## Preset default settings — `wa-defaults.ini`
|
||||||
|
|
||||||
|
On a machine's **first** launch (before `settings.json` exists), WhispAssist reads an admin-supplied
|
||||||
|
INI and seeds that user's `settings.json`. After that the user's own settings win and the file is
|
||||||
|
ignored. First location found wins:
|
||||||
|
|
||||||
|
1. `%PROGRAMDATA%\WhispAssist\wa-defaults.ini` — machine-wide. Deploy with a GPO/SCCM/Intune file copy.
|
||||||
|
2. `<install dir>\wa-defaults.ini` — the template shipped next to the executable.
|
||||||
|
|
||||||
|
The shipped template is fully commented out, so a default install behaves as if it were absent.
|
||||||
|
Uncomment and edit the keys you want to preset.
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
Flat `key = value`, one per line. `;` and `#` comment lines and `[section]` headers are ignored.
|
||||||
|
`true`/`false` become switches, plain numbers become numbers, everything else is text. Unknown or
|
||||||
|
misspelled keys are ignored.
|
||||||
|
|
||||||
|
> **Never put secrets in this file.** API keys, OAuth tokens and sync passwords live only in the OS
|
||||||
|
> credential store. Any key containing `key`, `token`, `secret`, `credential` or `password` is
|
||||||
|
> dropped on read. Presetting `llm_provider = anthropic` still requires the key to be provisioned
|
||||||
|
> separately — the file adds no egress path.
|
||||||
|
|
||||||
|
### Keys
|
||||||
|
|
||||||
|
| Key | Values | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `default_record` | true/false | Record every meeting by default (consent notice still applies). |
|
||||||
|
| `preferred_backend` | auto\|npu\|nvidia\|amd\|intel\|cpu | Transcription backend. |
|
||||||
|
| `whisper_model` | catalog id (e.g. `base.en-q5_1`) | Default transcription model. |
|
||||||
|
| `auto_download_model` | true/false | Fetch `whisper_model` in the background on first launch. |
|
||||||
|
| `whisper_language` | auto\|ISO-639-1 | Default language (multilingual model only). |
|
||||||
|
| `low_overhead` | true/false | CPU + smallest model preset. |
|
||||||
|
| `storage_root` | path | Where meetings are stored. |
|
||||||
|
| `retention_max_age_days` | number | Delete meetings older than N days. |
|
||||||
|
| `retention_max_size_gb` | number | Cap total storage at N GB. |
|
||||||
|
| `llm_provider` | ollama\|custom\|anthropic\|off | Summary provider (key provisioned separately). |
|
||||||
|
| `llm_endpoint` | url | LLM endpoint. |
|
||||||
|
| `llm_model` | text | LLM model name. |
|
||||||
|
| `microphone_enabled` | true/false | Capture the user's mic into the transcript. |
|
||||||
|
| `auto_record_calendar` | true/false | Auto-start recording on calendar events (app open only). |
|
||||||
|
| `theme` | system\|light\|dark | UI theme. |
|
||||||
|
| `auto_start` | true/false | Launch WhispAssist at login (per-user Run entry). |
|
||||||
|
| `sync_enabled` | true/false | Sync master switch (targets/credentials configured in-app). |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```ini
|
||||||
|
default_record = true
|
||||||
|
preferred_backend = npu
|
||||||
|
whisper_model = small.en-q5_1
|
||||||
|
auto_download_model = true
|
||||||
|
retention_max_age_days = 90
|
||||||
|
auto_start = true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Silent end-to-end example
|
||||||
|
|
||||||
|
```bat
|
||||||
|
:: 1. Push machine-wide defaults (as SYSTEM via GPO/SCCM)
|
||||||
|
mkdir "%ProgramData%\WhispAssist"
|
||||||
|
copy wa-defaults.ini "%ProgramData%\WhispAssist\wa-defaults.ini"
|
||||||
|
|
||||||
|
:: 2. Install per-machine, custom location, no UI
|
||||||
|
msiexec /i WhispAssist_<ver>_x64_en-US.msi INSTALLDIR="C:\Program Files\WhispAssist" /qn
|
||||||
|
```
|
||||||
|
|
||||||
|
Each user's first launch then seeds their `settings.json` from the machine-wide file.
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# i18n migration tracking
|
||||||
|
|
||||||
|
Working doc for the incremental UI-string translation effort. The i18n **mechanism**
|
||||||
|
is done; this tracks moving the app's remaining hardcoded strings into the translation
|
||||||
|
files, one batch at a time. Tick boxes as views are converted.
|
||||||
|
|
||||||
|
> **Status (2026-07-12): migration complete.** All views and components (Batches A–F) are
|
||||||
|
> converted; `en.json` holds ~506 keys. Every user-facing English string flows through
|
||||||
|
> `t()`. What remains is intentionally-untranslated data (see the "Not translated" section)
|
||||||
|
> — plus the actual work of adding a second language, which is now just translating
|
||||||
|
> `en.json` into a new `<code>.json`.
|
||||||
|
|
||||||
|
- **Engine:** hand-rolled, zero-dependency. `src/lib/i18n/index.svelte.ts`
|
||||||
|
- **Baseline dictionary:** `src/lib/i18n/en.json` (English is the source-of-truth key set
|
||||||
|
**and** the fallback for any missing key)
|
||||||
|
- **Selector:** Settings → Language (`section === "language"`)
|
||||||
|
- **Scope decision:** display language is separate from the transcription `whisper_language`
|
||||||
|
setting — don't conflate them.
|
||||||
|
|
||||||
|
## What we translate (and what we never do)
|
||||||
|
|
||||||
|
**Translate: UI chrome only** — fixed labels, buttons, headings, placeholders,
|
||||||
|
empty/loading/status states, tooltips, and app-generated default *labels*.
|
||||||
|
|
||||||
|
**Never translate: user-authored content.** Meeting titles, tag names, notes, transcript
|
||||||
|
text, search snippets, dates — these are bound straight from data (`item.title`, tag
|
||||||
|
values, `s.text`, …) and stay exactly as the user wrote them. If a string comes from the
|
||||||
|
user or the recording, it does not get a key.
|
||||||
|
|
||||||
|
So "convert view X" always means "key its fixed labels", never "touch its content". Most
|
||||||
|
views are mostly content with a thin shell of labels — MeetingsList, for example, is ~10
|
||||||
|
fixed labels (search placeholder, empty states, filter/bulk-export labels) wrapped around
|
||||||
|
a list whose rows are pure user data.
|
||||||
|
|
||||||
|
> Caveat — the `"Untitled meeting"` **default title is generated in the Rust backend**, not
|
||||||
|
> the frontend, so it never reaches `t()`. Localizing app-generated defaults is a separate
|
||||||
|
> backend decision, out of scope for this frontend effort.
|
||||||
|
|
||||||
|
## How to convert a string (the pattern)
|
||||||
|
|
||||||
|
1. Add a key to `en.json`. Naming: `<area>.<subarea>.<name>`, dotted, grouped by view —
|
||||||
|
e.g. `nav.recording`, `settings.transcription.title`, `meetings.empty`.
|
||||||
|
2. Replace the literal in markup with `{t("key")}` (import `t` from `../i18n/index.svelte`).
|
||||||
|
3. Dynamic bits use placeholders: `t("meetings.count", { n })` against
|
||||||
|
`"meetings.count": "{n} meetings"`. Handle plurals with a caller-side ternary for now
|
||||||
|
(`n === 1 ? t("...one") : t("...many")`) — the engine is intentionally simple.
|
||||||
|
4. Attributes translate the same way: `title={t("...")}`, `aria-label={t("...")}`,
|
||||||
|
`placeholder={t("...")}`.
|
||||||
|
5. `npm run check` must stay at 0 errors/warnings.
|
||||||
|
|
||||||
|
> When adding a **new language** (not covered by this doc's batches): copy `en.json` to
|
||||||
|
> `<code>.json`, translate it, and register it in `DICTS` + `LOCALES` in
|
||||||
|
> `index.svelte.ts`. That one file is the whole job.
|
||||||
|
|
||||||
|
## Done
|
||||||
|
|
||||||
|
- [x] i18n engine + `en.json` baseline + Settings language selector (branch
|
||||||
|
`feature_chore_bug_007`)
|
||||||
|
- [x] `Settings.svelte` — nav labels (`nav.*`), the Language section
|
||||||
|
(`settings.language.*`), and the Transcription Language section
|
||||||
|
(`settings.transcription.*`)
|
||||||
|
|
||||||
|
- [x] `src/App.svelte` — app shell chrome (`app.*`): header (tagline, template
|
||||||
|
picker, record/stop/cancel/add-meeting, recording status, retention, backend,
|
||||||
|
device-notice), Settings button, consent + vault-unlock dialogs, pane toggles,
|
||||||
|
splitter labels, and JS strings (discard confirm, SR announcements, vault error).
|
||||||
|
- [x] `src/lib/views/MeetingsList.svelte` — labels only (`meetings.*`): search/filter/
|
||||||
|
bulk-export controls, empty/loading/status states, status badges (via
|
||||||
|
`statusLabel`), resume + delete, delete-confirm, and the pluralized export result.
|
||||||
|
List rows (titles/tags/dates/snippets) left as user data.
|
||||||
|
- [x] `src/lib/views/TranscriptNotes.svelte` — transcript + notes chrome (`transcript.*`,
|
||||||
|
`notes.*`): pane headings/toggles, reprocess controls, notes toolbar (bold/heading/
|
||||||
|
list/preview), export button tooltips + dialog filter names, empty states, segment
|
||||||
|
tooltips + note placeholders. Transcript/notes/speaker text left as user data.
|
||||||
|
- [x] `src/lib/views/SummaryPanel.svelte` — all panel chrome (`summary.*`): section
|
||||||
|
headings (Recording/Sync/Tags/Summary/Briefs/Action items/Calendar/Participants/
|
||||||
|
Speakers), buttons, placeholders, empty states, provider labels (via `providerLabel`;
|
||||||
|
brand names kept literal), action-item + brief + speaker controls. Summary text,
|
||||||
|
tags, brief content, participant/speaker names left as user data.
|
||||||
|
- [x] `src/lib/views/Settings.svelte` — **all sections** done (`settings.*`): the modal
|
||||||
|
shell (dialog title, Close), plus recording, hardware (incl. models/diarization),
|
||||||
|
storage (+ export/import status), calendar (.pst import/cleanup/events), sync
|
||||||
|
(targets + WebDAV/OAuth forms), AI (provider + Ollama advanced params), MCP
|
||||||
|
(transport/scope/token/access log), privacy (egress self-check + vault), about.
|
||||||
|
OLLAMA_OPTIONS param catalog labels/help left as data (config catalog, like model
|
||||||
|
ids); example URLs/model-id placeholders left literal.
|
||||||
|
|
||||||
|
- [x] Components (Batch F): `ConsentNotice` + `HostedAiBanner` (legal/consent copy,
|
||||||
|
`consent.*` / `hosted.*`), `ThemeToggle` (`theme.*`), `ImportMeeting` (`import.*`),
|
||||||
|
`TagChip` (`tagchip.*`), `LevelMeter` (`levelmeter.*`). `Splitter`'s `label` is
|
||||||
|
caller-supplied and already translated by the parent; no strings of its own.
|
||||||
|
|
||||||
|
## Outstanding — suggested batches
|
||||||
|
|
||||||
|
_None — all batches complete._ The section below is kept as a record of the plan.
|
||||||
|
|
||||||
|
Ordered roughly by user-visibility ÷ effort. Sizes are rough (line count / labeled
|
||||||
|
attributes) to help portion the work, not exact string counts. A file isn't "done" until
|
||||||
|
its visible text **and** its `title`/`aria-label`/`placeholder` attributes are keyed.
|
||||||
|
|
||||||
|
### ~~Batch A — app shell~~ ✅ done (branch `feature_chore_bug_007`)
|
||||||
|
Moved to the Done list above.
|
||||||
|
|
||||||
|
### ~~Batch B — meetings list~~ ✅ done (branch `feature_chore_bug_007`)
|
||||||
|
Moved to the Done list above.
|
||||||
|
|
||||||
|
### ~~Batch C — transcript & notes~~ ✅ done (branch `feature_chore_bug_007`)
|
||||||
|
Moved to the Done list above.
|
||||||
|
|
||||||
|
### ~~Batch D — summary panel~~ ✅ done (branch `feature_chore_bug_007`)
|
||||||
|
Moved to the Done list above.
|
||||||
|
|
||||||
|
### ~~Batch E — Settings, all sections~~ ✅ done (branch `feature_chore_bug_007`)
|
||||||
|
All nine sections + the modal shell converted, one commit per section. Moved to the Done
|
||||||
|
list above.
|
||||||
|
|
||||||
|
### ~~Batch F — components~~ ✅ done (branch `feature_chore_bug_007`)
|
||||||
|
Moved to the Done list above.
|
||||||
|
|
||||||
|
## Not translated (intentional)
|
||||||
|
|
||||||
|
- **User-authored content** — meeting titles, tag names, notes, transcript text, search
|
||||||
|
snippets. Bound from data; stays as the user wrote it (see "What we translate" above).
|
||||||
|
- App-generated defaults created in the backend (e.g. `"Untitled meeting"`) — a separate
|
||||||
|
backend concern; the frontend `t()` never sees them.
|
||||||
|
- Backend / Rust error messages surfaced via `WaError` — out of scope for the frontend
|
||||||
|
`t()`; revisit only if we localize command errors.
|
||||||
|
- Provider/proper names (Ollama, Anthropic, OpenAI, Obsidian, WebDAV, WhispAssist),
|
||||||
|
model ids, ISO language codes.
|
||||||
|
- Console/`tracing` logs.
|
||||||
|
|
||||||
|
## Batch log
|
||||||
|
|
||||||
|
Record each landed batch here (date / branch / commit) so progress is auditable.
|
||||||
|
|
||||||
|
| Date | Batch | Branch / commit | Notes |
|
||||||
|
|------|-------|-----------------|-------|
|
||||||
|
| 2026-07-12 | Infra + Settings nav/language/transcription | `feature_chore_bug_007` | engine + en.json + selector |
|
||||||
|
| 2026-07-12 | Batch A (App shell) + Batch B (MeetingsList) | `feature_chore_bug_007` | +~60 keys; `app.*`, `meetings.*` |
|
||||||
|
| 2026-07-12 | Batch C (TranscriptNotes) + Batch D (SummaryPanel) | `feature_chore_bug_007` | +~120 keys; `transcript.*`, `notes.*`, `summary.*`; en.json now 198 keys |
|
||||||
|
| 2026-07-12 | Batch E (Settings, all 9 sections + shell) | `feature_chore_bug_007` | +~270 keys; `settings.*`; en.json now 470 keys; one commit per section |
|
||||||
|
| 2026-07-12 | Batch F (components) | `feature_chore_bug_007` | +~36 keys; `consent.*`/`hosted.*`/`theme.*`/`import.*`/`tagchip.*`/`levelmeter.*`; en.json now 506 keys — migration complete |
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
# Phase 3 design sketch: per-stream speaker attribution (FR-SPK)
|
||||||
|
|
||||||
|
Status: **signed off & in progress**, 2026-07-13. Deepens Phase 3 of
|
||||||
|
[`2026-07-13-diarization-speaker-accuracy.md`](./2026-07-13-diarization-speaker-accuracy.md).
|
||||||
|
The §§1–8 below are the original sketch (ADR-0005 review + alternatives); the **finalized
|
||||||
|
design** the user signed off on is at the top and supersedes the RMS-dominance heuristic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Finalized design (signed off 2026-07-13)
|
||||||
|
|
||||||
|
Simpler than the sketch's dominance heuristic, because the user records on **headphones** so the
|
||||||
|
mic never hears the far side (no bleed):
|
||||||
|
|
||||||
|
> **The microphone stream is "You" by definition. Diarization runs on the loopback (speaker)
|
||||||
|
> audio only, never the mic. Any loopback speaker is "Speaker N".**
|
||||||
|
|
||||||
|
Mechanism, all in **`audio.wav` frame time** (so You-spans and sherpa-spans share one clock):
|
||||||
|
|
||||||
|
1. **Mic-activity timeline** — captured in the loopback writer, the one place mic and loopback
|
||||||
|
exist separately in `audio.wav`'s timebase (`capture_loop`, where `bridge.pull()` is folded
|
||||||
|
in). Per 100 ms, flag whether the pulled mic chunk was speech-level (reuse `VOICE_ENERGY_FLOOR`).
|
||||||
|
Held in a shared `MicActivity` (sibling to `VoiceSample`); persisted for reprocess.
|
||||||
|
2. **At stop / reprocess:**
|
||||||
|
- Collapse active windows → **"You" spans** (dropping sub-`MIN_SPAN_MS` blips).
|
||||||
|
- **Mask** the You ranges out of `audio.wav` (zero them) and run sherpa on the remainder → far
|
||||||
|
side splits into **"Speaker N"** spans. Clustering never sees the mic, so the mic can't
|
||||||
|
pollute or merge with a far-side cluster.
|
||||||
|
- Merge span lists; `assign_by_overlap` unchanged.
|
||||||
|
3. **Voiceprint (Phase 1/2) becomes the fallback** for meetings with no timeline (imports, older
|
||||||
|
recordings). Live-tick voiceprint "You" is retired once the timeline drives labels.
|
||||||
|
|
||||||
|
Timebase note: with the capture-silence fix (FR-CAP-7) `audio.wav` is now continuous, so the
|
||||||
|
frame-indexed timeline aligns cleanly with what sherpa reads back via `read_wav_mono_16k`.
|
||||||
|
|
||||||
|
Implementation status (all landed 2026-07-13/14): **(a)** `MicActivity` struct + capture wiring —
|
||||||
|
done; **(b)** stop-path attribution + masked diarize (`phase3_attribute`) — done; **(c)** persist
|
||||||
|
`mic_activity.json` + reprocess uses it (imports fall back, no timeline) — done; **(d)** ADR-0005 +
|
||||||
|
`docs/02`/`03` amended — done. **Not yet done:** empirical user test; and the *live* 15s tick still
|
||||||
|
uses the Phase 2 voiceprint, not the timeline (a later polish so live labels match the stop result).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Original sketch (ADR-0005 review, risks, alternatives) follows._
|
||||||
|
|
||||||
|
Phases 0–2 shipped: cluster explosion tamed (0.7 + 700 ms floor), reprocess re-diarizes with a
|
||||||
|
persistent `voiceprint.wav`, and live "You" reaches the UI. Those all still treat diarization as
|
||||||
|
**blind clustering of a summed mono signal**, then guess which cluster is the mic. Phase 3
|
||||||
|
removes the guess for the mic side by using information WA throws away today.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. ADR-0005 review — what it commits us to
|
||||||
|
|
||||||
|
ADR-0005 (Accepted, 2026-06-30) decides: sherpa-onnx offline diarization (pyannote segmentation
|
||||||
|
+ ERes2Net embedding + clustering), behind the `diarization::Diarizer` trait, run as a
|
||||||
|
**post-processing pass over recorded audio**, aligned to whisper segments by **timestamp
|
||||||
|
overlap**. Speaker IDs (`S1`…) are internal/stable; names map in the DB, applied at render time,
|
||||||
|
**never rewritten onto segments**.
|
||||||
|
|
||||||
|
What Phase 3 must respect vs. what it may change:
|
||||||
|
|
||||||
|
- **Keeps (contract-level):** the `Diarizer` trait, post-pass-over-audio model, `SpeakerSpan`
|
||||||
|
→ segment alignment by overlap (`assign_by_overlap`), stable internal labels, names-in-DB.
|
||||||
|
Phase 3 produces *more* `SpeakerSpan`s from a better source; it does not rewrite segments or
|
||||||
|
move naming.
|
||||||
|
- **Bends (needs the ADR noted/updated):** ADR-0005 assumes one clustering pass over "the
|
||||||
|
recorded audio." Phase 3 introduces a **second, non-clustered source of spans** (the mic
|
||||||
|
activity timeline) and restricts clustering to the **far-side** audio. That is new enough to
|
||||||
|
warrant an ADR amendment or a short ADR-0005a, because a future reader will otherwise expect
|
||||||
|
all spans to come from sherpa.
|
||||||
|
- **ADR's "Revisit if"** (joint ASR+diarization model, or whisper.cpp diarization) is unrelated
|
||||||
|
— Phase 3 is orthogonal and does not trigger it.
|
||||||
|
|
||||||
|
Conclusion: Phase 3 fits inside the trait and the overlap-alignment contract. The only doc debt
|
||||||
|
is recording that "mic-dominant spans bypass clustering," which is a genuine deviation from
|
||||||
|
ADR-0005's single-source assumption. **Recommend: amend ADR-0005** (Consequences section) rather
|
||||||
|
than a new ADR — same decision, refined.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The asset we currently discard
|
||||||
|
|
||||||
|
When the mic is enabled (FR-CAP-7), `start_recording` (`commands.rs:342`) wires two independent
|
||||||
|
16 kHz-mono streams into `spawn_mixer` (`audio/mod.rs:1020`). The `Mixer` (`audio/mod.rs:979`)
|
||||||
|
holds `loopback` and `mic` **time-aligned in the same buffer** and sums them sample-for-sample
|
||||||
|
into the transcript stream — then the per-stream identity is **gone**. Diarization later reads
|
||||||
|
`audio.wav` (the summed signal) and has to reverse-engineer which cluster was the mic. That
|
||||||
|
reversal is the entire reason `voiceprint.rs` exists.
|
||||||
|
|
||||||
|
Two facts make this cheap to exploit:
|
||||||
|
|
||||||
|
1. **Per-stream RMS already exists.** `audio_level(mono, mic)` (`audio/mod.rs:522`) runs on every
|
||||||
|
WASAPI chunk for *both* directions (`is_loopback` at :564, emitted at :678). We already know,
|
||||||
|
per ~10 ms chunk, how loud each side is. We just don't persist it against time.
|
||||||
|
2. **The two streams are already aligned at the mixer**, in the **same timebase the transcript
|
||||||
|
segments derive from** (the streaming worker consumes the mixer's summed output; segment
|
||||||
|
timestamps are cumulative-samples-fed / 16). So a timeline indexed by cumulative mixer-output
|
||||||
|
samples shares the segments' clock — tighter than sherpa spans, which come from `audio.wav`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Core idea
|
||||||
|
|
||||||
|
> The mic stream is a **known** speaker. Don't cluster to find it — record when it's dominant,
|
||||||
|
> call those spans "You", and run sherpa only on the **far-side** audio to split the *other*
|
||||||
|
> participants.
|
||||||
|
|
||||||
|
Pipeline at stop (mic enabled + retained):
|
||||||
|
|
||||||
|
```
|
||||||
|
mixer ─┬─► loopback samples ──► [far-side WAV] ──► sherpa cluster ──► Speaker 2..N spans
|
||||||
|
│ │
|
||||||
|
└─► mic vs loopback RMS per 100 ms ──► mic-dominant ranges ──► "You" spans
|
||||||
|
│
|
||||||
|
merge span lists ──► assign_by_overlap ──► segments
|
||||||
|
```
|
||||||
|
|
||||||
|
The mic-dominant spans need no embedding, no clustering, no voiceprint — they are attribution by
|
||||||
|
construction. Voiceprint (Phases 1–2) stays as the **fallback** for meetings without a timeline
|
||||||
|
(imports, pre-Phase-3 recordings, mic-disabled meetings).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. The two real risks (why this needs sign-off, not just a ticket)
|
||||||
|
|
||||||
|
### 4a. Acoustic bleed — the mic is not purely "You"
|
||||||
|
|
||||||
|
Without echo cancellation (WA has none — fully local, minimal), a user on **speakers** (not
|
||||||
|
headphones) has the far side playing into the room and back into the mic. Naive "mic has signal →
|
||||||
|
You" would attribute the far side to the user whenever the far side is loud.
|
||||||
|
|
||||||
|
Mitigation (this is why the parent plan says *mic-**dominant**,* not *mic-active*): a range is
|
||||||
|
"You" only when **mic RMS meaningfully exceeds loopback RMS** in that window (e.g.
|
||||||
|
`mic_rms > k · loopback_rms` with `k ≈ 2`, plus an absolute mic-VAD floor so silence isn't
|
||||||
|
"You"). When both are comparably loud → treat as far-side/ambiguous, let sherpa/overlap decide.
|
||||||
|
This degrades gracefully: headphone users get near-perfect mic isolation; speaker users get
|
||||||
|
"You" only on clear self-speech and fall back to the old behavior during overlap. **`k` is a
|
||||||
|
calibration knob, not a constant to bury** — real rooms differ.
|
||||||
|
|
||||||
|
### 4b. Timebase — three clocks, currently reconciled by tolerance
|
||||||
|
|
||||||
|
- **Segments** are in *mixer-output* time (streaming worker over the summed stream).
|
||||||
|
- **sherpa spans** are in *`audio.wav`* time (loopback thread's byte-accurate WAV, mic bridged in
|
||||||
|
at native rate then resampled to 16 kHz on read).
|
||||||
|
- The **new mic-timeline** would be in *mixer-output* time.
|
||||||
|
|
||||||
|
Today `assign_by_overlap` already bridges segments↔sherpa-spans across the first two clocks and
|
||||||
|
tolerates the small skew. Phase 3 merges a *third* source. The mic-timeline is actually the
|
||||||
|
**closest** to segment time (shared origin), so aligning You-spans to segments is *more* reliable
|
||||||
|
than sherpa's. The residual risk is only that You-spans (mixer time) and Speaker-2..N spans
|
||||||
|
(`audio.wav` time) coexist in one merged list with a small relative skew at the boundaries. In
|
||||||
|
practice that costs at most a fraction of a `MIN_SPAN_MS` at each turn boundary — below segment
|
||||||
|
granularity. **Verification item, not a blocker.** If it proves visible, the clean fix is to run
|
||||||
|
the far-side sherpa pass over the mixer's *loopback lane* buffered to a WAV rather than over
|
||||||
|
`audio.wav`, putting everything on one clock; heavier, so deferred until measured.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Concrete design
|
||||||
|
|
||||||
|
**Capture (live, near-zero cost).** Add a mic-activity accumulator the mixer writes to. In
|
||||||
|
`Mixer::drain_ready` (or the `mixer_loop`), for each emitted window compute `mic_rms` and
|
||||||
|
`loopback_rms` over the aligned overlap (samples are already in hand there) and append a coarse
|
||||||
|
flag per 100 ms:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// audio/mod.rs — new, alongside Mixer
|
||||||
|
struct MicActivity { flags: Vec<bool>, /* one per 100 ms of mixer output */ }
|
||||||
|
// ponytail: Vec<bool> at 10 Hz = 10 bytes/s ≈ 36 KB/hour. No ring buffer, no config.
|
||||||
|
```
|
||||||
|
|
||||||
|
Expose it via the same `Arc` sharing pattern `VoiceSample` uses so `stop_recording` can read it.
|
||||||
|
Reuse `audio_level`'s RMS math — do **not** add a second RMS implementation.
|
||||||
|
|
||||||
|
**Persist (retained meetings).** At stop, alongside `voiceprint.wav`, write the timeline as
|
||||||
|
`mic_activity.json` (or pack into an existing artifact) so **reprocess** can reuse it — same
|
||||||
|
ADR-0009 retention/consent gate as `voiceprint.wav`, same lives-and-dies-with-`audio.wav` rule
|
||||||
|
(delete it everywhere `audio.wav` is deleted). Tiny, plaintext-or-sealed to match.
|
||||||
|
|
||||||
|
**Attribute (stop + reprocess).**
|
||||||
|
1. Collapse the flag timeline into `You` `SpeakerSpan`s (merge adjacent flags, drop runs
|
||||||
|
`< MIN_SPAN_MS`, reuse Phase 0's floor).
|
||||||
|
2. Build a **far-side-only** 16 kHz signal: zero out (or excise) mic-dominant ranges from the
|
||||||
|
loopback lane, run the existing `SherpaDiarizer::diarize` over it → `Speaker 2..N` spans
|
||||||
|
(label numbering starts at 2, mirroring `voiceprint::build_name_map`).
|
||||||
|
3. Merge the two span lists; `assign_by_overlap` unchanged.
|
||||||
|
4. Fallback chain: no timeline → today's Phase-1/2 voiceprint path; no models → raw `S1`.
|
||||||
|
|
||||||
|
**Contract/doc changes.**
|
||||||
|
- Amend **ADR-0005** Consequences: "mic-dominant spans are attributed directly from a per-stream
|
||||||
|
activity timeline and bypass clustering; sherpa runs on the far-side residual only."
|
||||||
|
- `docs/02-architecture.md` data-flow: note the mixer emits a mic-activity side-channel.
|
||||||
|
- `docs/03-data-model.md`: add `mic_activity.json` to the meeting-dir list (like `voiceprint.wav`).
|
||||||
|
- No new egress; no new default-on setting; `k` threshold lives in code with a calibration comment.
|
||||||
|
|
||||||
|
**Tests.**
|
||||||
|
- Unit: flag-run → span collapse (boundaries, sub-`MIN_SPAN_MS` drop) — pure, table-driven.
|
||||||
|
- Unit: dominance classifier (`mic`, `loop` RMS pairs → You / far / ambiguous), incl. the bleed
|
||||||
|
case (both loud → not You).
|
||||||
|
- Manual: 2-person call on **headphones** → exactly "You" + 1 speaker, near-zero errors; repeat
|
||||||
|
on **speakers** → verify bleed doesn't mislabel far-side as You.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Alternatives considered (and why not)
|
||||||
|
|
||||||
|
- **Do nothing / stop at Phase 2.** Legitimate. Phases 0–2 already fix the reported bugs
|
||||||
|
(83-cluster explosion, reprocess collapse, live "You"). Phase 3 is an **accuracy ceiling
|
||||||
|
raise**, not a bug fix. *If 2-person calls are the dominant case and Phase 2's voiceprint "You"
|
||||||
|
tests well in the field, Phase 3 may not clear the cost/benefit bar — measure Phase 2 first.*
|
||||||
|
This is the ponytail-honest recommendation: **verify Phase 2 empirically before building 3.**
|
||||||
|
- **"Mic active → You" (no dominance test).** Simpler, but wrong under speaker bleed (4a). The
|
||||||
|
dominance test is the minimum that survives real rooms.
|
||||||
|
- **Full source separation / AEC.** Much larger, new deps, defeats "minimal + local." The
|
||||||
|
dominance heuristic gets ~90 % of the benefit for ~5 % of the effort.
|
||||||
|
- **Better voiceprint only** (bigger sample, re-match every tick — already done in Phase 2).
|
||||||
|
Still bottlenecked on sherpa's ability to *form* a clean mic cluster in the summed signal;
|
||||||
|
Phase 3's point is to stop relying on that.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Open questions for sign-off
|
||||||
|
|
||||||
|
1. **Build it now, or verify Phase 2 first?** Recommendation: run the empirical 2-person
|
||||||
|
re-diarization (outstanding for Phases 0–2) and a Phase-2 live "You" check **before**
|
||||||
|
committing to Phase 3. If Phase 2 accuracy is acceptable, Phase 3 becomes optional.
|
||||||
|
2. **Dominance threshold `k` and mic-VAD floor** — accept a code-constant default with a
|
||||||
|
calibration comment (proposed), or expose a hidden setting? Recommendation: constant first.
|
||||||
|
3. **Far-side signal construction** — zero-out mic-dominant ranges in the loopback lane (simple,
|
||||||
|
preserves timeline) vs. excise-and-concat (shorter audio, shifts timestamps → needs remap).
|
||||||
|
Recommendation: **zero-out**, keeps one timebase.
|
||||||
|
4. **ADR: amend 0005 vs. new ADR-0005a?** Recommendation: amend.
|
||||||
|
5. **Scope of `mic_activity.json`** — persist for reprocess (proposed) or compute live-only and
|
||||||
|
accept that reprocess falls back to voiceprint? Persisting is cheap and keeps reprocess at
|
||||||
|
parity.
|
||||||
|
|
||||||
|
## 8. Rough effort
|
||||||
|
|
||||||
|
Backend-only until the far-side pass; no frontend change (labels already flow via Phase 2). Est.
|
||||||
|
~1–1.5 days: mixer side-channel + accumulator (½ day), span collapse + far-side pass + merge
|
||||||
|
(½ day), persistence + reprocess wiring + docs/tests (½ day). Contained, reversible, behind the
|
||||||
|
existing mic-enabled + retention gates.
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# Fix plan: speaker diarization accuracy ("You", live labels, 83-speaker explosion)
|
||||||
|
|
||||||
|
Status: planned 2026-07-13 (v0.5.2). Investigation confirmed in code; see memanto memories
|
||||||
|
`a9e5f29e` (diagnosis) and the entry referencing this file (fix order). Execute phases **in
|
||||||
|
order** — each phase is independently shippable and verifiable, and later phases assume
|
||||||
|
earlier ones landed.
|
||||||
|
|
||||||
|
## Symptoms (user report, all reproduced against code)
|
||||||
|
|
||||||
|
1. Live recording shows `S1` for every line; the user's own speech should show "You".
|
||||||
|
2. Post-stop diarization of a 2-person conversation produced **83** speakers.
|
||||||
|
3. Re-transcribing (English Medium) collapsed every segment to speaker "You".
|
||||||
|
|
||||||
|
## Root causes (verified anchors)
|
||||||
|
|
||||||
|
| # | Cause | Anchor |
|
||||||
|
|---|-------|--------|
|
||||||
|
| A | Live segments always carry the pre-diarization `"S1"` placeholder | `src-tauri/src/transcription/mod.rs:222`, emitted at `src-tauri/src/commands.rs:480` |
|
||||||
|
| B | The 15s provisional diarization tick relabels only the backend buffer and emits `diarization://updated`, which **no frontend code listens to** (`src/lib/api.ts` has no listener); live view renders raw labels with no name map (`src/lib/views/TranscriptNotes.svelte:527`) | `src-tauri/src/commands.rs:509-567` |
|
||||||
|
| C | "You" (mic voiceprint match) runs only in `stop_recording`, never live | `src-tauri/src/commands.rs:666-695`, `src-tauri/src/diarization/voiceprint.rs` |
|
||||||
|
| D | `reprocess_transcript` never diarizes: fresh segments all default to `"S1"`, then the meeting's **stale** name map (`S1 → "You"` from the original run) is reused, so the whole transcript renders "You" | `src-tauri/src/commands.rs:1646-1742` |
|
||||||
|
| E | Clustering over the **summed mic+loopback mono** WAV with `num_clusters: -1, threshold: 0.5` over-clusters badly (overlapped speech → mixed embeddings; short pyannote chunks → unstable ERes2Net embeddings) | `src-tauri/src/diarization/mod.rs:68-75` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0 — Tame the cluster explosion (do first; everything else is useless at 83 clusters)
|
||||||
|
|
||||||
|
Smallest possible change, lands before Phase 1 so re-diarization doesn't reproduce the
|
||||||
|
83-speaker mess.
|
||||||
|
|
||||||
|
1. In `SherpaDiarizer::new` (`src-tauri/src/diarization/mod.rs:73`) raise `threshold` from
|
||||||
|
`0.5` to `0.7` as a code constant with a comment naming the tuning evidence (83 clusters
|
||||||
|
for 2 speakers at 0.5 on mixed mono audio). sherpa fast-clustering semantics: larger
|
||||||
|
threshold → fewer clusters. Do **not** add a settings knob yet.
|
||||||
|
2. In `segment_to_span`'s caller (`SherpaDiarizer::diarize`), drop spans shorter than
|
||||||
|
700 ms before returning — sub-second chunks carry unstable embeddings and only cause
|
||||||
|
label churn in `assign_by_overlap` (which already keeps a segment's prior label when no
|
||||||
|
span overlaps).
|
||||||
|
3. Unit test: spans under the minimum are filtered; existing overlap tests still pass.
|
||||||
|
4. **Verify empirically**: re-run diarization over the user's retained 2-person recording
|
||||||
|
(the 0.5.2 test meeting) and confirm the cluster count lands near 2–4, not 83. If 0.7
|
||||||
|
still over-clusters, try 0.8 before considering per-meeting configurability.
|
||||||
|
|
||||||
|
Acceptance: 2-person retained recording diarizes to ≤4 speakers.
|
||||||
|
|
||||||
|
## Phase 1 — `reprocess_transcript` re-diarizes + persistent mic voiceprint (fixes "everything is You")
|
||||||
|
|
||||||
|
1. **Persist the voiceprint at stop.** In `stop_recording`
|
||||||
|
(`src-tauri/src/commands.rs` after the transcription worker join), when
|
||||||
|
`session.mic_voice_sample` exists **and** `session.retention` is on, write the mic
|
||||||
|
sample as 16 kHz mono `voiceprint.wav` next to `audio.wav` in `meeting_dir(&meeting_id)`.
|
||||||
|
ADR-0009 gate: it is retained audio of the user's voice, so it lives and dies with
|
||||||
|
`audio.wav` — every code path that deletes `audio.wav` (retention off at finalize,
|
||||||
|
delete-recording command) must delete `voiceprint.wav` too. Update
|
||||||
|
`docs/03-data-model.md` (meeting dir file list).
|
||||||
|
2. **Re-diarize on reprocess.** In `reprocess_transcript` (`src-tauri/src/commands.rs:1646`),
|
||||||
|
after `transcribe_file`: build the diarizer via `diarizer_from_installed_models`
|
||||||
|
(inside `spawn_blocking`, same as `stop_recording`), `diarize(&wav_path)`, then
|
||||||
|
`assign(&mut segments, &spans)`. Missing models → skip gracefully (same degradation as
|
||||||
|
live).
|
||||||
|
3. **Rebuild the name map instead of reusing the stale one.** Old labels are meaningless
|
||||||
|
after re-clustering. If `voiceprint.wav` exists, run
|
||||||
|
`voiceprint::match_mic_speaker` against the new spans to get a fresh
|
||||||
|
`You`/`Speaker N` map; otherwise use an empty map (raw `S1…` labels).
|
||||||
|
Pass `speaker_infos_from_segments(&segments, &new_names)` to `finalize_meeting`
|
||||||
|
instead of `meeting.speakers` (`src-tauri/src/commands.rs:1713`), and persist the new
|
||||||
|
names via the store the same way `stop_recording` does. Deliberate policy: user-typed
|
||||||
|
names from the original run are dropped on reprocess because they key to dead labels —
|
||||||
|
note this in the command's doc comment.
|
||||||
|
4. `import_media` reuses this path (`commands.rs:1825` "same batch path"); confirm imported
|
||||||
|
meetings get diarized speakers too (they have no voiceprint — expect raw labels).
|
||||||
|
5. Tests: unit test the fresh-name-map policy (stale map not reused); manual: re-transcribe
|
||||||
|
the 0.5.2 meeting with English Medium → segments show distinct speakers, the user's own
|
||||||
|
lines show "You".
|
||||||
|
|
||||||
|
Acceptance: re-transcription yields per-speaker labels again, with "You" on the mic
|
||||||
|
speaker when a voiceprint exists; never a single-speaker collapse.
|
||||||
|
|
||||||
|
## Phase 2 — Live labels reach the UI, including live "You" (fixes "S1 for everyone")
|
||||||
|
|
||||||
|
Backend (`src-tauri/src/commands.rs`, the 15s tick at 509–567):
|
||||||
|
|
||||||
|
1. After `diarizer.assign` in the tick, **re-emit every committed segment whose speaker
|
||||||
|
changed** via the existing `transcript://segment` event (ids are stable; the frontend
|
||||||
|
store already replaces by id — `src/lib/stores/recording.svelte.ts:49-53`). No new
|
||||||
|
event needed for relabeling.
|
||||||
|
2. In the same tick, run `voiceprint::match_mic_speaker` with
|
||||||
|
`session.mic_voice_sample.samples()` against the fresh spans (clusters re-shuffle every
|
||||||
|
tick, so match every tick; candidate audio is already capped at 10 s per cluster).
|
||||||
|
Merge results into `session.speaker_names` **without overwriting user-set names** —
|
||||||
|
same already-named guard as the post-stop pass (`commands.rs:676-683`). The tick's
|
||||||
|
existing `diarization://updated` emit then carries the "You" display name.
|
||||||
|
3. Frontend: add an `onDiarizationUpdated` listener to `src/lib/api.ts` (payload
|
||||||
|
`{ meetingId, speakers: SpeakerInfo[] }`, already documented in
|
||||||
|
`docs/04-api-contracts.md:188`). Recording store gains a `speakers` state updated by
|
||||||
|
it; live rendering at `TranscriptNotes.svelte:527` passes that list to `speakerName`
|
||||||
|
(the finalized path at :344 already does this).
|
||||||
|
4. Docs: note in `docs/04-api-contracts.md` that `transcript://segment` may re-emit a
|
||||||
|
committed segment with an updated `speaker` (replace-by-id contract).
|
||||||
|
5. Tests: frontend store test — a re-emitted segment with the same id replaces the old
|
||||||
|
one; manual: during a live 2-person call, own speech flips to "You" within ~15–30 s.
|
||||||
|
|
||||||
|
Acceptance: during recording, labels differentiate speakers and the mic speaker shows
|
||||||
|
"You" while talking (within one tick), not just after stop.
|
||||||
|
|
||||||
|
## Phase 3 — Per-stream attribution (accuracy end-game, larger change, needs design sign-off)
|
||||||
|
|
||||||
|
Blind clustering of a summed mono signal is the ceiling on accuracy. WA knows which
|
||||||
|
samples are mic before `spawn_mixer` sums them (`commands.rs:342-371`,
|
||||||
|
`src-tauri/src/audio/mod.rs` MicBridge/mixer). Design sketch — do NOT start without
|
||||||
|
reviewing ADR-0005 and getting sign-off, since it changes the diarization contract:
|
||||||
|
|
||||||
|
- Record a coarse mic-activity timeline during capture (e.g. per-100 ms mic-RMS-dominant
|
||||||
|
flags, negligible memory).
|
||||||
|
- At stop: mic-dominant ranges become "You" spans directly; run sherpa only over the
|
||||||
|
remaining (far-side) ranges to split the *other* participants; merge span lists before
|
||||||
|
`assign_by_overlap`.
|
||||||
|
- Voiceprint match remains as fallback for meetings without the timeline (imports, old
|
||||||
|
recordings).
|
||||||
|
- Update ADR-0005 (or add a new ADR) + `docs/02-architecture.md` data flow.
|
||||||
|
|
||||||
|
Acceptance: 2-person call yields exactly "You" + 1 speaker with near-zero attribution
|
||||||
|
errors on non-overlapping speech; overlapping speech attributes to the dominant stream.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-cutting rules for the executing agent
|
||||||
|
|
||||||
|
- CLAUDE.md applies in full: `cargo fmt` + `cargo clippy -- -D warnings`, no
|
||||||
|
`unwrap()`/`expect()` on user-reachable paths, conventional commits referencing
|
||||||
|
FR-SPK-*, commit each file right after finishing it (one commit per file), docs updated
|
||||||
|
in the same change when contracts move.
|
||||||
|
- Memory discipline: `memanto agent activate whispassist` first; `memanto remember` every
|
||||||
|
decision/tuning result (especially the empirical threshold from Phase 0) with full
|
||||||
|
metadata; record phase completion so the next agent knows where to resume.
|
||||||
|
- No new egress, no new settings defaults ON; `voiceprint.wav` is local retained audio
|
||||||
|
under the existing ADR-0009 consent/retention gate.
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# Plan: dual-channel capture (mic = left, loopback = right) — FR-SPK / FR-CAP
|
||||||
|
|
||||||
|
Status: **implemented** 2026-07-14 (all 3 sign-off answers: DB column; retire MicActivity with a
|
||||||
|
code note; export → dual-mono). Migration 0009 + `write_wav_split` + `attribute_split` +
|
||||||
|
`fold_wav` + ADR-0005 Phase 3.5. Supersedes the summed-mono + masked-diarize approach
|
||||||
|
in [`2026-07-13-diarization-phase3-per-stream-attribution.md`](./2026-07-13-diarization-phase3-per-stream-attribution.md).
|
||||||
|
The proposed **ADR-0005 amendment** is at the bottom — apply it on approval.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
Every diarization problem this month traces to one root: **`audio.wav` is a summed mono mix** of
|
||||||
|
mic + loopback, so the two sources are irrecoverable after the fact. Phase 3 works around that by
|
||||||
|
(a) recording a separate `mic_activity.json` "You" timeline and (b) diarizing the mix with the You
|
||||||
|
ranges masked out. It works at stop but is fragile on **reprocess** — the persisted timeline has to
|
||||||
|
re-align against freshly-transcribed segments over a mono mix, and it currently collapses to a
|
||||||
|
single "Speaker 2". The timelines are being written correctly (verified: real `you_spans`), so the
|
||||||
|
failure is the masked-mono re-alignment itself, not the capture.
|
||||||
|
|
||||||
|
**Fix the root, not the instance:** keep both sources *physically separate in the recording* by
|
||||||
|
recording **stereo — left channel = microphone ("You"), right channel = system/loopback
|
||||||
|
("Speaker")**. The separation then lives in the file forever; reprocess and diarization always have
|
||||||
|
both streams; no sidecar timeline, no masking, no summed-mono clustering.
|
||||||
|
|
||||||
|
## Key facts that make this cheap
|
||||||
|
|
||||||
|
- `audio.wav` is **already 2-channel** on a normal (stereo) render endpoint — today we *sum the mic
|
||||||
|
into both channels*. Writing mic→L and loopback(downmixed)→R is the **same channel count, same
|
||||||
|
bytes** — no size increase in the common case.
|
||||||
|
- Transcription already downmixes to mono: `read_wav_mono_16k` averaging L+R of a split file yields
|
||||||
|
`(mic + loopback)/2` = the same summed signal it gets today, so **transcription is unchanged**.
|
||||||
|
- The mic↔loopback alignment machinery (`MicBridge`, the drain fix, the capture-silence fix) carries
|
||||||
|
over unchanged — we just route the aligned samples to two channels instead of summing them.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### 1. Capture write path (`audio`)
|
||||||
|
- Force the recording spec to **2ch, 16-bit, native rate** when the mic is enabled ("split"
|
||||||
|
layout). Loopback-only (mic off) keeps today's behavior.
|
||||||
|
- New writer (replacing `write_wav_bytes`'s summing for split recordings): per frame,
|
||||||
|
**L = mic sample** (0 when the mic underran), **R = downmix(loopback channels)**.
|
||||||
|
- Idle path (loopback silent): **L = drained mic, R = 0** — same silence-synthesis + device-paced
|
||||||
|
drain we just fixed, now writing to L instead of summing.
|
||||||
|
|
||||||
|
### 2. Layout versioning (distinguish old "summed" from new "split")
|
||||||
|
Content alone can't tell a summed-stereo file from a split-stereo one, so mark it explicitly:
|
||||||
|
- Add `audio_layout TEXT` to the `meetings` row: `'summed'` (default / NULL for all existing rows
|
||||||
|
and imports) or `'split'` (new mic-enabled recordings).
|
||||||
|
- Include it in the **`MeetingBundle` manifest** so exported recordings stay interpretable on
|
||||||
|
another machine.
|
||||||
|
- Playback and diarization branch on it; old recordings keep every current code path untouched.
|
||||||
|
|
||||||
|
### 3. Read helpers (`audio`)
|
||||||
|
- `read_wav_mono_16k` — **unchanged** (downmix all channels → mono 16k). Used for transcription of
|
||||||
|
both layouts; on a split file it returns the summed signal, exactly as wanted.
|
||||||
|
- New `read_wav_channel_16k(path, channel)` — extract one channel → mono 16k. Diarization reads the
|
||||||
|
**right** channel (far side); "You" detection reads the **left** (mic).
|
||||||
|
|
||||||
|
### 4. Diarization / attribution (`commands`) — *simpler* than Phase 3
|
||||||
|
For a **split** recording, one path replaces the timeline + masking:
|
||||||
|
```
|
||||||
|
far_spans = diarizer.diarize_samples( read_wav_channel_16k(wav, RIGHT) ) // clean far side
|
||||||
|
you_spans = vad_spans( read_wav_channel_16k(wav, LEFT) ) // energy-windowed mic
|
||||||
|
merge + assign_by_overlap + build_name_map("You") // You + Speaker N
|
||||||
|
```
|
||||||
|
- The mic is **never** in the clustering input — structurally, not procedurally.
|
||||||
|
- `vad_spans` is the `MicActivity` windowing logic (energy floor + 100 ms windows) applied to the L
|
||||||
|
channel from the file — so **stop and reprocess share one path** and produce identical results.
|
||||||
|
- **This deletes `MicActivity`, `mic_activity.json` (persist + read + delete), and `mask_ranges`.**
|
||||||
|
Net: less code than the current Phase 3.
|
||||||
|
- Mic-off (`'summed'`) recordings keep the whole-signal pass + voiceprint fallback.
|
||||||
|
|
||||||
|
### 5. Playback (`commands::serve_recording`)
|
||||||
|
- A split file plays mic-in-left / computer-in-right — wrong for a listener. On playback, **downmix
|
||||||
|
L+R → mono** (dual-mono) so both sources come out both ears.
|
||||||
|
- `serve_recording` already reads+decrypts the whole file into memory; add a downmix-to-mono step
|
||||||
|
for `'split'` layout and serve that mono WAV (range/seek computed over the transcoded bytes).
|
||||||
|
`'summed'` files stream as today.
|
||||||
|
|
||||||
|
### 6. Migration & portability
|
||||||
|
- **Existing recordings** (`'summed'`): every current path unchanged. Their reprocess falls back to
|
||||||
|
the voiceprint match (acceptable — they predate split). We may retire the `MicActivity`/mask code
|
||||||
|
once split is default; the handful of recent test recordings lose Phase 3 on reprocess, which is
|
||||||
|
fine.
|
||||||
|
- **Bundle export**: export **downmixes the canonical split `audio.wav` to mono** so a shared file
|
||||||
|
plays normally in any player; the working file stays split for WA's own re-processing.
|
||||||
|
|
||||||
|
## What this removes / simplifies
|
||||||
|
|
||||||
|
| Removed | Replaced by |
|
||||||
|
|---|---|
|
||||||
|
| `MicActivity` struct + capture wiring | L-channel VAD read from the file |
|
||||||
|
| `mic_activity.json` (write/read/delete) | nothing — recompute from L channel |
|
||||||
|
| `mask_ranges` + masked diarize | diarize the R channel directly |
|
||||||
|
| stop vs reprocess divergence | one shared split-attribution path |
|
||||||
|
|
||||||
|
The capture-silence fix, the device-paced `drain`, `diarize_samples`, and the `build_name_map`
|
||||||
|
naming all **stay**.
|
||||||
|
|
||||||
|
## Costs / risks (none fatal)
|
||||||
|
- **New code:** split writer, `audio_layout` column + migration + manifest field, channel read
|
||||||
|
helper, playback downmix, `vad_spans`, and the split-vs-summed branch. Bigger than a patch, but
|
||||||
|
it *retires* comparable Phase 3 code.
|
||||||
|
- **Loopback stereo image lost** (R = loopback downmixed to mono) — irrelevant for speech.
|
||||||
|
- **Mono render endpoint** (rare): 1ch→2ch doubles that recording's size.
|
||||||
|
- **Raw file plays split** in a third-party player until exported (export downmixes) — minor.
|
||||||
|
- **Clock drift** between mic/loopback now shows as tiny L/R desync instead of dropped summed
|
||||||
|
samples — harmless (diarization tolerates it; playback sums it away).
|
||||||
|
|
||||||
|
## Open questions for sign-off
|
||||||
|
1. Layout marker: **DB column** (proposed) vs. a self-describing WAV chunk? DB column is simpler and
|
||||||
|
travels via the bundle manifest; recommend it.
|
||||||
|
2. Retire `MicActivity`/`mask_ranges` outright, or keep them for existing `'summed'` recordings'
|
||||||
|
reprocess? Recommend **retire** (summed → voiceprint fallback) to avoid two live systems.
|
||||||
|
3. Export downmix to mono — agree that's the right default for shared/synced copies?
|
||||||
|
|
||||||
|
## Implementation order
|
||||||
|
1. `audio_layout` column + `MeetingBundle` field + read on `get_meeting` (no behavior yet).
|
||||||
|
2. Split writer + force 2ch when mic on; set `audio_layout='split'`. (`audio` + `start_recording`.)
|
||||||
|
3. `read_wav_channel_16k` + `vad_spans`; split-attribution path in stop + reprocess.
|
||||||
|
4. Playback downmix for split; bundle-export downmix.
|
||||||
|
5. Delete `MicActivity` / `mic_activity.json` / `mask_ranges`; update tests.
|
||||||
|
6. Apply the ADR-0005 amendment + update `docs/02`/`03`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed ADR-0005 amendment (apply on approval)
|
||||||
|
|
||||||
|
> ### Phase 3.5 refinement — dual-channel capture (FR-SPK/FR-CAP, 2026-07-14)
|
||||||
|
>
|
||||||
|
> The Phase 3 per-stream attribution (above) reconstructs the mic/far-side split *after the fact*
|
||||||
|
> from a summed-mono `audio.wav` plus a `mic_activity.json` timeline, then masks the mic ranges
|
||||||
|
> before clustering. This is reliable at stop but fragile on reprocess (re-aligning a sidecar
|
||||||
|
> timeline against a mono mix), and it carries a parallel persistence path.
|
||||||
|
>
|
||||||
|
> **Decision:** when the microphone is enabled, record `audio.wav` as **stereo with the streams
|
||||||
|
> separated — left = microphone ("You"), right = system/loopback ("Speaker")** — instead of summing
|
||||||
|
> them. The separation is then intrinsic to the recording:
|
||||||
|
> - Diarization runs on the **right channel only** → `Speaker N`; "You" comes from **left-channel**
|
||||||
|
> voice activity. The mic is never clustered, by construction.
|
||||||
|
> - Reprocess recomputes both from the file — no sidecar timeline, no masking.
|
||||||
|
> - Transcription still downmixes (L+R) to the same summed mono it uses today; playback and bundle
|
||||||
|
> export downmix to mono so shared/played audio is normal.
|
||||||
|
> - A `meetings.audio_layout` flag (`summed` | `split`, in the bundle manifest for portability)
|
||||||
|
> distinguishes recordings; existing `summed` recordings keep the original blind-clustering +
|
||||||
|
> voiceprint path. On a stereo render endpoint this is **size-neutral** (the file was already 2ch
|
||||||
|
> with the mic summed into both).
|
||||||
|
>
|
||||||
|
> This **supersedes** `MicActivity` / `mic_activity.json` / masked diarization from Phase 3; the
|
||||||
|
> capture-silence handling, `diarize_samples`, and the `build_name_map` naming are retained. Segment
|
||||||
|
> IDs and the names-in-DB rule are unchanged.
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "whispassist",
|
"name": "whispassist",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.0",
|
"version": "0.7.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Privacy-first, fully local Windows meeting assistant.",
|
"description": "Privacy-first, fully local Windows meeting assistant.",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
|
|||||||
Generated
+57
-2
@@ -124,6 +124,17 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "auto-launch"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
|
||||||
|
dependencies = [
|
||||||
|
"dirs 4.0.0",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"winreg 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.1"
|
version = "1.5.1"
|
||||||
@@ -848,6 +859,15 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys 0.3.7",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "5.0.1"
|
version = "5.0.1"
|
||||||
@@ -866,6 +886,17 @@ dependencies = [
|
|||||||
"dirs-sys 0.5.0",
|
"dirs-sys 0.5.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"redox_users 0.4.6",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-sys"
|
name = "dirs-sys"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -1043,7 +1074,7 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
"toml 1.1.2+spec-1.1.0",
|
"toml 1.1.2+spec-1.1.0",
|
||||||
"vswhom",
|
"vswhom",
|
||||||
"winreg",
|
"winreg 0.55.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4935,6 +4966,20 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-autostart"
|
||||||
|
version = "2.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "459383cebc193cdd03d1ba4acc40f2c408a7abce419d64bdcd2d745bc2886f70"
|
||||||
|
dependencies = [
|
||||||
|
"auto-launch",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-dialog"
|
name = "tauri-plugin-dialog"
|
||||||
version = "2.7.1"
|
version = "2.7.1"
|
||||||
@@ -6043,7 +6088,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whispassist"
|
name = "whispassist"
|
||||||
version = "0.4.0"
|
version = "0.7.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -6073,6 +6118,7 @@ dependencies = [
|
|||||||
"sqlx",
|
"sqlx",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-autostart",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -6767,6 +6813,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winreg"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winreg"
|
name = "winreg"
|
||||||
version = "0.55.0"
|
version = "0.55.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "whispassist"
|
name = "whispassist"
|
||||||
version = "0.4.0"
|
version = "0.7.2"
|
||||||
description = "Privacy-first, fully local Windows meeting assistant"
|
description = "Privacy-first, fully local Windows meeting assistant"
|
||||||
authors = ["WhispAssist contributors"]
|
authors = ["WhispAssist contributors"]
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
@@ -81,6 +81,7 @@ ort = { version = "=2.0.0-rc.10", optional = true, default-features = false, fea
|
|||||||
rustfft = { version = "6", optional = true }
|
rustfft = { version = "6", optional = true }
|
||||||
sherpa-rs = { version = "0.6", optional = true, default-features = false, features = ["download-binaries"] } # sherpa-onnx bindings (Phase 4, ADR-0005)
|
sherpa-rs = { version = "0.6", optional = true, default-features = false, features = ["download-binaries"] } # sherpa-onnx bindings (Phase 4, ADR-0005)
|
||||||
tauri-plugin-dialog = "2" # native Save/choose-folder (Phase 2 export)
|
tauri-plugin-dialog = "2" # native Save/choose-folder (Phase 2 export)
|
||||||
|
tauri-plugin-autostart = "2" # opt-in launch-on-login (per-user HKCU\Run, no admin; NFR-RES-4)
|
||||||
|
|
||||||
# notes export (Phase 8, FR-NOTE-4) — pure-Rust, no external binary/cloud
|
# notes export (Phase 8, FR-NOTE-4) — pure-Rust, no external binary/cloud
|
||||||
# conversion service, consistent with the fully-local invariant.
|
# conversion service, consistent with the fully-local invariant.
|
||||||
@@ -99,6 +100,7 @@ windows = { version = "0.58", features = [
|
|||||||
"Win32_Devices_DeviceAndDriverInstallation", # SetupAPI: NPU detection (Phase 3, T3.4)
|
"Win32_Devices_DeviceAndDriverInstallation", # SetupAPI: NPU detection (Phase 3, T3.4)
|
||||||
"Win32_System_Com",
|
"Win32_System_Com",
|
||||||
"Win32_UI_Shell", # SetCurrentProcessExplicitAppUserModelID (Phase 8, T8.6)
|
"Win32_UI_Shell", # SetCurrentProcessExplicitAppUserModelID (Phase 8, T8.6)
|
||||||
|
"Win32_UI_WindowsAndMessaging", # MessageBoxW: fatal-startup error dialog (see run())
|
||||||
"UI_Notifications", # scheduled toast reminders (Phase 8, T8.6, FR-CAL-5) — the
|
"UI_Notifications", # scheduled toast reminders (Phase 8, T8.6, FR-CAL-5) — the
|
||||||
"Data_Xml_Dom", # OS delivers these itself at the due time, no polling timer
|
"Data_Xml_Dom", # OS delivers these itself at the due time, no polling timer
|
||||||
"Foundation",
|
"Foundation",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -176,6 +176,48 @@
|
|||||||
"Identifier": {
|
"Identifier": {
|
||||||
"description": "Permission identifier",
|
"description": "Permission identifier",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n\n#### This default permission set includes:\n\n- `allow-enable`\n- `allow-disable`\n- `allow-is-enabled`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "autostart:default",
|
||||||
|
"markdownDescription": "This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n\n#### This default permission set includes:\n\n- `allow-enable`\n- `allow-disable`\n- `allow-is-enabled`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the disable command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "autostart:allow-disable",
|
||||||
|
"markdownDescription": "Enables the disable command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the enable command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "autostart:allow-enable",
|
||||||
|
"markdownDescription": "Enables the enable command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the is_enabled command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "autostart:allow-is-enabled",
|
||||||
|
"markdownDescription": "Enables the is_enabled command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the disable command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "autostart:deny-disable",
|
||||||
|
"markdownDescription": "Denies the disable command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the enable command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "autostart:deny-enable",
|
||||||
|
"markdownDescription": "Denies the enable command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the is_enabled command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "autostart:deny-is-enabled",
|
||||||
|
"markdownDescription": "Denies the is_enabled command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
|
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -176,6 +176,48 @@
|
|||||||
"Identifier": {
|
"Identifier": {
|
||||||
"description": "Permission identifier",
|
"description": "Permission identifier",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n\n#### This default permission set includes:\n\n- `allow-enable`\n- `allow-disable`\n- `allow-is-enabled`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "autostart:default",
|
||||||
|
"markdownDescription": "This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n\n#### This default permission set includes:\n\n- `allow-enable`\n- `allow-disable`\n- `allow-is-enabled`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the disable command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "autostart:allow-disable",
|
||||||
|
"markdownDescription": "Enables the disable command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the enable command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "autostart:allow-enable",
|
||||||
|
"markdownDescription": "Enables the enable command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the is_enabled command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "autostart:allow-is-enabled",
|
||||||
|
"markdownDescription": "Enables the is_enabled command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the disable command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "autostart:deny-disable",
|
||||||
|
"markdownDescription": "Denies the disable command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the enable command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "autostart:deny-enable",
|
||||||
|
"markdownDescription": "Denies the enable command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the is_enabled command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "autostart:deny-is-enabled",
|
||||||
|
"markdownDescription": "Denies the is_enabled command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
|
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- WhispAssist dual-channel capture (FR-SPK/FR-CAP). Forward-only migration.
|
||||||
|
-- Mirrors docs/03-data-model.md.
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
|
||||||
|
-- How audio.wav stores its channels:
|
||||||
|
-- NULL / 'summed' — mic summed into every loopback channel (all pre-2026-07-14
|
||||||
|
-- recordings and every import): one mixed mono-equivalent.
|
||||||
|
-- 'split' — stereo, left = microphone ("You"), right = system/loopback
|
||||||
|
-- ("Speaker"), kept separate so diarization runs on the far
|
||||||
|
-- side alone. New mic-enabled recordings.
|
||||||
|
-- Diarization, playback (downmix), and bundle export branch on this.
|
||||||
|
ALTER TABLE meetings ADD COLUMN audio_layout TEXT;
|
||||||
+346
-27
@@ -45,9 +45,26 @@ pub enum AudioError {
|
|||||||
pub struct CaptureHandle {
|
pub struct CaptureHandle {
|
||||||
running: Arc<AtomicBool>,
|
running: Arc<AtomicBool>,
|
||||||
paused: Arc<AtomicBool>,
|
paused: Arc<AtomicBool>,
|
||||||
|
/// Mic-only (FR-CAP-7): when set, the microphone stream emits silence instead
|
||||||
|
/// of real samples — the recording's mic-left channel and the live transcript
|
||||||
|
/// go quiet, the meter drops to zero, while loopback keeps recording. Toggled
|
||||||
|
/// live via `set_muted` (the "press M to mute" control).
|
||||||
|
muted: Arc<AtomicBool>,
|
||||||
thread: JoinHandle<Result<CaptureSummary, AudioError>>,
|
thread: JoinHandle<Result<CaptureSummary, AudioError>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CaptureHandle {
|
||||||
|
/// Mute/unmute this stream live. Only meaningful for the microphone capture.
|
||||||
|
pub fn set_muted(&self, muted: bool) {
|
||||||
|
self.muted.store(muted, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this stream is currently muted.
|
||||||
|
pub fn is_muted(&self) -> bool {
|
||||||
|
self.muted.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Where captured frames are delivered for live transcription: mono f32 @ 16kHz,
|
/// Where captured frames are delivered for live transcription: mono f32 @ 16kHz,
|
||||||
/// bounded so a slow/absent consumer can never stall the capture thread.
|
/// bounded so a slow/absent consumer can never stall the capture thread.
|
||||||
pub type FrameSink = SyncSender<Vec<f32>>;
|
pub type FrameSink = SyncSender<Vec<f32>>;
|
||||||
@@ -201,16 +218,37 @@ impl MicBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loopback idle path: take everything buffered at once, device-paced. The
|
||||||
|
/// idle writer must NOT meter the mic by wall-clock — that lets the buffer
|
||||||
|
/// back up to `cap` and drop the oldest samples, which sounds like the audio
|
||||||
|
/// skipping/fast-forwarding late in a long silent stretch. Draining fully
|
||||||
|
/// keeps the mic flowing at its own clock.
|
||||||
|
fn drain(&self) -> Vec<f32> {
|
||||||
|
self.buf
|
||||||
|
.lock()
|
||||||
|
.map(|mut buf| buf.drain(..).collect())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Below this per-chunk RMS the mic is treated as silence/room noise and not
|
||||||
|
/// collected into a voiceprint. Without it the voiceprint was just the first
|
||||||
|
/// few seconds of the recording regardless of whether the user had spoken —
|
||||||
|
/// so a user silent for the first 30s got a noise voiceprint that spuriously
|
||||||
|
/// matched a cluster and mislabeled everyone "You". ponytail: fixed floor for
|
||||||
|
/// normalized [-1,1] speech; promote to a calibration knob if real mics vary.
|
||||||
|
const VOICE_ENERGY_FLOOR: f32 = 0.01;
|
||||||
|
|
||||||
/// A one-shot, bounded capture of raw mic-only audio (16kHz mono, same format
|
/// A one-shot, bounded capture of raw mic-only audio (16kHz mono, same format
|
||||||
/// the transcriber and diarizer both use) taken early in a recording — enough
|
/// the transcriber and diarizer both use) taken from the user's early *speech*
|
||||||
/// to compute a voiceprint that identifies which diarized speaker cluster is
|
/// — enough to compute a voiceprint that identifies which diarized speaker
|
||||||
/// the mic (so it can be labeled "You" instead of a clustered "S1"/"S2"; see
|
/// cluster is the mic (so it can be labeled "You" instead of a clustered
|
||||||
/// `diarization::voiceprint`). Unlike `MicBridge`, this is filled once and
|
/// "S1"/"S2"; see `diarization::voiceprint`). Unlike `MicBridge`, this is filled
|
||||||
/// never drained: the first `cap` samples are kept and everything after is
|
/// once and never drained: the first `cap` samples of speech-level audio are
|
||||||
/// dropped, since a voiceprint only needs a few seconds of real speech, not
|
/// kept and everything after is dropped, since a voiceprint only needs a few
|
||||||
/// the whole meeting.
|
/// seconds of real speech, not the whole meeting. Silence is skipped so the
|
||||||
|
/// window tracks when the user actually talks, not the wall clock.
|
||||||
pub struct VoiceSample {
|
pub struct VoiceSample {
|
||||||
cap: usize,
|
cap: usize,
|
||||||
buf: Mutex<Vec<f32>>,
|
buf: Mutex<Vec<f32>>,
|
||||||
@@ -225,6 +263,18 @@ impl VoiceSample {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn push(&self, samples: &[f32]) {
|
fn push(&self, samples: &[f32]) {
|
||||||
|
// Skip silence/room noise so the voiceprint is the user's voice, not
|
||||||
|
// whatever the mic picked up before they first spoke (see
|
||||||
|
// VOICE_ENERGY_FLOOR). RMS inline so VoiceSample stays independent of
|
||||||
|
// the `audio`-gated level helper.
|
||||||
|
let rms = if samples.is_empty() {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(samples.iter().map(|s| s * s).sum::<f32>() / samples.len() as f32).sqrt()
|
||||||
|
};
|
||||||
|
if rms < VOICE_ENERGY_FLOOR {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Ok(mut buf) = self.buf.lock() {
|
if let Ok(mut buf) = self.buf.lock() {
|
||||||
if buf.len() < self.cap {
|
if buf.len() < self.cap {
|
||||||
buf.extend_from_slice(samples);
|
buf.extend_from_slice(samples);
|
||||||
@@ -240,6 +290,12 @@ impl VoiceSample {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MicActivity (the live mic-speech timeline) was retired 2026-07-14 with the
|
||||||
|
// masked-mono diarization it fed — the split (mic-left/loopback-right) layout
|
||||||
|
// makes the mic/far-side separation intrinsic to audio.wav, so "You" is
|
||||||
|
// recomputed from the left channel via `vad_spans`. See the retirement note in
|
||||||
|
// commands.rs (attribute_split).
|
||||||
|
|
||||||
/// Number of audio frames in a raw WASAPI byte buffer of the given format.
|
/// Number of audio frames in a raw WASAPI byte buffer of the given format.
|
||||||
#[cfg(feature = "audio")]
|
#[cfg(feature = "audio")]
|
||||||
fn frame_count(bytes: &[u8], format: &WaveFormat) -> usize {
|
fn frame_count(bytes: &[u8], format: &WaveFormat) -> usize {
|
||||||
@@ -273,12 +329,15 @@ impl WasapiCapture {
|
|||||||
event_sink: EventSink,
|
event_sink: EventSink,
|
||||||
bridge: Option<Arc<MicBridge>>,
|
bridge: Option<Arc<MicBridge>>,
|
||||||
voice_sample: Option<Arc<VoiceSample>>,
|
voice_sample: Option<Arc<VoiceSample>>,
|
||||||
|
split: bool,
|
||||||
emit_level: bool,
|
emit_level: bool,
|
||||||
) -> Result<CaptureHandle, AudioError> {
|
) -> Result<CaptureHandle, AudioError> {
|
||||||
let running = Arc::new(AtomicBool::new(true));
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
let paused = Arc::new(AtomicBool::new(false));
|
let paused = Arc::new(AtomicBool::new(false));
|
||||||
|
let muted = Arc::new(AtomicBool::new(false));
|
||||||
let running_th = running.clone();
|
let running_th = running.clone();
|
||||||
let paused_th = paused.clone();
|
let paused_th = paused.clone();
|
||||||
|
let muted_th = muted.clone();
|
||||||
let wav_path = wav_path.map(Path::to_path_buf);
|
let wav_path = wav_path.map(Path::to_path_buf);
|
||||||
let device_id = device_id.map(str::to_string);
|
let device_id = device_id.map(str::to_string);
|
||||||
|
|
||||||
@@ -293,8 +352,10 @@ impl WasapiCapture {
|
|||||||
&event_sink,
|
&event_sink,
|
||||||
&running_th,
|
&running_th,
|
||||||
&paused_th,
|
&paused_th,
|
||||||
|
&muted_th,
|
||||||
bridge.as_ref(),
|
bridge.as_ref(),
|
||||||
voice_sample.as_ref(),
|
voice_sample.as_ref(),
|
||||||
|
split,
|
||||||
emit_level,
|
emit_level,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -303,6 +364,7 @@ impl WasapiCapture {
|
|||||||
Ok(CaptureHandle {
|
Ok(CaptureHandle {
|
||||||
running,
|
running,
|
||||||
paused,
|
paused,
|
||||||
|
muted,
|
||||||
thread,
|
thread,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -327,7 +389,8 @@ impl WasapiCapture {
|
|||||||
event_sink,
|
event_sink,
|
||||||
Some(bridge),
|
Some(bridge),
|
||||||
None,
|
None,
|
||||||
true,
|
true, // split: mic-enabled loopback records mic-left / loopback-right
|
||||||
|
true, // emit_level
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,7 +415,8 @@ impl WasapiCapture {
|
|||||||
event_sink,
|
event_sink,
|
||||||
Some(bridge),
|
Some(bridge),
|
||||||
voice_sample,
|
voice_sample,
|
||||||
true,
|
false, // split: mic stream has no writer
|
||||||
|
true, // emit_level
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,7 +439,8 @@ impl AudioCapture for WasapiCapture {
|
|||||||
event_sink,
|
event_sink,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
true,
|
false, // split: loopback-only / mic stream — no split writer
|
||||||
|
true, // emit_level
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,7 +459,8 @@ impl AudioCapture for WasapiCapture {
|
|||||||
event_sink,
|
event_sink,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
true,
|
false, // split: loopback-only / mic stream — no split writer
|
||||||
|
true, // emit_level
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,6 +606,18 @@ fn audio_level(mono: &[f32], mic: bool) -> AudioLevel {
|
|||||||
/// without flooding the frontend with an event per ~10ms WASAPI callback.
|
/// without flooding the frontend with an event per ~10ms WASAPI callback.
|
||||||
const LEVEL_EMIT_INTERVAL: Duration = Duration::from_millis(50);
|
const LEVEL_EMIT_INTERVAL: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
|
/// When the loopback (render) endpoint is idle, WASAPI delivers no packets, so
|
||||||
|
/// the recording would stall and the bridged mic — the user talking over system
|
||||||
|
/// silence — would be lost as the MicBridge overflows. The loopback loop then
|
||||||
|
/// synthesizes silence for the elapsed gap and folds the mic in, keeping
|
||||||
|
/// `audio.wav` continuous and complete (FR-CAP-7). MIN gates on a *sustained*
|
||||||
|
/// gap so brief inter-packet stalls during active playback don't punch silence
|
||||||
|
/// into the loopback (audible clicks); it stays well under the MicBridge's
|
||||||
|
/// ~0.5s cap so the mic can't back up and drop samples between drains. MAX caps
|
||||||
|
/// the silence padding written in one chunk.
|
||||||
|
const SILENCE_TICK_MIN: Duration = Duration::from_millis(150);
|
||||||
|
const SILENCE_TICK_MAX: Duration = Duration::from_millis(500);
|
||||||
|
|
||||||
/// Runs on a dedicated OS thread for the lifetime of a `CaptureHandle`. Owns the
|
/// Runs on a dedicated OS thread for the lifetime of a `CaptureHandle`. Owns the
|
||||||
/// WASAPI client and the WAV writer; exits (and finalizes the WAV) once `running`
|
/// WASAPI client and the WAV writer; exits (and finalizes the WAV) once `running`
|
||||||
/// is cleared.
|
/// is cleared.
|
||||||
@@ -553,8 +631,15 @@ fn capture_loop(
|
|||||||
event_sink: &EventSink,
|
event_sink: &EventSink,
|
||||||
running: &AtomicBool,
|
running: &AtomicBool,
|
||||||
paused: &AtomicBool,
|
paused: &AtomicBool,
|
||||||
|
// Mic-only live mute (FR-CAP-7): zeroes the decoded mic samples so the
|
||||||
|
// recording, transcript, and meter all go silent while loopback continues.
|
||||||
|
muted: &AtomicBool,
|
||||||
bridge: Option<&Arc<MicBridge>>,
|
bridge: Option<&Arc<MicBridge>>,
|
||||||
voice_sample: Option<&Arc<VoiceSample>>,
|
voice_sample: Option<&Arc<VoiceSample>>,
|
||||||
|
// FR-SPK: when true, the loopback WAV is stereo L=mic / R=loopback (the mic
|
||||||
|
// is a separate track, not summed in) so diarization can run on the far side
|
||||||
|
// alone. Only meaningful for the loopback writer.
|
||||||
|
split: bool,
|
||||||
emit_level: bool,
|
emit_level: bool,
|
||||||
) -> Result<CaptureSummary, AudioError> {
|
) -> Result<CaptureSummary, AudioError> {
|
||||||
wasapi::initialize_mta()
|
wasapi::initialize_mta()
|
||||||
@@ -566,14 +651,18 @@ fn capture_loop(
|
|||||||
// UI can overlay them — but only the loopback stream writes a WAV.
|
// UI can overlay them — but only the loopback stream writes a WAV.
|
||||||
let mut session = open_capture_session(&direction, device_id)?;
|
let mut session = open_capture_session(&direction, device_id)?;
|
||||||
// Loopback publishes its rate so the mic knows what to resample to before
|
// Loopback publishes its rate so the mic knows what to resample to before
|
||||||
// pushing into the shared bridge (mic-into-recording, FR-CAP-7).
|
// pushing into the shared bridge (mic-into-recording, FR-CAP-7), and so the
|
||||||
|
// Phase 3 mic-activity timeline maps frame positions to windows.
|
||||||
if is_loopback {
|
if is_loopback {
|
||||||
if let Some(b) = bridge {
|
if let Some(b) = bridge {
|
||||||
b.rate
|
b.rate
|
||||||
.store(session.format.get_samplespersec(), Ordering::Relaxed);
|
.store(session.format.get_samplespersec(), Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let spec = wav_spec_for(&session.format)?;
|
let mut spec = wav_spec_for(&session.format)?;
|
||||||
|
if split && is_loopback {
|
||||||
|
spec.channels = 2; // L = mic, R = loopback (downmixed)
|
||||||
|
}
|
||||||
let mut writer = match wav_path {
|
let mut writer = match wav_path {
|
||||||
Some(path) => Some(WavWriter::create(path, spec).map_err(|e| {
|
Some(path) => Some(WavWriter::create(path, spec).map_err(|e| {
|
||||||
AudioError::Capture(format!("could not create {}: {e}", path.display()))
|
AudioError::Capture(format!("could not create {}: {e}", path.display()))
|
||||||
@@ -593,6 +682,9 @@ fn capture_loop(
|
|||||||
let mut queue: std::collections::VecDeque<u8> = std::collections::VecDeque::new();
|
let mut queue: std::collections::VecDeque<u8> = std::collections::VecDeque::new();
|
||||||
let mut frames_written: u64 = 0;
|
let mut frames_written: u64 = 0;
|
||||||
let mut last_level_emit = Instant::now() - LEVEL_EMIT_INTERVAL;
|
let mut last_level_emit = Instant::now() - LEVEL_EMIT_INTERVAL;
|
||||||
|
// Wall-clock of the last WAV write (real or synthesized), so the loopback
|
||||||
|
// loop can fill idle gaps with silence+mic instead of stalling.
|
||||||
|
let mut last_write = Instant::now();
|
||||||
|
|
||||||
while running.load(Ordering::Relaxed) {
|
while running.load(Ordering::Relaxed) {
|
||||||
// Short timeout so we periodically re-check `running` even with no data.
|
// Short timeout so we periodically re-check `running` even with no data.
|
||||||
@@ -636,8 +728,43 @@ fn capture_loop(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if queue.is_empty() {
|
if queue.is_empty() {
|
||||||
|
// Loopback idle (nothing playing): keep audio.wav advancing so the
|
||||||
|
// bridged mic (user talking over system silence) is preserved
|
||||||
|
// instead of lost to MicBridge overflow. Only after a *sustained*
|
||||||
|
// gap, so brief inter-packet stalls during active playback don't
|
||||||
|
// punch silence into the loopback (clicks). Loopback + mic-enabled
|
||||||
|
// only; the mic capture stream has no writer.
|
||||||
|
if is_loopback && !paused.load(Ordering::Relaxed) {
|
||||||
|
if let (Some(w), Some(b)) = (writer.as_mut(), bridge) {
|
||||||
|
let elapsed = last_write.elapsed();
|
||||||
|
if elapsed >= SILENCE_TICK_MIN {
|
||||||
|
// Drain the whole mic buffer (device-paced). Metering the
|
||||||
|
// pull by wall-clock let the buffer back up to its cap and
|
||||||
|
// drop the oldest samples — the audio skipped/fast-
|
||||||
|
// forwarded late in a long gap. Draining fully avoids that.
|
||||||
|
let mic = b.drain();
|
||||||
|
let rate = session.format.get_samplespersec() as usize;
|
||||||
|
// Advance the timeline by the wall-clock gap (capped), but
|
||||||
|
// never fewer frames than the mic we must write.
|
||||||
|
let gap_frames = (elapsed.min(SILENCE_TICK_MAX).as_micros() as usize
|
||||||
|
* rate
|
||||||
|
/ 1_000_000)
|
||||||
|
.max(mic.len());
|
||||||
|
let bpf = session.format.get_nchannels() as usize
|
||||||
|
* (session.format.get_bitspersample() as usize / 8);
|
||||||
|
let silence = vec![0u8; gap_frames * bpf];
|
||||||
|
frames_written += if split {
|
||||||
|
write_wav_split(w, &silence, &session.format, &mic)?
|
||||||
|
} else {
|
||||||
|
write_wav_bytes(w, &silence, &session.format, &mic)?
|
||||||
|
};
|
||||||
|
last_write = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
last_write = Instant::now();
|
||||||
let bytes: Vec<u8> = queue.drain(..).collect();
|
let bytes: Vec<u8> = queue.drain(..).collect();
|
||||||
|
|
||||||
// Must keep pulling WASAPI buffers even while paused (required to avoid
|
// Must keep pulling WASAPI buffers even while paused (required to avoid
|
||||||
@@ -653,9 +780,20 @@ fn capture_loop(
|
|||||||
Some(b) if is_loopback => b.pull(frame_count(&bytes, &session.format)),
|
Some(b) if is_loopback => b.pull(frame_count(&bytes, &session.format)),
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
};
|
};
|
||||||
frames_written += write_wav_bytes(w, &bytes, &session.format, &mic)?;
|
frames_written += if split {
|
||||||
|
write_wav_split(w, &bytes, &session.format, &mic)?
|
||||||
|
} else {
|
||||||
|
write_wav_bytes(w, &bytes, &session.format, &mic)?
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let mut mono = decode_mono_f32(&bytes, &session.format)?;
|
||||||
|
// Mic muted: replace the decoded samples with silence before anything
|
||||||
|
// downstream sees them — the recording's mic channel, the bridge, the
|
||||||
|
// transcript feed, the meter, and the voiceprint sample all go quiet.
|
||||||
|
// Loopback (`is_loopback`) is never muted this way.
|
||||||
|
if !is_loopback && muted.load(Ordering::Relaxed) {
|
||||||
|
mono.iter_mut().for_each(|s| *s = 0.0);
|
||||||
}
|
}
|
||||||
let mono = decode_mono_f32(&bytes, &session.format)?;
|
|
||||||
|
|
||||||
// Mic: feed the shared bridge (resampled to the loopback's rate) so the
|
// Mic: feed the shared bridge (resampled to the loopback's rate) so the
|
||||||
// loopback thread can fold it into the recording.
|
// loopback thread can fold it into the recording.
|
||||||
@@ -783,6 +921,53 @@ fn f32_to_i16(v: f32) -> i16 {
|
|||||||
(v.clamp(-1.0, 1.0) * i16::MAX as f32) as i16
|
(v.clamp(-1.0, 1.0) * i16::MAX as f32) as i16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Split layout (FR-SPK): write frames as **stereo, L = mic, R = loopback**
|
||||||
|
/// (downmixed to mono), so the two sources stay separate in `audio.wav` and
|
||||||
|
/// diarization can run on the far side alone. `loopback_bytes` is the raw WASAPI
|
||||||
|
/// mix buffer (all-zero for a synthesized-silence chunk); `mic` may be shorter
|
||||||
|
/// than the frame count (rest silent) or empty (mic underran). Returns frames.
|
||||||
|
fn write_wav_split(
|
||||||
|
writer: &mut WavWriter<BufWriter<File>>,
|
||||||
|
loopback_bytes: &[u8],
|
||||||
|
format: &WaveFormat,
|
||||||
|
mic: &[f32],
|
||||||
|
) -> Result<u64, AudioError> {
|
||||||
|
let sample_type = format
|
||||||
|
.get_subformat()
|
||||||
|
.map_err(|e| AudioError::Device(format!("unrecognized mix format: {e}")))?;
|
||||||
|
let channels = format.get_nchannels() as usize;
|
||||||
|
if channels == 0 {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let (bytes_per_sample, decode): (usize, fn(&[u8]) -> f32) = match (sample_type, format.get_bitspersample()) {
|
||||||
|
(SampleType::Float, 32) => (4, |c| f32::from_le_bytes(c.try_into().unwrap())),
|
||||||
|
(SampleType::Int, 16) => (2, |c| {
|
||||||
|
i16::from_le_bytes(c.try_into().unwrap()) as f32 / i16::MAX as f32
|
||||||
|
}),
|
||||||
|
(st, bits) => {
|
||||||
|
return Err(AudioError::Device(format!(
|
||||||
|
"unsupported capture format: {st} {bits}-bit"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut frames = 0u64;
|
||||||
|
for frame in loopback_bytes.chunks_exact(bytes_per_sample * channels) {
|
||||||
|
// R = loopback downmixed to mono.
|
||||||
|
let loop_mono =
|
||||||
|
frame.chunks_exact(bytes_per_sample).map(decode).sum::<f32>() / channels as f32;
|
||||||
|
// L = mic for this frame (silent if the mic underran).
|
||||||
|
let mic_s = mic.get(frames as usize).copied().unwrap_or(0.0);
|
||||||
|
writer
|
||||||
|
.write_sample(f32_to_i16(mic_s))
|
||||||
|
.map_err(|e| AudioError::Capture(format!("wav write: {e}")))?;
|
||||||
|
writer
|
||||||
|
.write_sample(f32_to_i16(loop_mono))
|
||||||
|
.map_err(|e| AudioError::Capture(format!("wav write: {e}")))?;
|
||||||
|
frames += 1;
|
||||||
|
}
|
||||||
|
Ok(frames)
|
||||||
|
}
|
||||||
|
|
||||||
/// Downmix raw WASAPI capture bytes to mono `f32` in `[-1.0, 1.0]`, at the
|
/// Downmix raw WASAPI capture bytes to mono `f32` in `[-1.0, 1.0]`, at the
|
||||||
/// device's native sample rate (resampling to 16kHz happens separately).
|
/// device's native sample rate (resampling to 16kHz happens separately).
|
||||||
fn decode_mono_f32(bytes: &[u8], format: &WaveFormat) -> Result<Vec<f32>, AudioError> {
|
fn decode_mono_f32(bytes: &[u8], format: &WaveFormat) -> Result<Vec<f32>, AudioError> {
|
||||||
@@ -897,6 +1082,22 @@ impl Resampler {
|
|||||||
/// through hound's size-aware iterator; hound is only used to parse the
|
/// through hound's size-aware iterator; hound is only used to parse the
|
||||||
/// format/`fmt ` chunk.
|
/// format/`fmt ` chunk.
|
||||||
pub fn read_wav_mono_16k(path: &Path) -> Result<Vec<f32>, AudioError> {
|
pub fn read_wav_mono_16k(path: &Path) -> Result<Vec<f32>, AudioError> {
|
||||||
|
read_wav_16k(path, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`read_wav_mono_16k`] but extracts a single channel instead of
|
||||||
|
/// downmixing — used by the Phase 3.5 split path to diarize the right
|
||||||
|
/// (loopback) channel and VAD the left (mic) channel (FR-SPK). `channel` is
|
||||||
|
/// clamped to the file's last channel, so it's safe on a mono/summed file.
|
||||||
|
pub fn read_wav_channel_16k(path: &Path, channel: usize) -> Result<Vec<f32>, AudioError> {
|
||||||
|
read_wav_16k(path, Some(channel))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared reader: decrypt (T8.8), reduce each frame to one f32 — averaging all
|
||||||
|
/// channels (`channel = None`) or picking one (`channel = Some`) — resample to
|
||||||
|
/// 16kHz. Reads the `data` payload as raw bytes (an unfinalized WAV's size
|
||||||
|
/// fields read back as 0), so it works on a mid-write / crash-left file (FR-REL-1).
|
||||||
|
fn read_wav_16k(path: &Path, channel: Option<usize>) -> Result<Vec<f32>, AudioError> {
|
||||||
let raw = std::fs::read(path).map_err(|e| AudioError::Capture(format!("read wav: {e}")))?;
|
let raw = std::fs::read(path).map_err(|e| AudioError::Capture(format!("read wav: {e}")))?;
|
||||||
// Decrypt if the recording was sealed at rest (T8.8); plaintext passes through.
|
// Decrypt if the recording was sealed at rest (T8.8); plaintext passes through.
|
||||||
let file =
|
let file =
|
||||||
@@ -906,6 +1107,8 @@ pub fn read_wav_mono_16k(path: &Path) -> Result<Vec<f32>, AudioError> {
|
|||||||
.map_err(|e| AudioError::Capture(format!("open wav: {e}")))?
|
.map_err(|e| AudioError::Capture(format!("open wav: {e}")))?
|
||||||
.spec();
|
.spec();
|
||||||
let channels = spec.channels.max(1) as usize;
|
let channels = spec.channels.max(1) as usize;
|
||||||
|
// Which channel index to pick (clamped), or None to downmix.
|
||||||
|
let pick = channel.map(|c| c.min(channels - 1));
|
||||||
|
|
||||||
let data_marker = file
|
let data_marker = file
|
||||||
.windows(4)
|
.windows(4)
|
||||||
@@ -917,22 +1120,14 @@ pub fn read_wav_mono_16k(path: &Path) -> Result<Vec<f32>, AudioError> {
|
|||||||
let mono: Vec<f32> = match (spec.sample_format, spec.bits_per_sample) {
|
let mono: Vec<f32> = match (spec.sample_format, spec.bits_per_sample) {
|
||||||
(SampleFormat::Float, 32) => bytes
|
(SampleFormat::Float, 32) => bytes
|
||||||
.chunks_exact(4 * channels)
|
.chunks_exact(4 * channels)
|
||||||
.map(|frame| {
|
.map(|frame| reduce_frame(frame, 4, pick, channels, |c| f32::from_le_bytes(c.try_into().unwrap())))
|
||||||
frame
|
|
||||||
.chunks_exact(4)
|
|
||||||
.map(|c| f32::from_le_bytes(c.try_into().unwrap()))
|
|
||||||
.sum::<f32>()
|
|
||||||
/ channels as f32
|
|
||||||
})
|
|
||||||
.collect(),
|
.collect(),
|
||||||
(SampleFormat::Int, 16) => bytes
|
(SampleFormat::Int, 16) => bytes
|
||||||
.chunks_exact(2 * channels)
|
.chunks_exact(2 * channels)
|
||||||
.map(|frame| {
|
.map(|frame| {
|
||||||
frame
|
reduce_frame(frame, 2, pick, channels, |c| {
|
||||||
.chunks_exact(2)
|
i16::from_le_bytes(c.try_into().unwrap()) as f32 / i16::MAX as f32
|
||||||
.map(|c| i16::from_le_bytes(c.try_into().unwrap()) as f32 / i16::MAX as f32)
|
})
|
||||||
.sum::<f32>()
|
|
||||||
/ channels as f32
|
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
(fmt, bits) => {
|
(fmt, bits) => {
|
||||||
@@ -946,6 +1141,78 @@ pub fn read_wav_mono_16k(path: &Path) -> Result<Vec<f32>, AudioError> {
|
|||||||
Ok(resampler.process(&mono))
|
Ok(resampler.process(&mono))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Voice-activity "You" spans (FR-SPK): 100ms windows of 16kHz mono audio above
|
||||||
|
/// [`VOICE_ENERGY_FLOOR`], merged into `(start_ms, end_ms)` ranges. Run on the
|
||||||
|
/// left (mic) channel of a split recording to attribute segments to "You" —
|
||||||
|
/// replaces the old live `MicActivity` timeline (recomputed from the file, so
|
||||||
|
/// stop and reprocess agree).
|
||||||
|
pub fn vad_spans(samples: &[f32]) -> Vec<(u64, u64)> {
|
||||||
|
const WINDOW_MS: u64 = 100;
|
||||||
|
let win = (TARGET_SAMPLE_RATE as u64 * WINDOW_MS / 1000) as usize; // 1600 @16kHz
|
||||||
|
if win == 0 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let mut spans = Vec::new();
|
||||||
|
let mut start: Option<u64> = None;
|
||||||
|
let mut w = 0u64;
|
||||||
|
let mut i = 0usize;
|
||||||
|
while i < samples.len() {
|
||||||
|
let chunk = &samples[i..(i + win).min(samples.len())];
|
||||||
|
let rms = (chunk.iter().map(|s| s * s).sum::<f32>() / chunk.len().max(1) as f32).sqrt();
|
||||||
|
match (rms >= VOICE_ENERGY_FLOOR, start) {
|
||||||
|
(true, None) => start = Some(w * WINDOW_MS),
|
||||||
|
(false, Some(s)) => {
|
||||||
|
spans.push((s, w * WINDOW_MS));
|
||||||
|
start = None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
i += win;
|
||||||
|
w += 1;
|
||||||
|
}
|
||||||
|
if let Some(s) = start {
|
||||||
|
spans.push((s, w * WINDOW_MS));
|
||||||
|
}
|
||||||
|
spans
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reduce one interleaved frame to a single f32: pick channel `pick` or average
|
||||||
|
/// all `channels`. `width` is bytes-per-sample; `decode` turns those bytes → f32.
|
||||||
|
fn reduce_frame(
|
||||||
|
frame: &[u8],
|
||||||
|
width: usize,
|
||||||
|
pick: Option<usize>,
|
||||||
|
channels: usize,
|
||||||
|
decode: impl Fn(&[u8]) -> f32,
|
||||||
|
) -> f32 {
|
||||||
|
match pick {
|
||||||
|
Some(c) => decode(&frame[c * width..c * width + width]),
|
||||||
|
None => frame.chunks_exact(width).map(decode).sum::<f32>() / channels as f32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write mono 16kHz `f32` samples as a Float32 WAV — the format
|
||||||
|
/// [`read_wav_mono_16k`] reads back. Used to persist the mic voiceprint sample
|
||||||
|
/// next to a retained recording so it can be re-matched on reprocess (FR-SPK).
|
||||||
|
pub fn write_wav_mono_16k(path: &Path, samples: &[f32]) -> Result<(), AudioError> {
|
||||||
|
let spec = WavSpec {
|
||||||
|
channels: 1,
|
||||||
|
sample_rate: 16_000,
|
||||||
|
bits_per_sample: 32,
|
||||||
|
sample_format: SampleFormat::Float,
|
||||||
|
};
|
||||||
|
let mut writer =
|
||||||
|
WavWriter::create(path, spec).map_err(|e| AudioError::Capture(format!("write wav: {e}")))?;
|
||||||
|
for &s in samples {
|
||||||
|
writer
|
||||||
|
.write_sample(s)
|
||||||
|
.map_err(|e| AudioError::Capture(format!("write wav: {e}")))?;
|
||||||
|
}
|
||||||
|
writer
|
||||||
|
.finalize()
|
||||||
|
.map_err(|e| AudioError::Capture(format!("write wav: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
/// Ceiling on how far the still-flowing stream may run ahead of a stalled
|
/// Ceiling on how far the still-flowing stream may run ahead of a stalled
|
||||||
/// partner before the mixer forwards it alone — so a muted/dead microphone (or a
|
/// partner before the mixer forwards it alone — so a muted/dead microphone (or a
|
||||||
/// silent system output) can't hold up the live transcript. 0.5s @ 16kHz.
|
/// silent system output) can't hold up the live transcript. 0.5s @ 16kHz.
|
||||||
@@ -1051,6 +1318,31 @@ fn mixer_loop(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vad_spans_ignores_silence() {
|
||||||
|
// 200ms of 16kHz silence → no "You" spans.
|
||||||
|
assert!(vad_spans(&[0.0; 3_200]).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vad_spans_collapses_speech_into_spans_with_gaps() {
|
||||||
|
// 16kHz: 1600 samples per 100ms window. Speech in windows 0,1 (0-200ms)
|
||||||
|
// and 4,5 (400-600ms), silence between.
|
||||||
|
let mut samples = vec![0.0f32; 9_600]; // 600ms
|
||||||
|
samples[0..3_200].fill(0.2); // windows 0,1
|
||||||
|
samples[6_400..9_600].fill(0.2); // windows 4,5
|
||||||
|
assert_eq!(vad_spans(&samples), vec![(0, 200), (400, 600)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn voice_sample_skips_silence_and_keeps_speech() {
|
||||||
|
let vs = VoiceSample::new(16_000);
|
||||||
|
vs.push(&[0.0; 4_000]); // silence: below the energy floor
|
||||||
|
assert!(vs.samples().is_empty(), "silence must not fill the voiceprint");
|
||||||
|
vs.push(&[0.2; 4_000]); // speech-level audio
|
||||||
|
assert_eq!(vs.samples().len(), 4_000, "speech-level audio is collected");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mixer_sums_aligned_streams_and_keeps_the_unmatched_tail() {
|
fn mixer_sums_aligned_streams_and_keeps_the_unmatched_tail() {
|
||||||
let mut m = Mixer::new();
|
let mut m = Mixer::new();
|
||||||
@@ -1127,6 +1419,33 @@ mod tests {
|
|||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_wav_split_puts_mic_left_and_loopback_right() {
|
||||||
|
let dir = std::env::temp_dir().join(format!("wa-test-{}", uuid::Uuid::new_v4()));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let path = dir.join("audio.wav");
|
||||||
|
// 2ch float loopback input; split output is always 2ch 16-bit.
|
||||||
|
let format = WaveFormat::new(32, 32, &SampleType::Float, 48_000, 2, None);
|
||||||
|
let spec = WavSpec {
|
||||||
|
channels: 2,
|
||||||
|
sample_rate: 48_000,
|
||||||
|
bits_per_sample: 16,
|
||||||
|
sample_format: SampleFormat::Int,
|
||||||
|
};
|
||||||
|
let mut writer = WavWriter::create(&path, spec).unwrap();
|
||||||
|
// One 2ch loopback frame [0.4, 0.6] → R = downmix 0.5; mic 0.2 → L.
|
||||||
|
let loop_bytes: Vec<u8> = [0.4f32, 0.6].iter().flat_map(|s| s.to_le_bytes()).collect();
|
||||||
|
let n = write_wav_split(&mut writer, &loop_bytes, &format, &[0.2]).unwrap();
|
||||||
|
writer.finalize().unwrap();
|
||||||
|
assert_eq!(n, 1);
|
||||||
|
|
||||||
|
let mut reader = hound::WavReader::open(&path).unwrap();
|
||||||
|
let s: Vec<i16> = reader.samples::<i16>().map(|x| x.unwrap()).collect();
|
||||||
|
assert!((s[0] as f32 / i16::MAX as f32 - 0.2).abs() < 0.001); // L = mic
|
||||||
|
assert!((s[1] as f32 / i16::MAX as f32 - 0.5).abs() < 0.001); // R = loopback downmix
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn f32_to_i16_clamps_and_scales() {
|
fn f32_to_i16_clamps_and_scales() {
|
||||||
assert_eq!(f32_to_i16(0.0), 0);
|
assert_eq!(f32_to_i16(0.0), 0);
|
||||||
|
|||||||
+1370
-59
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,178 @@
|
|||||||
|
//! Enterprise deployment: seed default settings from an admin-supplied `.ini`
|
||||||
|
//! on **first run only** (before any `settings.json` exists).
|
||||||
|
//!
|
||||||
|
//! An admin mass-deploying WhispAssist (GPO / SCCM / Intune) drops a
|
||||||
|
//! `wa-defaults.ini` and every fresh install picks it up once, seeding
|
||||||
|
//! `settings.json` with their chosen defaults (record-by-default, preferred
|
||||||
|
//! backend, retention, model to auto-download, …) — all via native Windows file
|
||||||
|
//! deployment, no WiX custom actions. See `docs/enterprise-deployment.md`.
|
||||||
|
//!
|
||||||
|
//! **Guardrail (CLAUDE.md):** the file must never carry secrets. Keys that look
|
||||||
|
//! like credentials are ignored here as defense in depth — API keys / OAuth
|
||||||
|
//! tokens live only in the OS credential store.
|
||||||
|
|
||||||
|
use crate::models::Settings;
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Special (non-`Settings`) INI key: when truthy, the first-run seed also fetches
|
||||||
|
/// the configured `whisper_model` in the background so the machine is ready
|
||||||
|
/// offline. Stripped before the settings merge.
|
||||||
|
const AUTO_DOWNLOAD_KEY: &str = "auto_download_model";
|
||||||
|
|
||||||
|
/// Candidate locations, first found wins:
|
||||||
|
/// 1. `%PROGRAMDATA%\WhispAssist\wa-defaults.ini` — machine-wide enterprise path.
|
||||||
|
/// 2. `<exe dir>\wa-defaults.ini` — the bundled template / per-install override.
|
||||||
|
fn candidate_paths() -> Vec<PathBuf> {
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
if let Ok(program_data) = std::env::var("ProgramData") {
|
||||||
|
paths.push(PathBuf::from(program_data).join("WhispAssist").join("wa-defaults.ini"));
|
||||||
|
}
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
if let Some(dir) = exe.parent() {
|
||||||
|
paths.push(dir.join("wa-defaults.ini"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the first existing defaults file and produces the seeded settings plus
|
||||||
|
/// the whisper model id to auto-download (if `auto_download_model` was set).
|
||||||
|
/// `None` when no file exists or it contains no overrides (the shipped template
|
||||||
|
/// is fully commented, so normal installs get exactly today's behavior).
|
||||||
|
pub fn seed_settings_from_defaults() -> Option<(Settings, Option<String>)> {
|
||||||
|
let text = candidate_paths()
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|p| std::fs::read_to_string(p).ok())?;
|
||||||
|
seed_from_ini(&text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Testable core: parse INI text → merge onto the built-in defaults.
|
||||||
|
fn seed_from_ini(text: &str) -> Option<(Settings, Option<String>)> {
|
||||||
|
let mut overrides = parse_ini(text);
|
||||||
|
if overrides.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the non-Settings auto-download flag out before the merge.
|
||||||
|
let auto_download = overrides
|
||||||
|
.remove(AUTO_DOWNLOAD_KEY)
|
||||||
|
.map(|v| truthy(&v))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
// Merge overrides onto the default settings' JSON form, then deserialize.
|
||||||
|
// Unknown keys (typos) are ignored — `Settings` has no deny_unknown_fields.
|
||||||
|
let mut base = match serde_json::to_value(crate::commands::default_settings()) {
|
||||||
|
Ok(Value::Object(map)) => map,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
for (k, v) in overrides {
|
||||||
|
base.insert(k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings: Settings = serde_json::from_value(Value::Object(base)).ok()?;
|
||||||
|
let model = if auto_download {
|
||||||
|
Some(settings.whisper_model.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Some((settings, model))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimal INI reader: skips blanks, `;`/`#` comments and `[section]` headers;
|
||||||
|
/// splits each `key = value` on the first `=`; coerces values to bool / integer /
|
||||||
|
/// string so serde lands them on the typed `Settings` fields. Silently drops any
|
||||||
|
/// key that looks like a secret (guardrail — no credentials in the deploy file).
|
||||||
|
fn parse_ini(text: &str) -> Map<String, Value> {
|
||||||
|
let mut map = Map::new();
|
||||||
|
for line in text.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty()
|
||||||
|
|| line.starts_with(';')
|
||||||
|
|| line.starts_with('#')
|
||||||
|
|| line.starts_with('[')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some((key, value)) = line.split_once('=') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let key = key.trim().to_string();
|
||||||
|
let value = value.trim();
|
||||||
|
if key.is_empty() || looks_like_secret(&key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
map.insert(key, coerce(value));
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true`/`false` → bool, all-integer → number, everything else → string.
|
||||||
|
fn coerce(value: &str) -> Value {
|
||||||
|
match value.to_ascii_lowercase().as_str() {
|
||||||
|
"true" => return Value::Bool(true),
|
||||||
|
"false" => return Value::Bool(false),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if let Ok(n) = value.parse::<i64>() {
|
||||||
|
return Value::Number(n.into());
|
||||||
|
}
|
||||||
|
Value::String(value.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truthy(v: &Value) -> bool {
|
||||||
|
matches!(v, Value::Bool(true)) || matches!(v, Value::String(s) if s.eq_ignore_ascii_case("true"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Defense in depth: never seed anything that smells like a credential.
|
||||||
|
fn looks_like_secret(key: &str) -> bool {
|
||||||
|
let k = key.to_ascii_lowercase();
|
||||||
|
["key", "token", "secret", "credential", "password"]
|
||||||
|
.iter()
|
||||||
|
.any(|needle| k.contains(needle))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fully_commented_file_is_a_noop() {
|
||||||
|
let ini = "; default_record = true\n# preferred_backend = cpu\n[general]\n\n";
|
||||||
|
assert!(seed_from_ini(ini).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coerces_bool_int_and_string_fields() {
|
||||||
|
let ini = "default_record = true\nretention_max_age_days = 90\npreferred_backend = cpu\n";
|
||||||
|
let (settings, model) = seed_from_ini(ini).expect("overrides present");
|
||||||
|
assert!(settings.default_record);
|
||||||
|
assert_eq!(settings.retention_max_age_days, Some(90));
|
||||||
|
assert_eq!(settings.preferred_backend, "cpu");
|
||||||
|
assert!(model.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_download_returns_the_configured_model() {
|
||||||
|
let ini = "whisper_model = base.en-q5_1\nauto_download_model = true\n";
|
||||||
|
let (_settings, model) = seed_from_ini(ini).expect("overrides present");
|
||||||
|
assert_eq!(model.as_deref(), Some("base.en-q5_1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unset_fields_keep_their_defaults() {
|
||||||
|
let ini = "default_record = true\n";
|
||||||
|
let (settings, _) = seed_from_ini(ini).unwrap();
|
||||||
|
// microphone stays on, auto_start stays off — only the named key changed.
|
||||||
|
assert!(settings.microphone_enabled);
|
||||||
|
assert!(!settings.auto_start);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn secret_keys_are_ignored() {
|
||||||
|
let ini = "anthropic_api_key = sk-should-be-dropped\ndefault_record = true\n";
|
||||||
|
let map = parse_ini(ini);
|
||||||
|
assert!(!map.contains_key("anthropic_api_key"));
|
||||||
|
assert!(map.contains_key("default_record"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,11 @@ pub enum DiarError {
|
|||||||
pub trait Diarizer: Send + Sync {
|
pub trait Diarizer: Send + Sync {
|
||||||
/// Partition audio into speaker spans.
|
/// Partition audio into speaker spans.
|
||||||
fn diarize(&self, wav: &Path) -> Result<Vec<SpeakerSpan>, DiarError>;
|
fn diarize(&self, wav: &Path) -> Result<Vec<SpeakerSpan>, DiarError>;
|
||||||
|
/// Partition already-loaded 16kHz mono samples into speaker spans. Phase 3
|
||||||
|
/// (FR-SPK) diarizes a *masked* far-side signal (the mic's "You" ranges
|
||||||
|
/// zeroed), so the caller loads and masks the samples itself rather than
|
||||||
|
/// pointing at the raw WAV.
|
||||||
|
fn diarize_samples(&self, samples: Vec<f32>) -> Result<Vec<SpeakerSpan>, DiarError>;
|
||||||
/// Assign speaker labels to transcript segments by overlap with spans.
|
/// Assign speaker labels to transcript segments by overlap with spans.
|
||||||
fn assign(&self, segments: &mut [TranscriptSegment], spans: &[SpeakerSpan]);
|
fn assign(&self, segments: &mut [TranscriptSegment], spans: &[SpeakerSpan]);
|
||||||
}
|
}
|
||||||
@@ -53,6 +58,55 @@ pub fn assign_by_overlap(segments: &mut [TranscriptSegment], spans: &[SpeakerSpa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Split-layout attribution (FR-SPK): decides per segment between "You" (mic
|
||||||
|
/// channel voice activity) and the far side's diarized speakers by comparing
|
||||||
|
/// the *total* voiced overlap on each channel, not by picking the single
|
||||||
|
/// longest span — a long far-side diarizer span could otherwise swallow a
|
||||||
|
/// segment the user spoke most of, showing their words under "Speaker N".
|
||||||
|
/// The mic channel is physically the user's voice alone, so channel evidence
|
||||||
|
/// outranks cluster evidence; ties go to "You" (mislabeling the user's own
|
||||||
|
/// words as someone else is the worse failure). A segment with no voiced
|
||||||
|
/// overlap on either channel keeps its prior label rather than guessing.
|
||||||
|
// ponytail: whole-segment labels — a segment genuinely containing both sides
|
||||||
|
// still gets one speaker; the upgrade path is transcribing each channel
|
||||||
|
// separately so segments can never mix voices.
|
||||||
|
pub fn assign_split(
|
||||||
|
segments: &mut [TranscriptSegment],
|
||||||
|
you_spans: &[(u64, u64)],
|
||||||
|
far_vad: &[(u64, u64)],
|
||||||
|
far_spans: &[SpeakerSpan],
|
||||||
|
) {
|
||||||
|
fn overlap(a0: u64, a1: u64, b0: u64, b1: u64) -> u64 {
|
||||||
|
a1.min(b1).saturating_sub(a0.max(b0))
|
||||||
|
}
|
||||||
|
for seg in segments.iter_mut() {
|
||||||
|
let mic_ms: u64 = you_spans
|
||||||
|
.iter()
|
||||||
|
.map(|&(s, e)| overlap(seg.start_ms, seg.end_ms, s, e))
|
||||||
|
.sum();
|
||||||
|
let far_ms: u64 = far_vad
|
||||||
|
.iter()
|
||||||
|
.map(|&(s, e)| overlap(seg.start_ms, seg.end_ms, s, e))
|
||||||
|
.sum();
|
||||||
|
if mic_ms == 0 && far_ms == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if mic_ms >= far_ms {
|
||||||
|
seg.speaker = "You".to_string();
|
||||||
|
} else if let Some(span) = far_spans
|
||||||
|
.iter()
|
||||||
|
.map(|sp| (overlap(seg.start_ms, seg.end_ms, sp.start_ms, sp.end_ms), sp))
|
||||||
|
.filter(|(o, _)| *o > 0)
|
||||||
|
.max_by_key(|(o, _)| *o)
|
||||||
|
.map(|(_, sp)| sp)
|
||||||
|
{
|
||||||
|
seg.speaker = span.speaker.clone();
|
||||||
|
}
|
||||||
|
// Far side voiced but no diarizer span overlaps (e.g. a sub-700ms span
|
||||||
|
// was filtered): keep the prior label rather than guess.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// sherpa-onnx-backed diarizer: pyannote segmentation + speaker-embedding +
|
/// sherpa-onnx-backed diarizer: pyannote segmentation + speaker-embedding +
|
||||||
/// fast clustering (ADR-0005, T4.1). `Diarize::compute` needs `&mut self`; it's
|
/// fast clustering (ADR-0005, T4.1). `Diarize::compute` needs `&mut self`; it's
|
||||||
/// wrapped in a `Mutex` to satisfy `Diarizer: Sync` — diarization is a
|
/// wrapped in a `Mutex` to satisfy `Diarizer: Sync` — diarization is a
|
||||||
@@ -70,7 +124,13 @@ impl SherpaDiarizer {
|
|||||||
// sherpa-onnx to pick the cluster count itself from `threshold`
|
// sherpa-onnx to pick the cluster count itself from `threshold`
|
||||||
// instead of forcing a fixed number of speakers.
|
// instead of forcing a fixed number of speakers.
|
||||||
num_clusters: Some(-1),
|
num_clusters: Some(-1),
|
||||||
threshold: Some(0.5),
|
// Tuning evidence: 0.5 over the summed mic+loopback mono WAV
|
||||||
|
// exploded a 2-speaker conversation into 83 clusters (overlapped
|
||||||
|
// speech + short pyannote chunks yield unstable embeddings). sherpa
|
||||||
|
// fast-clustering: larger threshold → fewer clusters. 0.7 lands a
|
||||||
|
// 2-person meeting near 2–4. ponytail: code constant, no settings
|
||||||
|
// knob until 0.7 proves insufficient in the field.
|
||||||
|
threshold: Some(0.7),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let engine = sherpa_rs::diarize::Diarize::new(segmentation_model, embedding_model, config)
|
let engine = sherpa_rs::diarize::Diarize::new(segmentation_model, embedding_model, config)
|
||||||
@@ -86,11 +146,28 @@ impl Diarizer for SherpaDiarizer {
|
|||||||
fn diarize(&self, wav: &Path) -> Result<Vec<SpeakerSpan>, DiarError> {
|
fn diarize(&self, wav: &Path) -> Result<Vec<SpeakerSpan>, DiarError> {
|
||||||
let samples =
|
let samples =
|
||||||
crate::audio::read_wav_mono_16k(wav).map_err(|e| DiarError::Run(e.to_string()))?;
|
crate::audio::read_wav_mono_16k(wav).map_err(|e| DiarError::Run(e.to_string()))?;
|
||||||
|
self.diarize_samples(samples)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diarize_samples(&self, samples: Vec<f32>) -> Result<Vec<SpeakerSpan>, DiarError> {
|
||||||
|
// Guard the native FFI against degenerate input (a fully-masked far side
|
||||||
|
// is all zeros; a tiny buffer has no usable window). sherpa can crash
|
||||||
|
// rather than error on these, so skip cleanly instead.
|
||||||
|
if samples.len() < MIN_DIARIZE_SAMPLES {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
let mut engine = self.engine.lock().unwrap_or_else(|e| e.into_inner());
|
let mut engine = self.engine.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
let segments = engine
|
let segments = engine
|
||||||
.compute(samples, None)
|
.compute(samples, None)
|
||||||
.map_err(|e| DiarError::Run(e.to_string()))?;
|
.map_err(|e| DiarError::Run(e.to_string()))?;
|
||||||
Ok(segments.into_iter().map(segment_to_span).collect())
|
// Drop sub-700ms spans: their embeddings are unstable and only cause
|
||||||
|
// label churn in assign_by_overlap (which keeps the prior label when no
|
||||||
|
// span overlaps). See MIN_SPAN_MS.
|
||||||
|
Ok(segments
|
||||||
|
.into_iter()
|
||||||
|
.map(segment_to_span)
|
||||||
|
.filter(span_long_enough)
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assign(&self, segments: &mut [TranscriptSegment], spans: &[SpeakerSpan]) {
|
fn assign(&self, segments: &mut [TranscriptSegment], spans: &[SpeakerSpan]) {
|
||||||
@@ -98,6 +175,23 @@ impl Diarizer for SherpaDiarizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Minimum span duration kept from a diarization pass. Sub-second pyannote
|
||||||
|
/// chunks carry unstable ERes2Net embeddings and only churn labels.
|
||||||
|
#[cfg(feature = "diarization")]
|
||||||
|
const MIN_SPAN_MS: u64 = 700;
|
||||||
|
|
||||||
|
/// Below this many 16kHz samples (~1s) the diarizer skips the sherpa FFI: a
|
||||||
|
/// fully-masked far side is all zeros and a tiny buffer has no usable
|
||||||
|
/// segmentation window, either of which can crash the native code.
|
||||||
|
#[cfg(feature = "diarization")]
|
||||||
|
const MIN_DIARIZE_SAMPLES: usize = 16_000;
|
||||||
|
|
||||||
|
/// A span is kept only if it lasts at least [`MIN_SPAN_MS`].
|
||||||
|
#[cfg(feature = "diarization")]
|
||||||
|
fn span_long_enough(span: &SpeakerSpan) -> bool {
|
||||||
|
span.end_ms.saturating_sub(span.start_ms) >= MIN_SPAN_MS
|
||||||
|
}
|
||||||
|
|
||||||
/// sherpa-onnx speaker indices are 0-based; WA's internal labels are 1-based ("S1"…).
|
/// sherpa-onnx speaker indices are 0-based; WA's internal labels are 1-based ("S1"…).
|
||||||
#[cfg(feature = "diarization")]
|
#[cfg(feature = "diarization")]
|
||||||
fn segment_to_span(seg: sherpa_rs::diarize::Segment) -> SpeakerSpan {
|
fn segment_to_span(seg: sherpa_rs::diarize::Segment) -> SpeakerSpan {
|
||||||
@@ -165,6 +259,58 @@ mod overlap_tests {
|
|||||||
assign_by_overlap(&mut segments, &[]);
|
assign_by_overlap(&mut segments, &[]);
|
||||||
assert_eq!(segments[0].speaker, "S1");
|
assert_eq!(segments[0].speaker, "S1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_labels_a_mic_dominant_segment_you_even_against_a_longer_far_span() {
|
||||||
|
// The user spoke 0-4000ms; the far side 4000-6000ms — but the far
|
||||||
|
// cluster span covers the whole window, so the old merged max-overlap
|
||||||
|
// pick handed the entire segment (the user's words included) to the
|
||||||
|
// far speaker. Channel totals must side with the mic instead.
|
||||||
|
let mut segments = vec![segment(0, 6000)];
|
||||||
|
let you = vec![(0u64, 4000u64)];
|
||||||
|
let far_vad = vec![(4000u64, 6000u64)];
|
||||||
|
let far_spans = vec![span(0, 6000, "S1")]; // long far cluster span
|
||||||
|
assign_split(&mut segments, &you, &far_vad, &far_spans);
|
||||||
|
assert_eq!(segments[0].speaker, "You");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_ties_go_to_you() {
|
||||||
|
let mut segments = vec![segment(0, 2000)];
|
||||||
|
let you = vec![(0u64, 1000u64)];
|
||||||
|
let far_vad = vec![(1000u64, 2000u64)];
|
||||||
|
let far_spans = vec![span(1000, 2000, "S1")];
|
||||||
|
assign_split(&mut segments, &you, &far_vad, &far_spans);
|
||||||
|
assert_eq!(segments[0].speaker, "You");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_assigns_the_best_far_span_when_the_far_side_dominates() {
|
||||||
|
let mut segments = vec![segment(0, 3000)];
|
||||||
|
let you = vec![(0u64, 500u64)];
|
||||||
|
let far_vad = vec![(500u64, 3000u64)];
|
||||||
|
let far_spans = vec![span(500, 1000, "S1"), span(1000, 3000, "S2")];
|
||||||
|
assign_split(&mut segments, &you, &far_vad, &far_spans);
|
||||||
|
assert_eq!(segments[0].speaker, "S2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_keeps_the_prior_label_when_both_channels_are_silent() {
|
||||||
|
let mut segments = vec![segment(5000, 6000)];
|
||||||
|
segments[0].speaker = "S9".to_string();
|
||||||
|
assign_split(&mut segments, &[(0, 1000)], &[(0, 1000)], &[span(0, 1000, "S1")]);
|
||||||
|
assert_eq!(segments[0].speaker, "S9");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_keeps_the_prior_label_when_far_is_voiced_but_no_far_span_overlaps() {
|
||||||
|
// Far VAD hears speech but every diarizer span was filtered (sub-700ms):
|
||||||
|
// don't guess a label.
|
||||||
|
let mut segments = vec![segment(0, 1000)];
|
||||||
|
segments[0].speaker = "S3".to_string();
|
||||||
|
assign_split(&mut segments, &[], &[(0, 1000)], &[span(2000, 3000, "S1")]);
|
||||||
|
assert_eq!(segments[0].speaker, "S3");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, feature = "diarization"))]
|
#[cfg(all(test, feature = "diarization"))]
|
||||||
@@ -184,6 +330,22 @@ mod tests {
|
|||||||
assert_eq!(span.speaker, "S1");
|
assert_eq!(span.speaker, "S1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn span_long_enough_filters_sub_minimum_spans() {
|
||||||
|
let short = SpeakerSpan {
|
||||||
|
start_ms: 0,
|
||||||
|
end_ms: MIN_SPAN_MS - 1,
|
||||||
|
speaker: "S1".to_string(),
|
||||||
|
};
|
||||||
|
let exact = SpeakerSpan {
|
||||||
|
start_ms: 0,
|
||||||
|
end_ms: MIN_SPAN_MS,
|
||||||
|
speaker: "S1".to_string(),
|
||||||
|
};
|
||||||
|
assert!(!span_long_enough(&short));
|
||||||
|
assert!(span_long_enough(&exact));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_surfaces_a_load_error_for_missing_models_instead_of_panicking() {
|
fn new_surfaces_a_load_error_for_missing_models_instead_of_panicking() {
|
||||||
let result =
|
let result =
|
||||||
|
|||||||
@@ -110,8 +110,9 @@ pub fn match_mic_speaker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Distinct speaker labels in first-appearance order — spans come back from
|
/// Distinct speaker labels in first-appearance order — spans come back from
|
||||||
/// the diarizer already sorted by start time.
|
/// the diarizer already sorted by start time. `pub(crate)` so the Phase 3
|
||||||
fn first_appearance_order(spans: &[SpeakerSpan]) -> Vec<String> {
|
/// per-stream path can reuse it to name its merged You + far-side spans.
|
||||||
|
pub(crate) fn first_appearance_order(spans: &[SpeakerSpan]) -> Vec<String> {
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
spans
|
spans
|
||||||
.iter()
|
.iter()
|
||||||
@@ -152,8 +153,12 @@ fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
|
|||||||
|
|
||||||
/// `mic_label` -> "You"; every other label, in first-appearance order ->
|
/// `mic_label` -> "You"; every other label, in first-appearance order ->
|
||||||
/// "Speaker 2", "Speaker 3", … (numbering starts at 2 — "You" stands in for
|
/// "Speaker 2", "Speaker 3", … (numbering starts at 2 — "You" stands in for
|
||||||
/// "Speaker 1" without ever being called that).
|
/// "Speaker 1" without ever being called that). `pub(crate)` so Phase 3 reuses
|
||||||
fn build_name_map(labels_in_order: &[String], mic_label: &str) -> HashMap<String, String> {
|
/// it for its merged You + far-side spans, keeping naming uniform app-wide.
|
||||||
|
pub(crate) fn build_name_map(
|
||||||
|
labels_in_order: &[String],
|
||||||
|
mic_label: &str,
|
||||||
|
) -> HashMap<String, String> {
|
||||||
let mut names = HashMap::new();
|
let mut names = HashMap::new();
|
||||||
let mut next_speaker_number = 2;
|
let mut next_speaker_number = 2;
|
||||||
for label in labels_in_order {
|
for label in labels_in_order {
|
||||||
|
|||||||
+159
-6
@@ -9,6 +9,7 @@ pub mod audio;
|
|||||||
pub mod briefs;
|
pub mod briefs;
|
||||||
pub mod calendar;
|
pub mod calendar;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod deploy;
|
||||||
pub mod diarization;
|
pub mod diarization;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod hardware;
|
pub mod hardware;
|
||||||
@@ -27,7 +28,8 @@ pub mod vault;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, Mutex as StdMutex};
|
use std::sync::{Arc, Mutex as StdMutex};
|
||||||
use std::thread::JoinHandle;
|
use std::thread::JoinHandle;
|
||||||
use tauri::tray::TrayIcon;
|
use tauri::menu::{Menu, MenuItem};
|
||||||
|
use tauri::tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
@@ -85,6 +87,10 @@ pub struct RecordingSession {
|
|||||||
/// auto-labeled "You" instead of a clustered "S1"/"S2". `None` when the
|
/// auto-labeled "You" instead of a clustered "S1"/"S2". `None` when the
|
||||||
/// mic is disabled (same conditions as `mic_capture`).
|
/// mic is disabled (same conditions as `mic_capture`).
|
||||||
pub mic_voice_sample: Option<Arc<audio::VoiceSample>>,
|
pub mic_voice_sample: Option<Arc<audio::VoiceSample>>,
|
||||||
|
/// `audio.wav` channel layout for this recording (FR-SPK): `"split"` (mic
|
||||||
|
/// enabled → stereo mic-left/loopback-right) or `"summed"` (mic off). Passed
|
||||||
|
/// to `finalize_meeting` so playback/diarization/export can branch on it.
|
||||||
|
pub audio_layout: &'static str,
|
||||||
/// Live notes redesign: raw user-authored notes accumulated *during* the
|
/// Live notes redesign: raw user-authored notes accumulated *during* the
|
||||||
/// recording (freeform text + per-moment annotations) — see
|
/// recording (freeform text + per-moment annotations) — see
|
||||||
/// `models::ManualNotes`. Mutated by `update_live_notes`/`set_segment_note`
|
/// `models::ManualNotes`. Mutated by `update_live_notes`/`set_segment_note`
|
||||||
@@ -98,18 +104,67 @@ pub struct RecordingSession {
|
|||||||
/// tooltip on recording state changes (FR-CAP-4).
|
/// tooltip on recording state changes (FR-CAP-4).
|
||||||
pub struct TrayHandle(pub TrayIcon);
|
pub struct TrayHandle(pub TrayIcon);
|
||||||
|
|
||||||
|
/// Show a blocking native error dialog and exit non-zero. Used for failures
|
||||||
|
/// that happen before the Tauri app (and its window) exist, where a silent
|
||||||
|
/// `panic=abort` fast-fail would otherwise close with no explanation. Exits
|
||||||
|
/// cleanly (code 1) rather than aborting so the OS reports no crash.
|
||||||
|
fn fatal_startup_error(body: &str) -> ! {
|
||||||
|
tracing::error!("fatal startup error: {body}");
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
use windows::core::PCWSTR;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::{
|
||||||
|
MessageBoxW, MB_ICONERROR, MB_OK, MB_SYSTEMMODAL,
|
||||||
|
};
|
||||||
|
// Null-terminated UTF-16 for the Win32 wide API.
|
||||||
|
let to_wide = |s: &str| s.encode_utf16().chain(std::iter::once(0)).collect::<Vec<u16>>();
|
||||||
|
let text = to_wide(body);
|
||||||
|
let caption = to_wide("WhispAssist can't start");
|
||||||
|
// SAFETY: both buffers are null-terminated and outlive the call.
|
||||||
|
unsafe {
|
||||||
|
MessageBoxW(
|
||||||
|
None,
|
||||||
|
PCWSTR(text.as_ptr()),
|
||||||
|
PCWSTR(caption.as_ptr()),
|
||||||
|
MB_OK | MB_ICONERROR | MB_SYSTEMMODAL,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
eprintln!("WhispAssist can't start: {body}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
/// Build state, register commands/events, and run the app.
|
/// Build state, register commands/events, and run the app.
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tracing_subscriber::fmt().with_env_filter("info").init();
|
tracing_subscriber::fmt().with_env_filter("info").init();
|
||||||
|
|
||||||
let store: Arc<dyn storage::Store> = Arc::new(
|
let store: Arc<dyn storage::Store> =
|
||||||
tauri::async_runtime::block_on(storage::SqliteStore::connect())
|
match tauri::async_runtime::block_on(storage::SqliteStore::connect()) {
|
||||||
.expect("failed to initialize storage (wa.db)"),
|
Ok(s) => Arc::new(s),
|
||||||
);
|
// A migration checksum mismatch (an edited-after-shipping migration)
|
||||||
|
// or a locked/corrupt file lands here. Tell the user instead of
|
||||||
|
// fast-failing into a silent no-launch.
|
||||||
|
Err(e) => fatal_startup_error(&format!(
|
||||||
|
"WhispAssist couldn't open its database (wa.db):\n\n{e}\n\n\
|
||||||
|
Your recordings and notes are not deleted. This usually means the \
|
||||||
|
database is from a newer version of WhispAssist, or a data migration \
|
||||||
|
is incompatible. Reinstalling the matching version, or moving wa.db \
|
||||||
|
aside from %LOCALAPPDATA%\\WhispAssist, will let the app start."
|
||||||
|
)),
|
||||||
|
};
|
||||||
let store_for_setup = store.clone();
|
let store_for_setup = store.clone();
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
// Opt-in launch-at-login (NFR-RES-4). The macOS launcher arg is required
|
||||||
|
// by the signature but unused on Windows, where enable/disable writes a
|
||||||
|
// per-user HKCU\...\Run entry (no admin). Off until the user (or an
|
||||||
|
// enterprise deploy file) turns `auto_start` on.
|
||||||
|
.plugin(tauri_plugin_autostart::init(
|
||||||
|
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
||||||
|
None,
|
||||||
|
))
|
||||||
// In-memory streaming of recordings for the player (FR-REC-5): decrypts
|
// In-memory streaming of recordings for the player (FR-REC-5): decrypts
|
||||||
// on the fly so no plaintext audio is ever written to disk.
|
// on the fly so no plaintext audio is ever written to disk.
|
||||||
.register_uri_scheme_protocol("waaudio", |_ctx, request| {
|
.register_uri_scheme_protocol("waaudio", |_ctx, request| {
|
||||||
@@ -120,13 +175,84 @@ pub fn run() {
|
|||||||
session: Mutex::new(None),
|
session: Mutex::new(None),
|
||||||
})
|
})
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
|
// Single tray icon (the `trayIcon` in tauri.conf.json was removed so
|
||||||
|
// this is the only one). It carries a Show/Quit menu and, on
|
||||||
|
// left-click, restores the window — the always-available way back
|
||||||
|
// from "close to tray".
|
||||||
let icon = tauri::image::Image::from_bytes(include_bytes!("../icons/tray.png"))?;
|
let icon = tauri::image::Image::from_bytes(include_bytes!("../icons/tray.png"))?;
|
||||||
let tray = tauri::tray::TrayIconBuilder::new()
|
let show_item = MenuItem::with_id(app, "show", "Show WhispAssist", true, None::<&str>)?;
|
||||||
|
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
|
||||||
|
let menu = Menu::with_items(app, &[&show_item, &quit_item])?;
|
||||||
|
let tray = TrayIconBuilder::new()
|
||||||
.icon(icon)
|
.icon(icon)
|
||||||
.tooltip("WhispAssist — idle")
|
.tooltip("WhispAssist — idle")
|
||||||
|
.menu(&menu)
|
||||||
|
.show_menu_on_left_click(false)
|
||||||
|
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||||
|
"show" => show_main_window(app),
|
||||||
|
"quit" => app.exit(0),
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.on_tray_icon_event(|tray, event| {
|
||||||
|
if let TrayIconEvent::Click {
|
||||||
|
button: MouseButton::Left,
|
||||||
|
button_state: MouseButtonState::Up,
|
||||||
|
..
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
show_main_window(tray.app_handle());
|
||||||
|
}
|
||||||
|
})
|
||||||
.build(app)?;
|
.build(app)?;
|
||||||
app.manage(TrayHandle(tray));
|
app.manage(TrayHandle(tray));
|
||||||
|
|
||||||
|
// First-run enterprise deploy seeding (deploy.rs): if no settings.json
|
||||||
|
// exists yet and an admin dropped a wa-defaults.ini, seed settings once
|
||||||
|
// and optionally fetch the configured model in the background. One-shot
|
||||||
|
// — guarded by the settings file's absence, so it never re-runs and adds
|
||||||
|
// nothing to idle cost (NFR-RES-1).
|
||||||
|
if !crate::paths::settings_path().exists() {
|
||||||
|
if let Some((seeded, model_to_download)) = deploy::seed_settings_from_defaults() {
|
||||||
|
match commands::save_settings(&seeded) {
|
||||||
|
Ok(()) => {
|
||||||
|
tracing::info!("seeded settings.json from wa-defaults.ini");
|
||||||
|
if let Some(id) = model_to_download {
|
||||||
|
let app_handle = app.handle().clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
if let Err(e) = commands::download_model(
|
||||||
|
app_handle,
|
||||||
|
commands::DownloadModelArgs {
|
||||||
|
kind: "whisper".into(),
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!("deploy auto-download of model failed: {e:?}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("first-run deploy seeding failed to write settings: {e:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconcile launch-at-login with the persisted preference (NFR-RES-4):
|
||||||
|
// if the user opted in but the OS entry is missing (e.g. after a
|
||||||
|
// reinstall or a deploy file that set auto_start), restore it. One-shot.
|
||||||
|
{
|
||||||
|
use tauri_plugin_autostart::ManagerExt;
|
||||||
|
let manager = app.autolaunch();
|
||||||
|
if commands::load_settings().auto_start && !manager.is_enabled().unwrap_or(false) {
|
||||||
|
if let Err(e) = manager.enable() {
|
||||||
|
tracing::warn!("failed to restore auto-start entry: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reminders (Phase 8, T8.6, FR-CAL-5) are Windows-scheduled toasts, not
|
// Reminders (Phase 8, T8.6, FR-CAL-5) are Windows-scheduled toasts, not
|
||||||
// an app-side timer — Windows itself is what's "polling", so this stays
|
// an app-side timer — Windows itself is what's "polling", so this stays
|
||||||
// within NFR-RES-1. init() just registers the AppUserModelID.
|
// within NFR-RES-1. init() just registers the AppUserModelID.
|
||||||
@@ -220,6 +346,17 @@ pub fn run() {
|
|||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
// Close to tray (keep running in background): when the setting is on,
|
||||||
|
// the window X hides instead of quitting; the tray "Quit" is the real
|
||||||
|
// exit. Off → default behavior (closing the window quits the app).
|
||||||
|
.on_window_event(|window, event| {
|
||||||
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||||
|
if commands::load_settings().close_to_tray {
|
||||||
|
api.prevent_close();
|
||||||
|
let _ = window.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
commands::start_recording,
|
commands::start_recording,
|
||||||
commands::stop_recording,
|
commands::stop_recording,
|
||||||
@@ -227,6 +364,7 @@ pub fn run() {
|
|||||||
commands::recording_playback_path,
|
commands::recording_playback_path,
|
||||||
commands::pause_recording,
|
commands::pause_recording,
|
||||||
commands::resume_recording,
|
commands::resume_recording,
|
||||||
|
commands::toggle_microphone_mute,
|
||||||
commands::set_recording_retention,
|
commands::set_recording_retention,
|
||||||
commands::acknowledge_recording_consent,
|
commands::acknowledge_recording_consent,
|
||||||
commands::update_live_notes,
|
commands::update_live_notes,
|
||||||
@@ -238,6 +376,9 @@ pub fn run() {
|
|||||||
commands::list_audio_devices,
|
commands::list_audio_devices,
|
||||||
commands::list_input_devices,
|
commands::list_input_devices,
|
||||||
commands::set_preferred_backend,
|
commands::set_preferred_backend,
|
||||||
|
commands::set_auto_start,
|
||||||
|
commands::monitor_audio_level,
|
||||||
|
commands::stress_test_hardware,
|
||||||
commands::list_models,
|
commands::list_models,
|
||||||
commands::list_whisper_languages,
|
commands::list_whisper_languages,
|
||||||
commands::download_npu_package,
|
commands::download_npu_package,
|
||||||
@@ -257,6 +398,7 @@ pub fn run() {
|
|||||||
commands::update_notes,
|
commands::update_notes,
|
||||||
commands::export_meeting,
|
commands::export_meeting,
|
||||||
commands::bulk_export_meetings,
|
commands::bulk_export_meetings,
|
||||||
|
commands::import_meeting_bundle,
|
||||||
commands::rename_speaker,
|
commands::rename_speaker,
|
||||||
commands::merge_speakers,
|
commands::merge_speakers,
|
||||||
commands::map_speaker_to_participant,
|
commands::map_speaker_to_participant,
|
||||||
@@ -265,6 +407,7 @@ pub fn run() {
|
|||||||
commands::generate_summary,
|
commands::generate_summary,
|
||||||
commands::confirm_action_items,
|
commands::confirm_action_items,
|
||||||
commands::generate_tags,
|
commands::generate_tags,
|
||||||
|
commands::enhance_notes,
|
||||||
commands::llm_setup_suggestions,
|
commands::llm_setup_suggestions,
|
||||||
commands::pull_ollama_model,
|
commands::pull_ollama_model,
|
||||||
commands::import_pst,
|
commands::import_pst,
|
||||||
@@ -309,6 +452,16 @@ pub fn run() {
|
|||||||
.expect("error while running WhispAssist");
|
.expect("error while running WhispAssist");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Restore the main window from the tray (show + unminimize + focus). Shared by
|
||||||
|
/// the tray left-click and the "Show WhispAssist" menu item.
|
||||||
|
fn show_main_window(app: &tauri::AppHandle) {
|
||||||
|
if let Some(w) = app.get_webview_window("main") {
|
||||||
|
let _ = w.show();
|
||||||
|
let _ = w.unminimize();
|
||||||
|
let _ = w.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Used by `commands.rs` to keep the tray tooltip honest about capture state (FR-CAP-4).
|
/// Used by `commands.rs` to keep the tray tooltip honest about capture state (FR-CAP-4).
|
||||||
pub(crate) fn update_tray_tooltip(app: &tauri::AppHandle, text: &str) {
|
pub(crate) fn update_tray_tooltip(app: &tauri::AppHandle, text: &str) {
|
||||||
if let Some(tray) = app.try_state::<TrayHandle>() {
|
if let Some(tray) = app.try_state::<TrayHandle>() {
|
||||||
|
|||||||
@@ -193,6 +193,34 @@ pub struct ActionItem {
|
|||||||
pub reminder_set: bool,
|
pub reminder_set: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Portable meeting export manifest — the `meeting.json` inside an export
|
||||||
|
/// bundle folder (FR-STORE-4). Carries everything needed to reconstruct a
|
||||||
|
/// meeting on another machine alongside the bundle's files (`audio.wav`,
|
||||||
|
/// `transcript.json`, `notes.md`, `summary.json`). Deliberately excludes the
|
||||||
|
/// meeting id (a fresh one is minted on import to avoid collisions) and the
|
||||||
|
/// calendar-event link (event ids are machine-local).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MeetingBundle {
|
||||||
|
pub schema: u32,
|
||||||
|
pub title: String,
|
||||||
|
pub started_at: i64,
|
||||||
|
pub ended_at: Option<i64>,
|
||||||
|
pub duration_secs: Option<i64>,
|
||||||
|
pub language: Option<String>,
|
||||||
|
pub backend_used: Option<String>,
|
||||||
|
pub model_used: Option<String>,
|
||||||
|
pub recorded: bool,
|
||||||
|
pub template_id: Option<String>,
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
pub speakers: Vec<SpeakerInfo>,
|
||||||
|
pub action_items: Vec<ActionItem>,
|
||||||
|
/// `audio.wav` channel layout (FR-SPK/FR-CAP): `"split"` or `"summed"`.
|
||||||
|
/// `default` so bundles exported before this field deserialize as `None`
|
||||||
|
/// (treated as `"summed"`).
|
||||||
|
#[serde(default)]
|
||||||
|
pub audio_layout: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CalendarEvent {
|
pub struct CalendarEvent {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@@ -326,6 +354,17 @@ pub struct Settings {
|
|||||||
pub mcp_expose: String, // none|selected|all
|
pub mcp_expose: String, // none|selected|all
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mcp_expose_recordings: bool,
|
pub mcp_expose_recordings: bool,
|
||||||
|
/// Launch WhispAssist automatically at login (opt-in, NFR-RES-4). OFF by
|
||||||
|
/// default; toggled via `set_auto_start`, which writes a per-user
|
||||||
|
/// `HKCU\...\Run` entry through `tauri-plugin-autostart` (no admin). An
|
||||||
|
/// enterprise deploy file may set this to `true` (see `deploy.rs`).
|
||||||
|
#[serde(default)]
|
||||||
|
pub auto_start: bool,
|
||||||
|
/// Closing the window hides WhispAssist to the system tray instead of
|
||||||
|
/// quitting, so it keeps running in the background (tray "Quit" really
|
||||||
|
/// exits). ON by default; the tray icon is the always-available way back.
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub close_to_tray: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_mcp_transport() -> String {
|
fn default_mcp_transport() -> String {
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ pub struct FinalizeMeeting {
|
|||||||
pub language: Option<String>,
|
pub language: Option<String>,
|
||||||
pub backend_used: Option<String>,
|
pub backend_used: Option<String>,
|
||||||
pub model_used: Option<String>,
|
pub model_used: Option<String>,
|
||||||
|
/// How `audio.wav`'s channels are laid out (FR-SPK/FR-CAP): `"split"` =
|
||||||
|
/// stereo mic-left/loopback-right, `"summed"`/`None` = mic mixed into every
|
||||||
|
/// channel. `None` leaves the stored value unchanged (e.g. on reprocess).
|
||||||
|
pub audio_layout: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Full meeting detail: DB row + transcript + speakers + notes (`get_meeting`'s
|
/// Full meeting detail: DB row + transcript + speakers + notes (`get_meeting`'s
|
||||||
@@ -91,6 +95,10 @@ pub struct Meeting {
|
|||||||
pub language: Option<String>,
|
pub language: Option<String>,
|
||||||
pub backend_used: Option<String>,
|
pub backend_used: Option<String>,
|
||||||
pub model_used: Option<String>,
|
pub model_used: Option<String>,
|
||||||
|
/// `audio.wav` channel layout (FR-SPK/FR-CAP): `"split"` (mic-left /
|
||||||
|
/// loopback-right) or `"summed"`/`None` (mic mixed into every channel).
|
||||||
|
/// Diarization, playback downmix, and bundle export branch on it.
|
||||||
|
pub audio_layout: Option<String>,
|
||||||
pub segments: Vec<TranscriptSegment>,
|
pub segments: Vec<TranscriptSegment>,
|
||||||
pub speakers: Vec<SpeakerInfo>,
|
pub speakers: Vec<SpeakerInfo>,
|
||||||
pub notes_markdown: String,
|
pub notes_markdown: String,
|
||||||
@@ -104,6 +112,12 @@ pub struct Meeting {
|
|||||||
/// Note-template id (Phase 8, T8.1, FR-NOTE-5), if one was picked at
|
/// Note-template id (Phase 8, T8.1, FR-NOTE-5), if one was picked at
|
||||||
/// creation — resolved against `notes::templates::catalog()`.
|
/// creation — resolved against `notes::templates::catalog()`.
|
||||||
pub template_id: Option<String>,
|
pub template_id: Option<String>,
|
||||||
|
/// Confirmed/edited action items from the `action_items` table — the
|
||||||
|
/// source of truth the user manages directly (add/edit/delete via
|
||||||
|
/// `confirm_action_items`, FR-LLM-3). Falls back to the drafts parsed
|
||||||
|
/// into `summary.json` when the table has none yet, so a freshly
|
||||||
|
/// generated summary still shows its suggestions.
|
||||||
|
pub action_items: Vec<ActionItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A calendar event with its attendees (T6.3/T6.4, FR-CAL-1/3) — the
|
/// A calendar event with its attendees (T6.3/T6.4, FR-CAL-1/3) — the
|
||||||
@@ -262,6 +276,10 @@ pub trait Store: Send + Sync {
|
|||||||
) -> Result<Vec<MeetingListItem>, StoreError>;
|
) -> Result<Vec<MeetingListItem>, StoreError>;
|
||||||
async fn get_meeting(&self, id: &MeetingId) -> Result<Meeting, StoreError>;
|
async fn get_meeting(&self, id: &MeetingId) -> Result<Meeting, StoreError>;
|
||||||
async fn delete_meeting(&self, id: &MeetingId) -> Result<(), StoreError>;
|
async fn delete_meeting(&self, id: &MeetingId) -> Result<(), StoreError>;
|
||||||
|
/// Overwrite a meeting's lifecycle `status` (e.g. mark a background import
|
||||||
|
/// `transcribing` while it runs, or `error` if it fails). `finalize_meeting`
|
||||||
|
/// is still the only path to `ready`.
|
||||||
|
async fn set_meeting_status(&self, id: &MeetingId, status: &str) -> Result<(), StoreError>;
|
||||||
async fn update_notes(&self, id: &MeetingId, markdown: &str) -> Result<(), StoreError>;
|
async fn update_notes(&self, id: &MeetingId, markdown: &str) -> Result<(), StoreError>;
|
||||||
/// (Re)builds this meeting's FTS index row from the current title and
|
/// (Re)builds this meeting's FTS index row from the current title and
|
||||||
/// whatever's on disk/in the DB for transcript/notes/summary/tags (Phase
|
/// whatever's on disk/in the DB for transcript/notes/summary/tags (Phase
|
||||||
@@ -278,6 +296,14 @@ pub trait Store: Send + Sync {
|
|||||||
label: &str,
|
label: &str,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<(), StoreError>;
|
) -> Result<(), StoreError>;
|
||||||
|
/// Delete every `speakers` row for a meeting (labels, names, participant
|
||||||
|
/// links, merges). Used before a full re-diarization rebuild
|
||||||
|
/// (`reprocess_transcript`, FR-SPK): the old labels key to the previous
|
||||||
|
/// clustering and are meaningless once the audio is re-clustered, so the
|
||||||
|
/// caller re-inserts the fresh set via `finalize_meeting`. Without this,
|
||||||
|
/// stale labels from an over-split run linger in the DB (and the
|
||||||
|
/// Participants pane) even though the transcript no longer references them.
|
||||||
|
async fn clear_speakers(&self, id: &MeetingId) -> Result<(), StoreError>;
|
||||||
/// Fold over-split speaker labels into one canonical label (T4.5,
|
/// Fold over-split speaker labels into one canonical label (T4.5,
|
||||||
/// FR-SPK-3). Segment speaker IDs in storage are never rewritten
|
/// FR-SPK-3). Segment speaker IDs in storage are never rewritten
|
||||||
/// (FR-SPK-5) — `get_meeting` resolves `from` labels to `into` when it
|
/// (FR-SPK-5) — `get_meeting` resolves `from` labels to `into` when it
|
||||||
@@ -337,6 +363,16 @@ pub trait Store: Send + Sync {
|
|||||||
/// Manually rename a meeting — recordings otherwise default to "Untitled
|
/// Manually rename a meeting — recordings otherwise default to "Untitled
|
||||||
/// meeting" with no other way to change that.
|
/// meeting" with no other way to change that.
|
||||||
async fn rename_meeting(&self, meeting_id: &MeetingId, title: &str) -> Result<(), StoreError>;
|
async fn rename_meeting(&self, meeting_id: &MeetingId, title: &str) -> Result<(), StoreError>;
|
||||||
|
/// Overwrite a meeting's start/end timestamps — used by bundle import
|
||||||
|
/// (FR-STORE-4) so a moved recording keeps its original date rather than
|
||||||
|
/// showing the import time (`create_meeting`/`finalize_meeting` both stamp
|
||||||
|
/// "now").
|
||||||
|
async fn set_meeting_times(
|
||||||
|
&self,
|
||||||
|
meeting_id: &MeetingId,
|
||||||
|
started_at: i64,
|
||||||
|
ended_at: Option<i64>,
|
||||||
|
) -> Result<(), StoreError>;
|
||||||
/// Names a speaker AND links them to a known `Participant` (T6.5/T6.6,
|
/// Names a speaker AND links them to a known `Participant` (T6.5/T6.6,
|
||||||
/// FR-SPK-4): the display name comes from the participant record, and
|
/// FR-SPK-4): the display name comes from the participant record, and
|
||||||
/// the shared `participant_id` is what gives naming "continuity" across
|
/// the shared `participant_id` is what gives naming "continuity" across
|
||||||
@@ -718,7 +754,8 @@ impl Store for SqliteStore {
|
|||||||
let now = now_unix();
|
let now = now_unix();
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"UPDATE meetings SET status = 'ready', ended_at = ?, duration_secs = ?, recorded = ?,
|
"UPDATE meetings SET status = 'ready', ended_at = ?, duration_secs = ?, recorded = ?,
|
||||||
language = ?, backend_used = ?, model_used = ?, updated_at = ? WHERE id = ?",
|
language = ?, backend_used = ?, model_used = ?,
|
||||||
|
audio_layout = COALESCE(?, audio_layout), updated_at = ? WHERE id = ?",
|
||||||
)
|
)
|
||||||
.bind(now)
|
.bind(now)
|
||||||
.bind(s.duration_secs)
|
.bind(s.duration_secs)
|
||||||
@@ -726,6 +763,7 @@ impl Store for SqliteStore {
|
|||||||
.bind(&s.language)
|
.bind(&s.language)
|
||||||
.bind(&s.backend_used)
|
.bind(&s.backend_used)
|
||||||
.bind(&s.model_used)
|
.bind(&s.model_used)
|
||||||
|
.bind(&s.audio_layout)
|
||||||
.bind(now)
|
.bind(now)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
@@ -841,7 +879,7 @@ impl Store for SqliteStore {
|
|||||||
|
|
||||||
async fn get_meeting(&self, id: &MeetingId) -> Result<Meeting, StoreError> {
|
async fn get_meeting(&self, id: &MeetingId) -> Result<Meeting, StoreError> {
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
"SELECT id, title, started_at, ended_at, duration_secs, status, recorded, language, backend_used, model_used, calendar_event_id, template_id
|
"SELECT id, title, started_at, ended_at, duration_secs, status, recorded, language, backend_used, model_used, calendar_event_id, template_id, audio_layout
|
||||||
FROM meetings WHERE id = ?",
|
FROM meetings WHERE id = ?",
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
@@ -892,6 +930,14 @@ impl Store for SqliteStore {
|
|||||||
let summary = read_artifact(&folder.join("summary.json"))
|
let summary = read_artifact(&folder.join("summary.json"))
|
||||||
.and_then(|s| serde_json::from_str::<SummaryFile>(&s).ok());
|
.and_then(|s| serde_json::from_str::<SummaryFile>(&s).ok());
|
||||||
let tags = self.tags_for_meeting(id).await?;
|
let tags = self.tags_for_meeting(id).await?;
|
||||||
|
// Table is the source of truth for confirmed/edited items; fall back
|
||||||
|
// to the summary's parsed drafts only when nothing's been saved yet.
|
||||||
|
let mut action_items = self.list_action_items(id).await?;
|
||||||
|
if action_items.is_empty() {
|
||||||
|
if let Some(s) = &summary {
|
||||||
|
action_items = s.action_items.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Meeting {
|
Ok(Meeting {
|
||||||
id: row.get("id"),
|
id: row.get("id"),
|
||||||
@@ -904,6 +950,7 @@ impl Store for SqliteStore {
|
|||||||
language: row.get("language"),
|
language: row.get("language"),
|
||||||
backend_used: row.get("backend_used"),
|
backend_used: row.get("backend_used"),
|
||||||
model_used: row.get("model_used"),
|
model_used: row.get("model_used"),
|
||||||
|
audio_layout: row.get("audio_layout"),
|
||||||
segments,
|
segments,
|
||||||
speakers,
|
speakers,
|
||||||
notes_markdown,
|
notes_markdown,
|
||||||
@@ -911,6 +958,7 @@ impl Store for SqliteStore {
|
|||||||
calendar_event_id: row.get("calendar_event_id"),
|
calendar_event_id: row.get("calendar_event_id"),
|
||||||
tags,
|
tags,
|
||||||
template_id: row.get("template_id"),
|
template_id: row.get("template_id"),
|
||||||
|
action_items,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -932,6 +980,16 @@ impl Store for SqliteStore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn set_meeting_status(&self, id: &MeetingId, status: &str) -> Result<(), StoreError> {
|
||||||
|
sqlx::query("UPDATE meetings SET status = ?, updated_at = ? WHERE id = ?")
|
||||||
|
.bind(status)
|
||||||
|
.bind(now_unix())
|
||||||
|
.bind(id)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn update_notes(&self, id: &MeetingId, markdown: &str) -> Result<(), StoreError> {
|
async fn update_notes(&self, id: &MeetingId, markdown: &str) -> Result<(), StoreError> {
|
||||||
write_artifact(
|
write_artifact(
|
||||||
&paths::meeting_dir(id).join("notes.md"),
|
&paths::meeting_dir(id).join("notes.md"),
|
||||||
@@ -1196,6 +1254,22 @@ impl Store for SqliteStore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn set_meeting_times(
|
||||||
|
&self,
|
||||||
|
meeting_id: &MeetingId,
|
||||||
|
started_at: i64,
|
||||||
|
ended_at: Option<i64>,
|
||||||
|
) -> Result<(), StoreError> {
|
||||||
|
sqlx::query("UPDATE meetings SET started_at = ?, ended_at = ?, updated_at = ? WHERE id = ?")
|
||||||
|
.bind(started_at)
|
||||||
|
.bind(ended_at)
|
||||||
|
.bind(now_unix())
|
||||||
|
.bind(meeting_id)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn map_speaker_to_participant(
|
async fn map_speaker_to_participant(
|
||||||
&self,
|
&self,
|
||||||
meeting_id: &MeetingId,
|
meeting_id: &MeetingId,
|
||||||
@@ -1211,6 +1285,14 @@ impl Store for SqliteStore {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn clear_speakers(&self, id: &MeetingId) -> Result<(), StoreError> {
|
||||||
|
sqlx::query("DELETE FROM speakers WHERE meeting_id = ?")
|
||||||
|
.bind(id)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn merge_speakers(
|
async fn merge_speakers(
|
||||||
&self,
|
&self,
|
||||||
id: &MeetingId,
|
id: &MeetingId,
|
||||||
@@ -1254,6 +1336,26 @@ impl Store for SqliteStore {
|
|||||||
items: &[ActionItem],
|
items: &[ActionItem],
|
||||||
) -> Result<Vec<ActionItem>, StoreError> {
|
) -> Result<Vec<ActionItem>, StoreError> {
|
||||||
let now = now_unix();
|
let now = now_unix();
|
||||||
|
// Reconcile deletes: any row previously saved for this meeting that the
|
||||||
|
// caller no longer includes was removed in the UI (FR-LLM-3). Action
|
||||||
|
// items per meeting number in the low tens, so a per-row delete loop is
|
||||||
|
// fine. ponytail: O(n) delete scan, batch it only if n ever gets large.
|
||||||
|
let keep: std::collections::HashSet<&str> =
|
||||||
|
items.iter().filter_map(|i| i.id.as_deref()).collect();
|
||||||
|
let existing_ids: Vec<String> =
|
||||||
|
sqlx::query_scalar("SELECT id FROM action_items WHERE meeting_id = ?")
|
||||||
|
.bind(id)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
for eid in &existing_ids {
|
||||||
|
if !keep.contains(eid.as_str()) {
|
||||||
|
sqlx::query("DELETE FROM action_items WHERE id = ? AND meeting_id = ?")
|
||||||
|
.bind(eid)
|
||||||
|
.bind(id)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut saved = Vec::with_capacity(items.len());
|
let mut saved = Vec::with_capacity(items.len());
|
||||||
for item in items {
|
for item in items {
|
||||||
let item_id = match &item.id {
|
let item_id = match &item.id {
|
||||||
@@ -1954,6 +2056,7 @@ mod tests {
|
|||||||
language: Some("fr".to_string()), // what auto-detect resolved to
|
language: Some("fr".to_string()), // what auto-detect resolved to
|
||||||
backend_used: Some("cpu".to_string()),
|
backend_used: Some("cpu".to_string()),
|
||||||
model_used: Some("small-q5_1".to_string()),
|
model_used: Some("small-q5_1".to_string()),
|
||||||
|
audio_layout: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1997,6 +2100,7 @@ mod tests {
|
|||||||
language: Some("en".to_string()),
|
language: Some("en".to_string()),
|
||||||
backend_used: Some("cpu".to_string()),
|
backend_used: Some("cpu".to_string()),
|
||||||
model_used: Some("small-q5_1".to_string()),
|
model_used: Some("small-q5_1".to_string()),
|
||||||
|
audio_layout: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "WhispAssist",
|
"productName": "WhispAssist",
|
||||||
"version": "0.4.0",
|
"version": "0.7.2",
|
||||||
"identifier": "bet.dou.whispassist",
|
"identifier": "bet.dou.whispassist",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
@@ -22,18 +22,18 @@
|
|||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": "default-src 'self'; connect-src 'self' http://localhost:* http://127.0.0.1:*; img-src 'self' data:; media-src 'self' http://waaudio.localhost; style-src 'self' 'unsafe-inline'"
|
"csp": "default-src 'self'; connect-src 'self' http://localhost:* http://127.0.0.1:*; img-src 'self' data:; media-src 'self' http://waaudio.localhost; style-src 'self' 'unsafe-inline'"
|
||||||
},
|
|
||||||
"trayIcon": {
|
|
||||||
"iconPath": "icons/tray.png",
|
|
||||||
"tooltip": "WhispAssist"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": ["msi", "nsis"],
|
"targets": ["msi", "nsis"],
|
||||||
"icon": ["icons/icon.ico"],
|
"icon": ["icons/icon.ico"],
|
||||||
|
"resources": ["wa-defaults.ini"],
|
||||||
"windows": {
|
"windows": {
|
||||||
"webviewInstallMode": { "type": "downloadBootstrapper" }
|
"webviewInstallMode": { "type": "downloadBootstrapper" },
|
||||||
|
"nsis": {
|
||||||
|
"installMode": "both"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "gen/schemas/desktop-schema.json",
|
"$schema": "gen/schemas/desktop-schema.json",
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"resources": ["vulkan-1.dll"]
|
"resources": ["vulkan-1.dll", "wa-defaults.ini"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
; ============================================================================
|
||||||
|
; WhispAssist enterprise deployment defaults (wa-defaults.ini)
|
||||||
|
; ============================================================================
|
||||||
|
; Read ONCE, on a machine's FIRST launch (before settings.json exists), to
|
||||||
|
; seed the app's default settings. After that the user's own settings.json
|
||||||
|
; wins and this file is ignored. Deploy it with native Windows tooling
|
||||||
|
; (Group Policy / SCCM / Intune file copy) to either location — the first
|
||||||
|
; one found wins:
|
||||||
|
;
|
||||||
|
; 1. %PROGRAMDATA%\WhispAssist\wa-defaults.ini (machine-wide)
|
||||||
|
; 2. <install dir>\wa-defaults.ini (this bundled template)
|
||||||
|
;
|
||||||
|
; As shipped, every setting below is COMMENTED OUT, so a normal install
|
||||||
|
; behaves exactly as if this file were absent. Uncomment and edit the lines
|
||||||
|
; you want to preset. Format is flat "key = value" — no sections required;
|
||||||
|
; [section] headers, ';' and '#' comment lines are ignored.
|
||||||
|
;
|
||||||
|
; Values: true / false for switches; a plain number for numeric fields;
|
||||||
|
; text otherwise. Unknown / misspelled keys are ignored.
|
||||||
|
;
|
||||||
|
; SECURITY: never put secrets here. API keys, OAuth tokens and sync
|
||||||
|
; passwords live only in the OS credential store; any key containing
|
||||||
|
; "key", "token", "secret", "credential" or "password" is dropped on read.
|
||||||
|
; ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
; ---- Recording (ADR-0009) --------------------------------------------------
|
||||||
|
; Record every meeting to disk by default (consent notice still applies).
|
||||||
|
; default_record = false
|
||||||
|
|
||||||
|
; ---- Transcription backend & model -----------------------------------------
|
||||||
|
; preferred_backend = auto ; auto | npu | nvidia | amd | intel | cpu
|
||||||
|
; whisper_model = base.en-q5_1 ; a catalog model id
|
||||||
|
; whisper_language = auto ; auto | ISO-639-1 code (multilingual model only)
|
||||||
|
; low_overhead = false
|
||||||
|
|
||||||
|
; Download whisper_model in the background on first launch so the machine is
|
||||||
|
; ready offline. Requires network at first run.
|
||||||
|
; auto_download_model = false
|
||||||
|
|
||||||
|
; ---- Storage & retention (FR-STORE-2) --------------------------------------
|
||||||
|
; storage_root = C:\ProgramData\WhispAssist\data
|
||||||
|
; retention_max_age_days = 90
|
||||||
|
; retention_max_size_gb = 20
|
||||||
|
|
||||||
|
; ---- Local LLM / summaries (ADR-0007) --------------------------------------
|
||||||
|
; llm_provider = ollama ; ollama | custom | anthropic | off
|
||||||
|
; llm_endpoint = http://localhost:11434
|
||||||
|
; llm_model = llama3
|
||||||
|
|
||||||
|
; ---- Capture & UX ----------------------------------------------------------
|
||||||
|
; microphone_enabled = true
|
||||||
|
; auto_record_calendar = false
|
||||||
|
; theme = system ; system | light | dark
|
||||||
|
|
||||||
|
; ---- Startup (NFR-RES-4) ---------------------------------------------------
|
||||||
|
; Launch WhispAssist automatically at login for the user.
|
||||||
|
; auto_start = false
|
||||||
|
|
||||||
|
; ---- Sync master switch (ADR-0010; targets/creds configured in-app) --------
|
||||||
|
; sync_enabled = false
|
||||||
+118
-67
@@ -12,17 +12,21 @@
|
|||||||
import { recording } from "./lib/stores/recording.svelte";
|
import { recording } from "./lib/stores/recording.svelte";
|
||||||
import { settings } from "./lib/stores/settings.svelte";
|
import { settings } from "./lib/stores/settings.svelte";
|
||||||
import { meetings } from "./lib/stores/meetings.svelte";
|
import { meetings } from "./lib/stores/meetings.svelte";
|
||||||
|
import { imports } from "./lib/stores/imports.svelte";
|
||||||
import { calendar } from "./lib/stores/calendar.svelte";
|
import { calendar } from "./lib/stores/calendar.svelte";
|
||||||
import { api, type NoteTemplate, type CalendarEvent } from "./lib/api";
|
import { api, type NoteTemplate, type CalendarEvent } from "./lib/api";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import ThemeToggle from "./lib/components/ThemeToggle.svelte";
|
import ThemeToggle from "./lib/components/ThemeToggle.svelte";
|
||||||
import Splitter from "./lib/components/Splitter.svelte";
|
import Splitter from "./lib/components/Splitter.svelte";
|
||||||
import { layout, clamp } from "./lib/stores/layout.svelte";
|
import { layout, clamp } from "./lib/stores/layout.svelte";
|
||||||
|
import { t } from "./lib/i18n/index.svelte";
|
||||||
import {
|
import {
|
||||||
Circle,
|
Circle,
|
||||||
Square,
|
Square,
|
||||||
Trash2,
|
Trash2,
|
||||||
FilePlus,
|
FilePlus,
|
||||||
|
Mic,
|
||||||
|
MicOff,
|
||||||
Settings as SettingsIcon,
|
Settings as SettingsIcon,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
@@ -62,7 +66,7 @@
|
|||||||
vaultLocked = false;
|
vaultLocked = false;
|
||||||
meetings.load(); // refresh now that encrypted content is readable
|
meetings.load(); // refresh now that encrypted content is readable
|
||||||
} catch {
|
} catch {
|
||||||
vaultErr = "Incorrect password";
|
vaultErr = t("app.vault_incorrect");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,11 +134,12 @@
|
|||||||
recording.init();
|
recording.init();
|
||||||
settings.load();
|
settings.load();
|
||||||
meetings.init();
|
meetings.init();
|
||||||
|
imports.init(); // live background-import progress for the tracker
|
||||||
calendar.load(); // events power the auto-record timer above
|
calendar.load(); // events power the auto-record timer above
|
||||||
checkVault();
|
checkVault();
|
||||||
api
|
api
|
||||||
.listNoteTemplates()
|
.listNoteTemplates()
|
||||||
.then((t) => (noteTemplates = t))
|
.then((tpls) => (noteTemplates = tpls))
|
||||||
.catch(() => (noteTemplates = []));
|
.catch(() => (noteTemplates = []));
|
||||||
|
|
||||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
@@ -160,7 +165,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function cancelRecording() {
|
async function cancelRecording() {
|
||||||
if (!confirm("Discard this recording? Its audio and transcript will be deleted.")) return;
|
if (!confirm(t("app.discard_confirm"))) return;
|
||||||
await recording.cancel();
|
await recording.cancel();
|
||||||
meetings.deselect();
|
meetings.deselect();
|
||||||
}
|
}
|
||||||
@@ -186,9 +191,9 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
const current = recording.state;
|
const current = recording.state;
|
||||||
if (current !== previousRecordingState) {
|
if (current !== previousRecordingState) {
|
||||||
if (current === "recording") recordingAnnouncement = "Recording started";
|
if (current === "recording") recordingAnnouncement = t("app.announce_started");
|
||||||
else if (current === "paused") recordingAnnouncement = "Recording paused";
|
else if (current === "paused") recordingAnnouncement = t("app.announce_paused");
|
||||||
else if (previousRecordingState !== "idle") recordingAnnouncement = "Recording stopped";
|
else if (previousRecordingState !== "idle") recordingAnnouncement = t("app.announce_stopped");
|
||||||
previousRecordingState = current;
|
previousRecordingState = current;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -206,6 +211,18 @@
|
|||||||
} else if (e.ctrlKey && e.key === ",") {
|
} else if (e.ctrlKey && e.key === ",") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
showSettings = !showSettings;
|
showSettings = !showSettings;
|
||||||
|
} else if (
|
||||||
|
// Press "M" to mute/unmute the mic mid-meeting (FR-CAP-7). Bare key (no
|
||||||
|
// modifiers) and only while recording with the mic on.
|
||||||
|
e.key.toLowerCase() === "m" &&
|
||||||
|
!e.ctrlKey &&
|
||||||
|
!e.metaKey &&
|
||||||
|
!e.altKey &&
|
||||||
|
recording.state !== "idle" &&
|
||||||
|
settings.settings.microphone_enabled
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
recording.toggleMute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,59 +248,56 @@
|
|||||||
<div class="app" data-theme={resolvedTheme}>
|
<div class="app" data-theme={resolvedTheme}>
|
||||||
<div class="sr-only" role="status" aria-live="polite">{recordingAnnouncement}</div>
|
<div class="sr-only" role="status" aria-live="polite">{recordingAnnouncement}</div>
|
||||||
<header class="bar">
|
<header class="bar">
|
||||||
<strong>WhispAssist</strong>
|
|
||||||
<span class="muted">local · private</span>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
{#if recording.state === "idle"}
|
{#if recording.state === "idle"}
|
||||||
<select
|
|
||||||
class="theme-select"
|
|
||||||
bind:value={selectedTemplateId}
|
|
||||||
aria-label="Note template"
|
|
||||||
title="Note template"
|
|
||||||
>
|
|
||||||
<option value="">No template</option>
|
|
||||||
{#each noteTemplates as t (t.id)}
|
|
||||||
<option value={t.id}>{t.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
class="import-btn"
|
|
||||||
onclick={() => (showImport = true)}
|
|
||||||
title="Add a meeting from a file or URL"
|
|
||||||
>
|
|
||||||
<FilePlus size={13} aria-hidden="true" />
|
|
||||||
Add meeting
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
class="record-btn"
|
class="record-btn"
|
||||||
onclick={startRecording}
|
onclick={startRecording}
|
||||||
title="Start recording (Ctrl+Shift+R)"
|
title={t("app.record_title")}
|
||||||
aria-keyshortcuts="Control+Shift+R"
|
aria-keyshortcuts="Control+Shift+R"
|
||||||
>
|
>
|
||||||
<Circle size={11} fill="currentColor" aria-hidden="true" />
|
<Circle size={11} fill="currentColor" aria-hidden="true" />
|
||||||
Record
|
{t("app.record")}
|
||||||
</button>
|
</button>
|
||||||
|
<select
|
||||||
|
class="theme-select"
|
||||||
|
bind:value={selectedTemplateId}
|
||||||
|
aria-label={t("app.note_template")}
|
||||||
|
title={t("app.note_template")}
|
||||||
|
>
|
||||||
|
<option value="">{t("app.no_template")}</option>
|
||||||
|
{#each noteTemplates as tpl (tpl.id)}
|
||||||
|
<option value={tpl.id}>{tpl.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
class="stop-btn"
|
class="stop-btn"
|
||||||
onclick={() => recording.stop()}
|
onclick={() => recording.stop()}
|
||||||
title="Stop recording (Ctrl+Shift+R)"
|
title={t("app.stop_title")}
|
||||||
aria-keyshortcuts="Control+Shift+R"
|
aria-keyshortcuts="Control+Shift+R"
|
||||||
>
|
>
|
||||||
<Square size={11} fill="currentColor" aria-hidden="true" />
|
<Square size={11} fill="currentColor" aria-hidden="true" />
|
||||||
Stop
|
{t("app.stop")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="cancel-btn" onclick={cancelRecording} title={t("app.cancel_title")}>
|
||||||
class="cancel-btn"
|
|
||||||
onclick={cancelRecording}
|
|
||||||
title="Discard this recording and delete it"
|
|
||||||
>
|
|
||||||
<Trash2 size={12} aria-hidden="true" />
|
<Trash2 size={12} aria-hidden="true" />
|
||||||
Cancel
|
{t("app.cancel")}
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
|
<div class="spacer"></div>
|
||||||
|
{#if recording.state === "idle"}
|
||||||
|
<button
|
||||||
|
class="import-btn"
|
||||||
|
onclick={() => (showImport = true)}
|
||||||
|
title={t("app.add_meeting_title")}
|
||||||
|
>
|
||||||
|
<FilePlus size={13} aria-hidden="true" />
|
||||||
|
{t("app.add_meeting")}
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
<span class="rec">
|
<span class="rec">
|
||||||
<span class="rec-dot" aria-hidden="true"></span>
|
<span class="rec-dot" aria-hidden="true"></span>
|
||||||
Recording…
|
{t("app.recording")}
|
||||||
</span>
|
</span>
|
||||||
<LevelMeter
|
<LevelMeter
|
||||||
rms={recording.levelRms}
|
rms={recording.levelRms}
|
||||||
@@ -292,17 +306,34 @@
|
|||||||
micPeak={recording.levelPeakMic}
|
micPeak={recording.levelPeakMic}
|
||||||
showMic={settings.settings.microphone_enabled}
|
showMic={settings.settings.microphone_enabled}
|
||||||
/>
|
/>
|
||||||
{#if settings.hardware}
|
{#if settings.settings.microphone_enabled}
|
||||||
<span class="backend" title="Active transcription backend">{settings.hardware.active}</span>
|
<button
|
||||||
|
class="mute-btn"
|
||||||
|
class:muted={recording.micMuted}
|
||||||
|
onclick={() => recording.toggleMute()}
|
||||||
|
aria-pressed={recording.micMuted}
|
||||||
|
aria-keyshortcuts="M"
|
||||||
|
title={recording.micMuted ? t("app.unmute_title") : t("app.mute_title")}
|
||||||
|
>
|
||||||
|
{#if recording.micMuted}
|
||||||
|
<MicOff size={14} aria-hidden="true" />
|
||||||
|
{:else}
|
||||||
|
<Mic size={14} aria-hidden="true" />
|
||||||
{/if}
|
{/if}
|
||||||
<label class="retention" title="Save audio as .wav for this meeting">
|
<span class="sr-only">{recording.micMuted ? t("app.unmute") : t("app.mute")}</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if settings.hardware}
|
||||||
|
<span class="backend" title={t("app.backend_title")}>{settings.hardware.active}</span>
|
||||||
|
{/if}
|
||||||
|
<label class="retention" title={t("app.retention_title")}>
|
||||||
<input type="checkbox" checked={recording.retention} onchange={onToggleRetention} />
|
<input type="checkbox" checked={recording.retention} onchange={onToggleRetention} />
|
||||||
<span>{recording.retention ? "saving" : "not saved"}</span>
|
<span>{recording.retention ? t("app.saving") : t("app.not_saved")}</span>
|
||||||
</label>
|
</label>
|
||||||
{#if recording.deviceNotice}
|
{#if recording.deviceNotice}
|
||||||
<span class="device-notice" role="status" title={recording.deviceNotice}>
|
<span class="device-notice" role="status" title={recording.deviceNotice}>
|
||||||
<AlertTriangle size={14} aria-hidden="true" />
|
<AlertTriangle size={14} aria-hidden="true" />
|
||||||
reconnecting audio device…
|
{t("app.reconnecting")}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -312,8 +343,8 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="icon"
|
class="icon"
|
||||||
aria-label="Settings"
|
aria-label={t("app.settings")}
|
||||||
title="Settings (Ctrl+,)"
|
title={t("app.settings_title")}
|
||||||
aria-keyshortcuts="Control+,"
|
aria-keyshortcuts="Control+,"
|
||||||
onclick={() => (showSettings = !showSettings)}
|
onclick={() => (showSettings = !showSettings)}
|
||||||
>
|
>
|
||||||
@@ -326,7 +357,7 @@
|
|||||||
class="consent-overlay"
|
class="consent-overlay"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Recording consent"
|
aria-label={t("app.consent_dialog")}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
use:trapFocus
|
use:trapFocus
|
||||||
>
|
>
|
||||||
@@ -347,23 +378,27 @@
|
|||||||
class="vault-overlay"
|
class="vault-overlay"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Unlock encryption vault"
|
aria-label={t("app.vault_dialog")}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
use:trapFocus
|
use:trapFocus
|
||||||
>
|
>
|
||||||
<div class="vault-card">
|
<div class="vault-card">
|
||||||
<h2>Unlock encryption vault</h2>
|
<h2>{t("app.vault_title")}</h2>
|
||||||
<p class="muted">Your meetings are encrypted at rest. Enter your password to read them.</p>
|
<p class="muted">{t("app.vault_desc")}</p>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Vault password"
|
placeholder={t("app.vault_password")}
|
||||||
bind:value={vaultPw}
|
bind:value={vaultPw}
|
||||||
onkeydown={(e) => e.key === "Enter" && submitUnlock()}
|
onkeydown={(e) => e.key === "Enter" && submitUnlock()}
|
||||||
/>
|
/>
|
||||||
{#if vaultErr}<p class="vault-err">{vaultErr}</p>{/if}
|
{#if vaultErr}<p class="vault-err">{vaultErr}</p>{/if}
|
||||||
<div class="vault-actions">
|
<div class="vault-actions">
|
||||||
<button class="primary" onclick={submitUnlock} disabled={!vaultPw}>Unlock</button>
|
<button class="primary" onclick={submitUnlock} disabled={!vaultPw}
|
||||||
<button class="link" onclick={() => (vaultLocked = false)}>Continue locked</button>
|
>{t("app.unlock")}</button
|
||||||
|
>
|
||||||
|
<button class="link" onclick={() => (vaultLocked = false)}
|
||||||
|
>{t("app.continue_locked")}</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -385,8 +420,8 @@
|
|||||||
layout.leftCollapsed = false;
|
layout.leftCollapsed = false;
|
||||||
layout.persist();
|
layout.persist();
|
||||||
}}
|
}}
|
||||||
title="Show meetings list"
|
title={t("app.show_meetings")}
|
||||||
aria-label="Show meetings list"
|
aria-label={t("app.show_meetings")}
|
||||||
>
|
>
|
||||||
<PanelLeftOpen size={16} aria-hidden="true" />
|
<PanelLeftOpen size={16} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@@ -397,8 +432,8 @@
|
|||||||
layout.leftCollapsed = true;
|
layout.leftCollapsed = true;
|
||||||
layout.persist();
|
layout.persist();
|
||||||
}}
|
}}
|
||||||
title="Hide meetings list"
|
title={t("app.hide_meetings")}
|
||||||
aria-label="Hide meetings list"
|
aria-label={t("app.hide_meetings")}
|
||||||
>
|
>
|
||||||
<PanelLeftClose size={14} aria-hidden="true" />
|
<PanelLeftClose size={14} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@@ -407,7 +442,7 @@
|
|||||||
</aside>
|
</aside>
|
||||||
{#if !layout.leftCollapsed}
|
{#if !layout.leftCollapsed}
|
||||||
<Splitter
|
<Splitter
|
||||||
label="Resize meetings list"
|
label={t("app.resize_meetings")}
|
||||||
onResize={(d) => (layout.leftWidth = clamp(layout.leftWidth + d, 200, 480))}
|
onResize={(d) => (layout.leftWidth = clamp(layout.leftWidth + d, 200, 480))}
|
||||||
onResizeEnd={() => layout.persist()}
|
onResizeEnd={() => layout.persist()}
|
||||||
/>
|
/>
|
||||||
@@ -417,7 +452,7 @@
|
|||||||
<section class="center"><TranscriptNotes /></section>
|
<section class="center"><TranscriptNotes /></section>
|
||||||
{#if !layout.rightCollapsed}
|
{#if !layout.rightCollapsed}
|
||||||
<Splitter
|
<Splitter
|
||||||
label="Resize summary panel"
|
label={t("app.resize_summary")}
|
||||||
onResize={(d) => (layout.rightWidth = clamp(layout.rightWidth - d, 240, 560))}
|
onResize={(d) => (layout.rightWidth = clamp(layout.rightWidth - d, 240, 560))}
|
||||||
onResizeEnd={() => layout.persist()}
|
onResizeEnd={() => layout.persist()}
|
||||||
/>
|
/>
|
||||||
@@ -432,8 +467,8 @@
|
|||||||
layout.rightCollapsed = false;
|
layout.rightCollapsed = false;
|
||||||
layout.persist();
|
layout.persist();
|
||||||
}}
|
}}
|
||||||
title="Show summary panel"
|
title={t("app.show_summary")}
|
||||||
aria-label="Show summary panel"
|
aria-label={t("app.show_summary")}
|
||||||
>
|
>
|
||||||
<PanelRightOpen size={16} aria-hidden="true" />
|
<PanelRightOpen size={16} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@@ -444,8 +479,8 @@
|
|||||||
layout.rightCollapsed = true;
|
layout.rightCollapsed = true;
|
||||||
layout.persist();
|
layout.persist();
|
||||||
}}
|
}}
|
||||||
title="Hide summary panel"
|
title={t("app.hide_summary")}
|
||||||
aria-label="Hide summary panel"
|
aria-label={t("app.hide_summary")}
|
||||||
>
|
>
|
||||||
<PanelRightClose size={14} aria-hidden="true" />
|
<PanelRightClose size={14} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@@ -587,10 +622,6 @@
|
|||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
background: var(--bg-elevated);
|
background: var(--bg-elevated);
|
||||||
}
|
}
|
||||||
.bar strong {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}
|
|
||||||
.spacer {
|
.spacer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
@@ -717,6 +748,26 @@
|
|||||||
border-radius: var(--radius-full);
|
border-radius: var(--radius-full);
|
||||||
padding: 0.15rem 0.5rem;
|
padding: 0.15rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
.mute-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
color: var(--fg);
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.mute-btn:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
.mute-btn.muted {
|
||||||
|
color: var(--danger, #d33);
|
||||||
|
border-color: var(--danger, #d33);
|
||||||
|
background: color-mix(in srgb, var(--danger, #d33) 12%, transparent);
|
||||||
|
}
|
||||||
.retention {
|
.retention {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
+87
-7
@@ -56,6 +56,19 @@ export interface AudioDeviceInfo {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Quick hardware stress test (Settings ▸ Hardware): per-(backend, model)
|
||||||
|
// real-time factor, plus the recommended real-time-capable pairing.
|
||||||
|
export interface StressResult {
|
||||||
|
backend: string;
|
||||||
|
model: string;
|
||||||
|
rtf: number;
|
||||||
|
realtime: boolean;
|
||||||
|
}
|
||||||
|
export interface StressTestResult {
|
||||||
|
results: StressResult[];
|
||||||
|
recommended: { backend: string; model: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LlmStatus {
|
export interface LlmStatus {
|
||||||
provider: string; // ollama|custom|anthropic|off (ADR-0011; "openai" not yet wired)
|
provider: string; // ollama|custom|anthropic|off (ADR-0011; "openai" not yet wired)
|
||||||
reachable: boolean;
|
reachable: boolean;
|
||||||
@@ -83,6 +96,19 @@ export interface LanguageOption {
|
|||||||
|
|
||||||
export type MeetingStatus = "recording" | "transcribing" | "ready" | "recovering" | "error";
|
export type MeetingStatus = "recording" | "transcribing" | "ready" | "recovering" | "error";
|
||||||
|
|
||||||
|
// The four ordered phases of a background media import (import://progress).
|
||||||
|
export type ImportPhase = "prepare" | "transcribe" | "diarize" | "finalize";
|
||||||
|
|
||||||
|
// One `import://progress` tick. `state` is active (running), done (finished,
|
||||||
|
// `elapsedMs` set) or error (`error` message set) for the given `phase`.
|
||||||
|
export interface ImportProgress {
|
||||||
|
meetingId: MeetingId;
|
||||||
|
phase: ImportPhase;
|
||||||
|
state: "active" | "done" | "error";
|
||||||
|
elapsedMs: number | null;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MeetingListItem {
|
export interface MeetingListItem {
|
||||||
id: MeetingId;
|
id: MeetingId;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -172,6 +198,10 @@ export interface Meeting {
|
|||||||
speakers: SpeakerInfo[];
|
speakers: SpeakerInfo[];
|
||||||
notes_markdown: string;
|
notes_markdown: string;
|
||||||
summary: SummaryFile | null;
|
summary: SummaryFile | null;
|
||||||
|
// Confirmed/edited action items (table-backed source of truth) — falls back
|
||||||
|
// to summary.action_items drafts until anything is saved. Manage via
|
||||||
|
// confirmActionItems (add/edit/delete).
|
||||||
|
action_items: ActionItem[];
|
||||||
calendar_event_id: string | null;
|
calendar_event_id: string | null;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
template_id: string | null;
|
template_id: string | null;
|
||||||
@@ -332,6 +362,12 @@ export interface AppSettings {
|
|||||||
audio_output_device: string | null;
|
audio_output_device: string | null;
|
||||||
microphone_enabled: boolean;
|
microphone_enabled: boolean;
|
||||||
audio_input_device: string | null;
|
audio_input_device: string | null;
|
||||||
|
/** Launch WhispAssist at login (opt-in, off by default; NFR-RES-4). Toggled
|
||||||
|
* via setAutoStart, which writes a per-user Run entry (no admin). */
|
||||||
|
auto_start: boolean;
|
||||||
|
/** Closing the window hides to the tray (keep running in background) instead
|
||||||
|
* of quitting; on by default. Tray "Quit" is the real exit. */
|
||||||
|
close_to_tray: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feature brief — agent-ready spec distilled from a meeting (ADR-0011).
|
// Feature brief — agent-ready spec distilled from a meeting (ADR-0011).
|
||||||
@@ -394,6 +430,10 @@ export const api = {
|
|||||||
invoke<string>("recording_playback_path", { meetingId }),
|
invoke<string>("recording_playback_path", { meetingId }),
|
||||||
pauseRecording: (meetingId: MeetingId) => invoke<void>("pause_recording", { meetingId }),
|
pauseRecording: (meetingId: MeetingId) => invoke<void>("pause_recording", { meetingId }),
|
||||||
resumeRecording: (meetingId: MeetingId) => invoke<void>("resume_recording", { meetingId }),
|
resumeRecording: (meetingId: MeetingId) => invoke<void>("resume_recording", { meetingId }),
|
||||||
|
// Toggle mic mute for the active recording (FR-CAP-7); returns the new muted
|
||||||
|
// state. Errors if the meeting was started with the mic off.
|
||||||
|
toggleMicrophoneMute: (meetingId: MeetingId) =>
|
||||||
|
invoke<boolean>("toggle_microphone_mute", { meetingId }),
|
||||||
setRecordingRetention: (meetingId: MeetingId, record: boolean) =>
|
setRecordingRetention: (meetingId: MeetingId, record: boolean) =>
|
||||||
invoke<void>("set_recording_retention", { meetingId, record }),
|
invoke<void>("set_recording_retention", { meetingId, record }),
|
||||||
acknowledgeRecordingConsent: () => invoke<void>("acknowledge_recording_consent"),
|
acknowledgeRecordingConsent: () => invoke<void>("acknowledge_recording_consent"),
|
||||||
@@ -413,6 +453,11 @@ export const api = {
|
|||||||
listInputDevices: () => invoke<AudioDeviceInfo[]>("list_input_devices"),
|
listInputDevices: () => invoke<AudioDeviceInfo[]>("list_input_devices"),
|
||||||
setPreferredBackend: (backend: BackendId | "auto") =>
|
setPreferredBackend: (backend: BackendId | "auto") =>
|
||||||
invoke<void>("set_preferred_backend", { args: { backend } }),
|
invoke<void>("set_preferred_backend", { args: { backend } }),
|
||||||
|
setAutoStart: (enabled: boolean) => invoke<void>("set_auto_start", { enabled }),
|
||||||
|
// Test a device: stream device://level for a few seconds. Resolves when done.
|
||||||
|
monitorAudioLevel: (kind: "input" | "loopback", deviceId: string | null, durationMs = 6000) =>
|
||||||
|
invoke<void>("monitor_audio_level", { kind, deviceId, durationMs }),
|
||||||
|
stressTestHardware: () => invoke<StressTestResult>("stress_test_hardware"),
|
||||||
downloadNpuPackage: () => invoke<void>("download_npu_package"),
|
downloadNpuPackage: () => invoke<void>("download_npu_package"),
|
||||||
downloadDirectmlPackage: () => invoke<void>("download_directml_package"),
|
downloadDirectmlPackage: () => invoke<void>("download_directml_package"),
|
||||||
listModels: () => invoke<ModelInfo[]>("list_models"),
|
listModels: () => invoke<ModelInfo[]>("list_models"),
|
||||||
@@ -433,10 +478,12 @@ export const api = {
|
|||||||
invoke<void>("reprocess_transcript", { meetingId, model, language }),
|
invoke<void>("reprocess_transcript", { meetingId, model, language }),
|
||||||
// Manually add a meeting from an existing recording — a local audio/video
|
// Manually add a meeting from an existing recording — a local audio/video
|
||||||
// file path or a URL (YouTube/streaming page or direct media URL). Requires
|
// file path or a URL (YouTube/streaming page or direct media URL). Requires
|
||||||
// ffmpeg (and yt-dlp for URLs) on PATH; neither is bundled. Returns the new
|
// ffmpeg (and yt-dlp for URLs) on PATH; neither is bundled. `model` overrides
|
||||||
// meeting's id once transcription + diarization have finished.
|
// the Settings whisper model for this one import. Returns the new meeting's id
|
||||||
importMedia: (source: string, title?: string) =>
|
// *immediately*; transcode/transcribe/diarize run in the background and stream
|
||||||
invoke<MeetingId>("import_media", { source, title }),
|
// `import://progress` ticks, finishing with `transcript://finalized`.
|
||||||
|
importMedia: (source: string, title?: string, model?: string) =>
|
||||||
|
invoke<MeetingId>("import_media", { source, title, model }),
|
||||||
resumeTranscription: (meetingId: MeetingId) =>
|
resumeTranscription: (meetingId: MeetingId) =>
|
||||||
invoke<void>("resume_transcription", { meetingId }),
|
invoke<void>("resume_transcription", { meetingId }),
|
||||||
listMeetings: (filter?: MeetingFilter) =>
|
listMeetings: (filter?: MeetingFilter) =>
|
||||||
@@ -456,9 +503,17 @@ export const api = {
|
|||||||
deleteMeeting: (meetingId: MeetingId) => invoke<void>("delete_meeting", { meetingId }),
|
deleteMeeting: (meetingId: MeetingId) => invoke<void>("delete_meeting", { meetingId }),
|
||||||
updateNotes: (meetingId: MeetingId, markdown: string) =>
|
updateNotes: (meetingId: MeetingId, markdown: string) =>
|
||||||
invoke<void>("update_notes", { meetingId, markdown }),
|
invoke<void>("update_notes", { meetingId, markdown }),
|
||||||
// dest is a file path for md/pdf/docx, a folder for bundle.
|
// AI-enhance rough notes into structured Markdown grounded in the transcript
|
||||||
exportMeeting: (meetingId: MeetingId, dest: string, format: "md" | "pdf" | "docx" | "bundle") =>
|
// (Granola-style). Returns the enhanced text; the caller decides to keep it.
|
||||||
invoke<string>("export_meeting", { meetingId, dest, format }),
|
enhanceNotes: (meetingId: MeetingId, notes: string) =>
|
||||||
|
invoke<string>("enhance_notes", { meetingId, notes }),
|
||||||
|
// dest is a file path for md/pdf/docx/obsidian, a folder for bundle.
|
||||||
|
// "obsidian" writes one self-contained vault note (no audio) — FR-STORE-4.
|
||||||
|
exportMeeting: (
|
||||||
|
meetingId: MeetingId,
|
||||||
|
dest: string,
|
||||||
|
format: "md" | "pdf" | "docx" | "bundle" | "obsidian",
|
||||||
|
) => invoke<string>("export_meeting", { meetingId, dest, format }),
|
||||||
// Every meeting matching tag/date filters, one file (or bundle folder) per
|
// Every meeting matching tag/date filters, one file (or bundle folder) per
|
||||||
// meeting under destDir. Returns the count actually exported (T8.5, FR-STORE-4).
|
// meeting under destDir. Returns the count actually exported (T8.5, FR-STORE-4).
|
||||||
bulkExportMeetings: (
|
bulkExportMeetings: (
|
||||||
@@ -473,6 +528,10 @@ export const api = {
|
|||||||
from: filter?.from,
|
from: filter?.from,
|
||||||
to: filter?.to,
|
to: filter?.to,
|
||||||
}),
|
}),
|
||||||
|
// Import bundle(s) exported with format "bundle" — dir is a single bundle
|
||||||
|
// folder or a parent folder of them. Reconstructs each under a fresh id and
|
||||||
|
// returns the count imported (FR-STORE-4).
|
||||||
|
importMeetingBundle: (dir: string) => invoke<number>("import_meeting_bundle", { dir }),
|
||||||
|
|
||||||
llmStatus: () => invoke<LlmStatus>("llm_status"),
|
llmStatus: () => invoke<LlmStatus>("llm_status"),
|
||||||
// provider ∈ ollama|custom|anthropic|openai|off; apiKey (hosted) → OS credential store (ADR-0011).
|
// provider ∈ ollama|custom|anthropic|openai|off; apiKey (hosted) → OS credential store (ADR-0011).
|
||||||
@@ -576,12 +635,24 @@ export const events = {
|
|||||||
onDeviceChanged: (
|
onDeviceChanged: (
|
||||||
cb: (p: { meetingId: string; recovered: boolean; message: string }) => void,
|
cb: (p: { meetingId: string; recovered: boolean; message: string }) => void,
|
||||||
): Promise<UnlistenFn> => listen("recording://device", (e) => cb(e.payload as never)),
|
): Promise<UnlistenFn> => listen("recording://device", (e) => cb(e.payload as never)),
|
||||||
|
// Mic mute toggled for the active recording (FR-CAP-7).
|
||||||
|
onMicMuted: (
|
||||||
|
cb: (p: { meetingId: string; muted: boolean }) => void,
|
||||||
|
): Promise<UnlistenFn> => listen("recording://mic", (e) => cb(e.payload as never)),
|
||||||
onSegment: (
|
onSegment: (
|
||||||
cb: (p: { meetingId: string; segment: TranscriptSegment }) => void,
|
cb: (p: { meetingId: string; segment: TranscriptSegment }) => void,
|
||||||
): Promise<UnlistenFn> => listen("transcript://segment", (e) => cb(e.payload as never)),
|
): Promise<UnlistenFn> => listen("transcript://segment", (e) => cb(e.payload as never)),
|
||||||
onFinalized: (
|
onFinalized: (
|
||||||
cb: (p: { meetingId: string; segmentCount: number }) => void,
|
cb: (p: { meetingId: string; segmentCount: number }) => void,
|
||||||
): Promise<UnlistenFn> => listen("transcript://finalized", (e) => cb(e.payload as never)),
|
): Promise<UnlistenFn> => listen("transcript://finalized", (e) => cb(e.payload as never)),
|
||||||
|
// Per-phase progress of a background media import (feeds the import tracker).
|
||||||
|
onImportProgress: (cb: (p: ImportProgress) => void): Promise<UnlistenFn> =>
|
||||||
|
listen("import://progress", (e) => cb(e.payload as never)),
|
||||||
|
// Live diarization refined the speaker list mid-recording (FR-SPK): updated
|
||||||
|
// labels/display names, including the mic speaker resolved to "You".
|
||||||
|
onDiarizationUpdated: (
|
||||||
|
cb: (p: { meetingId: string; speakers: SpeakerInfo[] }) => void,
|
||||||
|
): Promise<UnlistenFn> => listen("diarization://updated", (e) => cb(e.payload as never)),
|
||||||
onLlmToken: (cb: (p: { meetingId: string; text: string }) => void): Promise<UnlistenFn> =>
|
onLlmToken: (cb: (p: { meetingId: string; text: string }) => void): Promise<UnlistenFn> =>
|
||||||
listen("llm://token", (e) => cb(e.payload as never)),
|
listen("llm://token", (e) => cb(e.payload as never)),
|
||||||
onLlmDone: (cb: (p: { meetingId: string; summary: SummaryFile }) => void): Promise<UnlistenFn> =>
|
onLlmDone: (cb: (p: { meetingId: string; summary: SummaryFile }) => void): Promise<UnlistenFn> =>
|
||||||
@@ -592,6 +663,15 @@ export const events = {
|
|||||||
onHardwareChanged: (
|
onHardwareChanged: (
|
||||||
cb: (p: { active: BackendId; reason: string }) => void,
|
cb: (p: { active: BackendId; reason: string }) => void,
|
||||||
): Promise<UnlistenFn> => listen("hardware://changed", (e) => cb(e.payload as never)),
|
): Promise<UnlistenFn> => listen("hardware://changed", (e) => cb(e.payload as never)),
|
||||||
|
// Live level meter for a device test (Settings ▸ Hardware). `done` marks the
|
||||||
|
// end of the monitor window.
|
||||||
|
onDeviceLevel: (
|
||||||
|
cb: (p: { kind: string; rms?: number; peak?: number; done?: boolean }) => void,
|
||||||
|
): Promise<UnlistenFn> => listen("device://level", (e) => cb(e.payload as never)),
|
||||||
|
// Per-(backend, model) progress ticks during the quick stress test.
|
||||||
|
onStressProgress: (
|
||||||
|
cb: (p: { backend: string; model: string }) => void,
|
||||||
|
): Promise<UnlistenFn> => listen("stress://progress", (e) => cb(e.payload as never)),
|
||||||
onNpuDownload: (
|
onNpuDownload: (
|
||||||
cb: (p: {
|
cb: (p: {
|
||||||
stage: "model" | "runtime" | "done";
|
stage: "model" | "runtime" | "done";
|
||||||
|
|||||||
@@ -3,21 +3,19 @@
|
|||||||
// Settings "Record by default" toggle and the mid-meeting retention toggle
|
// Settings "Record by default" toggle and the mid-meeting retention toggle
|
||||||
// so both gate on the same copy and the same acknowledgment call.
|
// so both gate on the same copy and the same acknowledgment call.
|
||||||
import { ShieldAlert } from "@lucide/svelte";
|
import { ShieldAlert } from "@lucide/svelte";
|
||||||
|
import { t } from "../i18n/index.svelte";
|
||||||
let { onAccept, onCancel }: { onAccept: () => void; onCancel: () => void } = $props();
|
let { onAccept, onCancel }: { onAccept: () => void; onCancel: () => void } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="consent">
|
<div class="consent">
|
||||||
<div class="heading">
|
<div class="heading">
|
||||||
<ShieldAlert size={18} aria-hidden="true" />
|
<ShieldAlert size={18} aria-hidden="true" />
|
||||||
<strong>Before you record</strong>
|
<strong>{t("consent.heading")}</strong>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>{t("consent.body")}</p>
|
||||||
Recording conversations without the consent of participants may be illegal in your region. Check
|
|
||||||
your local recording laws. This is a caution, not legal advice.
|
|
||||||
</p>
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="primary" onclick={onAccept}>I understand — enable recording</button>
|
<button class="primary" onclick={onAccept}>{t("consent.accept")}</button>
|
||||||
<button onclick={onCancel}>Cancel</button>
|
<button onclick={onCancel}>{t("consent.cancel")}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
// (recording consent, ADR-0009): both gate a single Settings toggle AND a
|
// (recording consent, ADR-0009): both gate a single Settings toggle AND a
|
||||||
// second use-time trigger point on the same one-time flag.
|
// second use-time trigger point on the same one-time flag.
|
||||||
import { Globe } from "@lucide/svelte";
|
import { Globe } from "@lucide/svelte";
|
||||||
|
import { t } from "../i18n/index.svelte";
|
||||||
let {
|
let {
|
||||||
providerLabel,
|
providerLabel,
|
||||||
onAccept,
|
onAccept,
|
||||||
@@ -16,16 +17,15 @@
|
|||||||
<div class="banner" role="alertdialog" aria-labelledby="hosted-ai-heading">
|
<div class="banner" role="alertdialog" aria-labelledby="hosted-ai-heading">
|
||||||
<div class="heading">
|
<div class="heading">
|
||||||
<Globe size={18} aria-hidden="true" />
|
<Globe size={18} aria-hidden="true" />
|
||||||
<strong id="hosted-ai-heading">Before using {providerLabel}</strong>
|
<strong id="hosted-ai-heading">{t("hosted.heading", { provider: providerLabel })}</strong>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
Generating with <strong>{providerLabel}</strong> sends this meeting's transcript to their servers
|
{t("hosted.body_1")} <strong>{providerLabel}</strong>
|
||||||
— it leaves this device and is subject to their privacy policy. WhispAssist has no control over data
|
{t("hosted.body_2")}
|
||||||
handling once it leaves your device. This is off by default; you're choosing it now.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="primary" onclick={onAccept}>I understand — continue</button>
|
<button class="primary" onclick={onAccept}>{t("hosted.accept")}</button>
|
||||||
<button class="ghost" onclick={onCancel}>Cancel</button>
|
<button class="ghost" onclick={onCancel}>{t("hosted.cancel")}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,63 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// Manually add a meeting from an existing recording (feature: "add a meeting
|
// Manually add a meeting from an existing recording (feature: "add a meeting
|
||||||
// + upload a video URL or audio file"). Transcoding is done by the backend
|
// + upload a video URL or audio file"). Transcoding is done by the backend
|
||||||
// via ffmpeg (+ yt-dlp for URLs) — both external, not bundled — so this is
|
// via ffmpeg (+ yt-dlp for URLs) — both external, not bundled — so this is a
|
||||||
// just a small form: pick a local file or paste a URL, optional title, go.
|
// small form: pick a file or URL, choose the transcription model, go. Import
|
||||||
import { api, errorMessage } from "../api";
|
// runs in the background (import_media returns as soon as the meeting row
|
||||||
|
// exists), so this closes immediately and the meetings list shows progress.
|
||||||
|
import { api, errorMessage, type ModelInfo } from "../api";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { onMount } from "svelte";
|
||||||
import { trapFocus } from "../actions/trapFocus";
|
import { trapFocus } from "../actions/trapFocus";
|
||||||
import { X, FileUp, Link as LinkIcon } from "@lucide/svelte";
|
import { t } from "../i18n/index.svelte";
|
||||||
|
import { X, FileUp, Download } from "@lucide/svelte";
|
||||||
|
|
||||||
let { onClose, onImported }: { onClose: () => void; onImported: (id: string) => void } = $props();
|
let { onClose, onImported }: { onClose: () => void; onImported: (id: string) => void } = $props();
|
||||||
|
|
||||||
|
// External download pages for the two tools this feature shells out to.
|
||||||
|
const FFMPEG_URL = "https://github.com/BtbN/FFmpeg-Builds/releases/latest";
|
||||||
|
const YTDLP_URL = "https://github.com/yt-dlp/yt-dlp/releases/latest";
|
||||||
|
|
||||||
// `source` is either a local file path (set via Browse) or a URL (typed).
|
// `source` is either a local file path (set via Browse) or a URL (typed).
|
||||||
let source = $state("");
|
let source = $state("");
|
||||||
let title = $state("");
|
let title = $state("");
|
||||||
|
let model = $state("");
|
||||||
|
let models = $state<ModelInfo[]>([]);
|
||||||
let busy = $state(false);
|
let busy = $state(false);
|
||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
|
|
||||||
|
// Only installed whisper models are selectable; default to the active one so
|
||||||
|
// the pick matches the user's Settings default unless they change it here.
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
const all = await api.listModels();
|
||||||
|
models = all.filter((m) => m.installed);
|
||||||
|
model = models.find((m) => m.active)?.id ?? models[0]?.id ?? "";
|
||||||
|
} catch {
|
||||||
|
models = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function browse() {
|
async function browse() {
|
||||||
const path = await open({
|
const path = await open({
|
||||||
multiple: false,
|
multiple: false,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: "Audio / video",
|
name: t("import.filter_av"),
|
||||||
extensions: ["mp3", "m4a", "wav", "aac", "ogg", "opus", "flac", "mp4", "mkv", "mov", "webm", "avi"],
|
extensions: [
|
||||||
|
"mp3",
|
||||||
|
"m4a",
|
||||||
|
"wav",
|
||||||
|
"aac",
|
||||||
|
"ogg",
|
||||||
|
"opus",
|
||||||
|
"flac",
|
||||||
|
"mp4",
|
||||||
|
"mkv",
|
||||||
|
"mov",
|
||||||
|
"webm",
|
||||||
|
"avi",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -37,7 +72,7 @@
|
|||||||
busy = true;
|
busy = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
const id = await api.importMedia(source.trim(), title.trim() || undefined);
|
const id = await api.importMedia(source.trim(), title.trim() || undefined, model || undefined);
|
||||||
onImported(id);
|
onImported(id);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -48,45 +83,71 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="overlay" role="dialog" aria-modal="true" aria-label="Add a meeting from a file or URL" tabindex="-1" use:trapFocus>
|
<div
|
||||||
|
class="overlay"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={t("import.dialog_label")}
|
||||||
|
tabindex="-1"
|
||||||
|
use:trapFocus
|
||||||
|
>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<header>
|
<header>
|
||||||
<strong>Add a meeting</strong>
|
<strong>{t("import.title")}</strong>
|
||||||
<button class="close" onclick={onClose} aria-label="Close" title="Close (Esc)">
|
<button
|
||||||
|
class="close"
|
||||||
|
onclick={onClose}
|
||||||
|
aria-label={t("import.close")}
|
||||||
|
title={t("import.close_title")}
|
||||||
|
>
|
||||||
<X size={18} aria-hidden="true" />
|
<X size={18} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<p class="muted">
|
<p class="muted">{t("import.body")}</p>
|
||||||
Import an existing recording — a local audio/video file, or a link
|
|
||||||
(YouTube, a streaming page, or a direct media URL). It's transcribed and
|
|
||||||
diarized just like a live recording.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<label class="wide">
|
<label class="wide">
|
||||||
File or URL
|
{t("import.file_or_url")}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<input
|
<input
|
||||||
class="grow"
|
class="grow"
|
||||||
bind:value={source}
|
bind:value={source}
|
||||||
placeholder="Paste a URL, or browse for a file…"
|
placeholder={t("import.source_placeholder")}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
/>
|
/>
|
||||||
<button onclick={browse} disabled={busy} title="Choose a local file">
|
<button onclick={browse} disabled={busy} title={t("import.choose_file")}>
|
||||||
<FileUp size={14} aria-hidden="true" /> Browse…
|
<FileUp size={14} aria-hidden="true" />
|
||||||
|
{t("import.browse")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="wide">
|
<div class="grid">
|
||||||
Title <em>(optional)</em>
|
<label>
|
||||||
<input bind:value={title} placeholder="Defaults to the file name" disabled={busy} />
|
{t("import.model_label")}
|
||||||
|
<select bind:value={model} disabled={busy || models.length === 0}>
|
||||||
|
{#each models as m (m.id)}
|
||||||
|
<option value={m.id}>{m.label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<span class="hint">{t("import.model_hint")}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<p class="muted small">
|
<label>
|
||||||
<LinkIcon size={12} aria-hidden="true" /> Requires <code>ffmpeg</code> installed and on your
|
{t("import.title_label")} <em>({t("import.optional")})</em>
|
||||||
PATH (plus <code>yt-dlp</code> for URLs). WhispAssist doesn't bundle them.
|
<input bind:value={title} placeholder={t("import.title_placeholder")} disabled={busy} />
|
||||||
</p>
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tools">
|
||||||
|
<span class="muted small">{t("import.requires")}</span>
|
||||||
|
<button class="tool" type="button" onclick={() => api.openUrl(FFMPEG_URL)}>
|
||||||
|
<Download size={12} aria-hidden="true" /> ffmpeg
|
||||||
|
</button>
|
||||||
|
<button class="tool" type="button" onclick={() => api.openUrl(YTDLP_URL)}>
|
||||||
|
<Download size={12} aria-hidden="true" /> yt-dlp
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="error">{error}</p>
|
<p class="error">{error}</p>
|
||||||
@@ -94,9 +155,10 @@
|
|||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="primary" onclick={doImport} disabled={!source.trim() || busy}>
|
<button class="primary" onclick={doImport} disabled={!source.trim() || busy}>
|
||||||
{busy ? "Importing… this can take a while" : "Import"}
|
{busy ? t("import.importing") : t("import.import")}
|
||||||
</button>
|
</button>
|
||||||
<button class="link" onclick={onClose} disabled={busy}>Cancel</button>
|
<span class="muted small note">{t("import.background_note")}</span>
|
||||||
|
<button class="link" onclick={onClose} disabled={busy}>{t("import.cancel")}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,7 +180,7 @@
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
width: min(520px, 100%);
|
width: min(540px, 100%);
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
|
||||||
@@ -154,7 +216,8 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
input {
|
input,
|
||||||
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0.4rem 0.55rem;
|
padding: 0.4rem 0.55rem;
|
||||||
@@ -164,10 +227,18 @@
|
|||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
input:focus-visible {
|
input:focus-visible,
|
||||||
|
select:focus-visible {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
.hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
@@ -188,18 +259,49 @@
|
|||||||
padding: 0.4rem 0.6rem;
|
padding: 0.4rem 0.6rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
/* Model + title side by side on wide panels, stacked when cramped. */
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0 0.75rem;
|
||||||
|
}
|
||||||
|
@media (max-width: 460px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tools {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-top: 0.9rem;
|
||||||
|
}
|
||||||
|
.tool {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
background: var(--bg-hover, transparent);
|
||||||
|
color: var(--accent);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.tool:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
.muted {
|
.muted {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
.small {
|
.small {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
color: var(--danger, #d33);
|
color: var(--danger, #d33);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -207,6 +309,10 @@
|
|||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
.actions .note {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
.actions .primary {
|
.actions .primary {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--accent-fg, #fff);
|
color: var(--accent-fg, #fff);
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// Domino's-pizza-tracker-style progress for a background media import: four
|
||||||
|
// ordered steps, the running one pulses, finished ones show how long they
|
||||||
|
// took. Fed by the `imports` store (import://progress events). Renders nothing
|
||||||
|
// until the first tick arrives. Design per ui-ux-pro-max: color is never the
|
||||||
|
// only signal (icon + label + time), tabular figures for the timers, and the
|
||||||
|
// pulse is dropped under prefers-reduced-motion.
|
||||||
|
import { imports } from "../stores/imports.svelte";
|
||||||
|
import { t } from "../i18n/index.svelte";
|
||||||
|
import { AudioLines, Captions, Users, FileCheck2, Check, X } from "@lucide/svelte";
|
||||||
|
|
||||||
|
let { meetingId }: { meetingId: string } = $props();
|
||||||
|
|
||||||
|
const run = $derived(imports.get(meetingId));
|
||||||
|
|
||||||
|
const ICONS = {
|
||||||
|
prepare: AudioLines,
|
||||||
|
transcribe: Captions,
|
||||||
|
diarize: Users,
|
||||||
|
finalize: FileCheck2,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ms → compact, human duration for a finished step ("820 ms", "4.3s", "2m 05s").
|
||||||
|
function fmtDur(ms: number | null): string {
|
||||||
|
if (ms == null) return "";
|
||||||
|
if (ms < 1000) return `${ms} ms`;
|
||||||
|
const s = ms / 1000;
|
||||||
|
if (s < 60) return `${s.toFixed(1)}s`;
|
||||||
|
const m = Math.floor(s / 60);
|
||||||
|
const rem = Math.round(s % 60);
|
||||||
|
return `${m}m ${String(rem).padStart(2, "0")}s`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if run}
|
||||||
|
<section class="tracker" aria-label={t("import.tracker.label")}>
|
||||||
|
<header>
|
||||||
|
{#if run.error}
|
||||||
|
<span class="head err">{t("import.tracker.failed")}</span>
|
||||||
|
{:else if run.done}
|
||||||
|
<span class="head ok">{t("import.tracker.done")}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="head">{t("import.tracker.running")}</span>
|
||||||
|
{/if}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<ol class="steps" aria-live="polite">
|
||||||
|
{#each run.phases as p (p.phase)}
|
||||||
|
{@const Icon = ICONS[p.phase]}
|
||||||
|
<li class="step {p.state}">
|
||||||
|
<div class="node">
|
||||||
|
{#if p.state === "done"}
|
||||||
|
<Check size={18} aria-hidden="true" />
|
||||||
|
{:else if p.state === "error"}
|
||||||
|
<X size={18} aria-hidden="true" />
|
||||||
|
{:else}
|
||||||
|
<Icon size={18} aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="name">{t(`import.phase.${p.phase}`)}</span>
|
||||||
|
<span class="time">
|
||||||
|
{#if p.state === "done"}{fmtDur(p.elapsedMs)}
|
||||||
|
{:else if p.state === "active"}{t("import.tracker.working")}
|
||||||
|
{:else if p.state === "error"}{t("import.tracker.stopped")}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
{#if run.error}
|
||||||
|
<p class="msg">{run.error}</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tracker {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--panel, var(--bg-elevated));
|
||||||
|
padding: 0.85rem 1rem 1rem;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
margin-bottom: 0.9rem;
|
||||||
|
}
|
||||||
|
.head {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
.head.ok {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
.head.err {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.step {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
/* Connector from the previous node's center to this one's (each step is the
|
||||||
|
same width, so -50%→+50% spans center to center), sitting behind the node. */
|
||||||
|
.step::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 17px;
|
||||||
|
left: -50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--border);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.step:first-child::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.step.done::before,
|
||||||
|
.step.active::before,
|
||||||
|
.step.error::before {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
margin: 0 auto 0.45rem;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.step.active .node {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--accent-soft, transparent);
|
||||||
|
color: var(--accent);
|
||||||
|
animation: pulse 1.4s ease-out infinite;
|
||||||
|
}
|
||||||
|
.step.done .node {
|
||||||
|
border-color: var(--success);
|
||||||
|
background: var(--success);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.step.error .node {
|
||||||
|
border-color: var(--danger);
|
||||||
|
background: var(--danger);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.1rem;
|
||||||
|
padding: 0 0.2rem;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.step.active .name,
|
||||||
|
.step.done .name {
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
.time {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
min-height: 1em;
|
||||||
|
}
|
||||||
|
.step.active .time {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg {
|
||||||
|
margin: 0.85rem 0 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--danger);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 45%, transparent);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 8px color-mix(in srgb, var(--accent) 0%, transparent);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.step.active .node {
|
||||||
|
animation: none;
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-soft, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
// system/loopback level (green) and — when the mic is enabled — the microphone
|
// system/loopback level (green) and — when the mic is enabled — the microphone
|
||||||
// level overlaid in the accent colour, so both sides of the call are visible
|
// level overlaid in the accent colour, so both sides of the call are visible
|
||||||
// at a glance. Each stream shows an rms fill + a peak marker.
|
// at a glance. Each stream shows an rms fill + a peak marker.
|
||||||
|
import { t } from "../i18n/index.svelte";
|
||||||
let {
|
let {
|
||||||
rms,
|
rms,
|
||||||
peak,
|
peak,
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
<div
|
<div
|
||||||
class="meter"
|
class="meter"
|
||||||
role="meter"
|
role="meter"
|
||||||
aria-label={showMic ? "System and microphone input level" : "System input level"}
|
aria-label={showMic ? t("levelmeter.system_mic") : t("levelmeter.system")}
|
||||||
aria-valuenow={Math.round(Math.max(rmsPct, showMic ? micRmsPct : 0))}
|
aria-valuenow={Math.round(Math.max(rmsPct, showMic ? micRmsPct : 0))}
|
||||||
aria-valuemin={0}
|
aria-valuemin={0}
|
||||||
aria-valuemax={100}
|
aria-valuemax={100}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
// list it's rendered in (not a global delete — the caller decides what
|
// list it's rendered in (not a global delete — the caller decides what
|
||||||
// "remove" means).
|
// "remove" means).
|
||||||
import { X } from "@lucide/svelte";
|
import { X } from "@lucide/svelte";
|
||||||
|
import { t } from "../i18n/index.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tag: string;
|
tag: string;
|
||||||
@@ -20,12 +21,17 @@
|
|||||||
class="label"
|
class="label"
|
||||||
onclick={onClick}
|
onclick={onClick}
|
||||||
disabled={!onClick}
|
disabled={!onClick}
|
||||||
title={onClick ? `Filter meetings tagged "${tag}"` : undefined}
|
title={onClick ? t("tagchip.filter", { tag }) : undefined}
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</button>
|
</button>
|
||||||
{#if removable}
|
{#if removable}
|
||||||
<button type="button" class="remove" onclick={onRemove} aria-label={`Remove tag ${tag}`}>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="remove"
|
||||||
|
onclick={onRemove}
|
||||||
|
aria-label={t("tagchip.remove", { tag })}
|
||||||
|
>
|
||||||
<X size={10} aria-hidden="true" />
|
<X size={10} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// plain <select> — three icon buttons instead of a text dropdown, defaults
|
// plain <select> — three icon buttons instead of a text dropdown, defaults
|
||||||
// to "system" so the app follows the OS until the user picks an override.
|
// to "system" so the app follows the OS until the user picks an override.
|
||||||
import { Monitor, Sun, Moon } from "@lucide/svelte";
|
import { Monitor, Sun, Moon } from "@lucide/svelte";
|
||||||
|
import { t } from "../i18n/index.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: "system" | "light" | "dark";
|
value: "system" | "light" | "dark";
|
||||||
@@ -10,21 +11,23 @@
|
|||||||
}
|
}
|
||||||
let { value, onchange }: Props = $props();
|
let { value, onchange }: Props = $props();
|
||||||
|
|
||||||
|
// labelKey resolves through t() in the template so a language switch relabels
|
||||||
|
// the buttons (a t() call in this module-level const would evaluate once).
|
||||||
const options = [
|
const options = [
|
||||||
{ id: "system", label: "Match system", Icon: Monitor },
|
{ id: "system", labelKey: "theme.system", Icon: Monitor },
|
||||||
{ id: "light", label: "Light", Icon: Sun },
|
{ id: "light", labelKey: "theme.light", Icon: Sun },
|
||||||
{ id: "dark", label: "Dark", Icon: Moon },
|
{ id: "dark", labelKey: "theme.dark", Icon: Moon },
|
||||||
] as const;
|
] as const;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="toggle" role="radiogroup" aria-label="Theme">
|
<div class="toggle" role="radiogroup" aria-label={t("theme.group")}>
|
||||||
{#each options as opt (opt.id)}
|
{#each options as opt (opt.id)}
|
||||||
<button
|
<button
|
||||||
role="radio"
|
role="radio"
|
||||||
aria-checked={value === opt.id}
|
aria-checked={value === opt.id}
|
||||||
class:active={value === opt.id}
|
class:active={value === opt.id}
|
||||||
title={opt.label}
|
title={t(opt.labelKey)}
|
||||||
aria-label={opt.label}
|
aria-label={t(opt.labelKey)}
|
||||||
onclick={() => onchange(opt.id)}
|
onclick={() => onchange(opt.id)}
|
||||||
>
|
>
|
||||||
<opt.Icon size={15} strokeWidth={2} aria-hidden="true" />
|
<opt.Icon size={15} strokeWidth={2} aria-hidden="true" />
|
||||||
|
|||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "التسجيل",
|
||||||
|
"nav.hardware": "العتاد",
|
||||||
|
"nav.storage": "التخزين",
|
||||||
|
"nav.calendar": "التقويم",
|
||||||
|
"nav.sync": "المزامنة",
|
||||||
|
"nav.ai": "الذكاء الاصطناعي",
|
||||||
|
"nav.mcp": "خادم MCP",
|
||||||
|
"nav.privacy": "الخصوصية",
|
||||||
|
"nav.about": "حول",
|
||||||
|
"nav.language": "اللغة",
|
||||||
|
|
||||||
|
"settings.language.title": "اللغة",
|
||||||
|
"settings.language.display": "لغة العرض",
|
||||||
|
"settings.language.display_hint": "لغة واجهة التطبيق. إضافة لغة بسيطة مثل إدراج ملف ترجمة واحد.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "لغة النسخ",
|
||||||
|
"settings.transcription.label": "اللغة",
|
||||||
|
"settings.transcription.auto": "الكشف التلقائي",
|
||||||
|
"settings.transcription.applies_hint": "ينطبق على التسجيل التالي. تُعرض اللغة المستخدمة فعليًا بعد ذلك في كل اجتماع.",
|
||||||
|
"settings.transcription.english_only": "الإنجليزية فقط",
|
||||||
|
"settings.transcription.model_english_only": "«{model}» بالإنجليزية فقط.",
|
||||||
|
"settings.transcription.no_model": "لم يتم اختيار نموذج.",
|
||||||
|
"settings.transcription.switch_multilingual": "بدّل إلى نموذج متعدد اللغات في الأعلى لاختيار لغة.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "العتاد",
|
||||||
|
"settings.hardware.active_backend": "الخلفية النشطة",
|
||||||
|
"settings.hardware.model_meta": "· نموذج {size}",
|
||||||
|
"settings.hardware.preferred_backend": "الخلفية المفضلة",
|
||||||
|
"settings.hardware.auto_backend": "تلقائي (الأفضل المتاح)",
|
||||||
|
"settings.hardware.not_available": "غير متاح",
|
||||||
|
"settings.hardware.fallback_hint": "يتراجع تلقائيًا (NPU → NVIDIA → AMD → Intel → CPU) إذا فشل تحميل الخلفية المختارة.",
|
||||||
|
"settings.hardware.audio_devices": "أجهزة الصوت",
|
||||||
|
"settings.hardware.recording_device": "جهاز التسجيل",
|
||||||
|
"settings.hardware.default_system_audio": "صوت النظام الافتراضي",
|
||||||
|
"settings.hardware.recording_device_hint": "يسجل WhispAssist ما يشغّله هذا الجهاز (استرجاع حلقي) — الطرف الآخر من المكالمة. اختر مخرجًا محددًا إذا كنت لا تريده أن يتبع افتراضي نظام Windows.",
|
||||||
|
"settings.hardware.microphone": "الميكروفون",
|
||||||
|
"settings.hardware.mic_off": "إيقاف — لا تلتقط ميكروفوني",
|
||||||
|
"settings.hardware.default_mic": "الميكروفون الافتراضي",
|
||||||
|
"settings.hardware.mic_hint": "يضيف صوتك إلى النسخ المباشر بحيث يُلتقط طرفا الاجتماع كلاهما. يبقى على جهازك — لا يُرفع أي شيء. اختر «إيقاف» لنسخ صوت النظام أعلاه فقط.",
|
||||||
|
"settings.hardware.npu_detected": "تم اكتشاف مسرّع NPU",
|
||||||
|
"settings.hardware.ready": "جاهز",
|
||||||
|
"settings.hardware.downloading": "جارٍ التنزيل…",
|
||||||
|
"settings.hardware.package_needed": "الحزمة مطلوبة",
|
||||||
|
"settings.hardware.npu_hint": "يحتاج NPU الخاص بك إلى حزمة تسريع لمرة واحدة (نموذج Whisper ONNX{extra}). يتم تنزيلها في الخلفية؛ يمكنك أيضًا بدؤها الآن.",
|
||||||
|
"settings.hardware.plus_openvino": " + بيئة تشغيل OpenVINO",
|
||||||
|
"settings.hardware.download_npu": "تنزيل حزمة NPU",
|
||||||
|
"settings.hardware.dml_title": "تسريع GPU (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "يمكن تسريع GPU الخاص بك دون Vulkan عبر DirectML — حزمة لمرة واحدة (نموذج Whisper ONNX{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + بيئة تشغيل DirectML",
|
||||||
|
"settings.hardware.download_dml": "تنزيل حزمة DirectML",
|
||||||
|
"settings.hardware.detection_unavailable": "اكتشاف العتاد غير متاح.",
|
||||||
|
"settings.hardware.low_overhead": "إعداد مسبق منخفض الحمل",
|
||||||
|
"settings.hardware.low_overhead_note": "المعالج + أصغر نموذج، لاستخدام البطارية/الخلفية",
|
||||||
|
"settings.hardware.models": "النماذج",
|
||||||
|
"settings.hardware.downloading_model": "جارٍ تنزيل {name}",
|
||||||
|
"settings.hardware.active_badge": "نشط",
|
||||||
|
"settings.hardware.installed": "مثبّت",
|
||||||
|
"settings.hardware.use": "استخدام",
|
||||||
|
"settings.hardware.remove": "إزالة",
|
||||||
|
"settings.hardware.cant_remove_active": "لا يمكن إزالة النموذج النشط",
|
||||||
|
"settings.hardware.download": "تنزيل",
|
||||||
|
"settings.hardware.diarization": "فصل المتحدثين",
|
||||||
|
"settings.hardware.diarization_hint": "ثبّت كلا النموذجين لفصل التسجيلات المكتملة حسب المتحدث (المتحدث 1، المتحدث 2، …) بدلًا من نسخ متواصل واحد. يعمل دون اتصال تمامًا، مرة واحدة لكل اجتماع بعد توقفه. بدونهما، يُنسب كل سطر إلى متحدث واحد.",
|
||||||
|
|
||||||
|
"settings.storage.title": "التخزين",
|
||||||
|
"settings.storage.location_label": "الموقع",
|
||||||
|
"settings.storage.location_hint": "تغيير موقع التخزين غير مدعوم بعد — هنا تقيم حاليًا الاجتماعات والنماذج وقاعدة البيانات.",
|
||||||
|
"settings.storage.retention_title": "الاحتفاظ",
|
||||||
|
"settings.storage.retention_hint": "يحذف تلقائيًا أقدم الاجتماعات التي تتجاوز حدًا معينًا. يُفحص مرة واحدة عند بدء التشغيل؛ الفارغ يعني بلا حد. لا يمس أبدًا اجتماعًا يجري تسجيله حاليًا.",
|
||||||
|
"settings.storage.max_age": "الحد الأقصى للعمر (أيام)",
|
||||||
|
"settings.storage.max_size": "الحد الأقصى للحجم (غيغابايت)",
|
||||||
|
"settings.storage.no_limit": "بلا حد",
|
||||||
|
"settings.storage.export_import_title": "التصدير والاستيراد",
|
||||||
|
"settings.storage.export_hint_1": "انقل التسجيلات بين أجهزة الكمبيوتر كمجلدات حزم محمولة (صوت، نسخ، ملاحظات، ملخص و",
|
||||||
|
"settings.storage.export_hint_2": "بيان). صدّر إلى أي مجلد — قرص متزامن، أو ذاكرة USB، أو نقطة تحميل محلية لهدف مزامنة — ثم استورده على الجهاز الآخر. تحصل الاجتماعات المستوردة على معرّف جديد، لذا لا تكتب إعادة الاستيراد فوق أي شيء أبدًا.",
|
||||||
|
"settings.storage.export_dialog_title": "تصدير كل الاجتماعات إلى…",
|
||||||
|
"settings.storage.import_dialog_title": "استيراد حزمة (أو مجلد حزم)…",
|
||||||
|
"settings.storage.exporting": "جارٍ التصدير…",
|
||||||
|
"settings.storage.export_all": "تصدير كل الاجتماعات…",
|
||||||
|
"settings.storage.importing": "جارٍ الاستيراد…",
|
||||||
|
"settings.storage.import_all": "استيراد اجتماعات…",
|
||||||
|
"settings.storage.exported_one": "تم تصدير {n} اجتماع.",
|
||||||
|
"settings.storage.exported_many": "تم تصدير {n} اجتماعًا.",
|
||||||
|
"settings.storage.imported_one": "تم استيراد {n} اجتماع.",
|
||||||
|
"settings.storage.imported_many": "تم استيراد {n} اجتماعًا.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "التقويم وOutlook .pst",
|
||||||
|
"settings.calendar.intro_1": "استورد الأحداث والحضور من نسخة Outlook الاحتياطية المحلية",
|
||||||
|
"settings.calendar.intro_2": "— للقراءة فقط، لا يُكتب أي شيء مرة أخرى في الملف. لا يغادر أي شيء هذا الجهاز.",
|
||||||
|
"settings.calendar.pst_file": "ملف .pst",
|
||||||
|
"settings.calendar.no_file": "لم يتم اختيار ملف",
|
||||||
|
"settings.calendar.browse": "استعراض…",
|
||||||
|
"settings.calendar.password": "كلمة المرور",
|
||||||
|
"settings.calendar.password_note": "نادرًا ما تكون مطلوبة",
|
||||||
|
"settings.calendar.optional": "اختياري",
|
||||||
|
"settings.calendar.import_range": "نطاق الاستيراد",
|
||||||
|
"settings.calendar.range_30": "آخر 30 يومًا",
|
||||||
|
"settings.calendar.range_90": "آخر 90 يومًا",
|
||||||
|
"settings.calendar.range_180": "آخر 6 أشهر",
|
||||||
|
"settings.calendar.range_365": "آخر سنة",
|
||||||
|
"settings.calendar.range_all": "كل الأوقات",
|
||||||
|
"settings.calendar.range_hint": "قد يحتوي صندوق بريد طويل العمر على سنوات من الإدخالات المتكررة/الأعياد — يبقي تضييق النطاق التقويم المستورد على ما هو ذو صلة فعلًا. ينطبق على زر الاستيراد هذا وعلى إعادة الاستيراد التلقائي عند التشغيل.",
|
||||||
|
"settings.calendar.importing": "جارٍ الاستيراد…",
|
||||||
|
"settings.calendar.import": "استيراد",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} حدثًا",
|
||||||
|
"settings.calendar.auto_reimport": "أعد استيراد هذا الملف تلقائيًا عند التشغيل",
|
||||||
|
"settings.calendar.auto_reimport_hint": "يعمل مرة واحدة عند بدء التشغيل، لا على مؤقّت — إعادة الاستيراد آمنة للتكرار (تُطابَق الأحداث الموجودة وتُحدَّث، لا تُكرَّر).",
|
||||||
|
"settings.calendar.import_failed": "فشل الاستيراد: {error} — الملف نفسه لم يُمس؛ تحقق من المسار وحاول مجددًا.",
|
||||||
|
"settings.calendar.cleanup_title": "تنظيف",
|
||||||
|
"settings.calendar.cleanup_hint": "تُحفظ دائمًا الأحداث المرتبطة بالفعل باجتماع مسجّل، بغض النظر عن الخيار المختار أدناه.",
|
||||||
|
"settings.calendar.cleanup_30": "أقدم من 30 يومًا",
|
||||||
|
"settings.calendar.cleanup_90": "أقدم من 90 يومًا",
|
||||||
|
"settings.calendar.cleanup_180": "أقدم من 6 أشهر",
|
||||||
|
"settings.calendar.cleanup_365": "أقدم من سنة",
|
||||||
|
"settings.calendar.cleanup_all": "حذف الكل",
|
||||||
|
"settings.calendar.cleaning": "جارٍ التنظيف…",
|
||||||
|
"settings.calendar.cleanup_btn": "تنظيف التقويم",
|
||||||
|
"settings.calendar.cleanup_result": "تم حذف {deleted}{extra}.",
|
||||||
|
"settings.calendar.cleanup_kept": "، مع الاحتفاظ بـ {n} (مرتبطة باجتماع)",
|
||||||
|
"settings.calendar.cleanup_failed": "فشل التنظيف: {error}",
|
||||||
|
"settings.calendar.imported_events": "الأحداث المستوردة",
|
||||||
|
"settings.calendar.no_events": "لم تُستورد أحداث بعد.",
|
||||||
|
"settings.calendar.search_title": "البحث في العنوان",
|
||||||
|
"settings.calendar.search_placeholder": "اسم الاجتماع…",
|
||||||
|
"settings.calendar.date_label": "التاريخ",
|
||||||
|
"settings.calendar.events_count": "{shown} من {total} حدثًا",
|
||||||
|
"settings.calendar.untitled": "(بلا عنوان)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "الإعدادات",
|
||||||
|
"settings.close": "إغلاق",
|
||||||
|
"settings.close_title": "إغلاق (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "وضع المعاينة — خلفية المزامنة غير منفّذة بعد (المرحلة 9). تُحفظ التغييرات في الواجهة فقط.",
|
||||||
|
"settings.sync.title": "المزامنة والرفع",
|
||||||
|
"settings.sync.enable_label": "تمكين رفع عناصر الاجتماع إلى الأهداف المُهيّأة",
|
||||||
|
"settings.sync.enable_hint": "متوقف افتراضيًا. لا يُرفع أي شيء ما لم يكن هذا مفعّلًا وهدف ممكّنًا. تُبقي الأهداف المستضافة ذاتيًا البيانات على خادمك الخاص؛ سُحُب الأطراف الثالثة مُعلّمة بوضوح.",
|
||||||
|
"settings.sync.targets_title": "الأهداف",
|
||||||
|
"settings.sync.no_targets": "لا أهداف بعد. أضف واحدًا أدناه.",
|
||||||
|
"settings.sync.third_party": "طرف ثالث",
|
||||||
|
"settings.sync.your_server": "خادمك",
|
||||||
|
"settings.sync.edit": "تحرير",
|
||||||
|
"settings.sync.remove": "إزالة",
|
||||||
|
"settings.sync.edit_target": "تحرير الهدف",
|
||||||
|
"settings.sync.add_target": "إضافة هدف",
|
||||||
|
"settings.sync.webdav_hint": "يغطي WebDAV كلًا من Nextcloud وownCloud وCloudreve وSeafile وSynology. (Seafile: فعّل SeafDAV من جهة الخادم.)",
|
||||||
|
"settings.sync.name": "الاسم",
|
||||||
|
"settings.sync.name_placeholder": "Nextcloud المنزلي",
|
||||||
|
"settings.sync.provider": "المزوّد",
|
||||||
|
"settings.sync.server_url": "عنوان URL للخادم",
|
||||||
|
"settings.sync.server_url_note_1": "عنوان URL لخادمك فقط — يضيف WhispAssist",
|
||||||
|
"settings.sync.server_url_note_2": "تلقائيًا.",
|
||||||
|
"settings.sync.remote_folder": "المجلد البعيد",
|
||||||
|
"settings.sync.username": "اسم المستخدم",
|
||||||
|
"settings.sync.app_password": "كلمة مرور التطبيق",
|
||||||
|
"settings.sync.pw_keep": "اتركها فارغة للاحتفاظ بكلمة المرور الحالية",
|
||||||
|
"settings.sync.pw_store": "مخزّنة في مخزن بيانات اعتماد نظام التشغيل",
|
||||||
|
"settings.sync.upload_legend": "رفع",
|
||||||
|
"settings.sync.artifact_transcript": "النسخ",
|
||||||
|
"settings.sync.artifact_notes": "الملاحظات",
|
||||||
|
"settings.sync.artifact_summary": "الملخص",
|
||||||
|
"settings.sync.artifact_recording": "التسجيل (.wav، إن احتُفظ به)",
|
||||||
|
"settings.sync.allow_plaintext": "السماح بـ http نصي عادي لعنوان LAN (غير موصى به)",
|
||||||
|
"settings.sync.encrypt_before": "التشفير قبل الرفع (تخزّن الوجهة النص المشفّر فقط)",
|
||||||
|
"settings.sync.requires_vault": "— يتطلب خزنة مفتوحة",
|
||||||
|
"settings.sync.test_connection": "اختبار الاتصال",
|
||||||
|
"settings.sync.save_changes": "حفظ التغييرات",
|
||||||
|
"settings.sync.add_target_btn": "إضافة هدف",
|
||||||
|
"settings.sync.cancel": "إلغاء",
|
||||||
|
"settings.sync.third_party_banner": "{kind} سحابة طرف ثالث — يرسل الرفع بياناتك خارج جهازك إلى {kind}.",
|
||||||
|
"settings.sync.link_account": "ربط حساب {kind}…",
|
||||||
|
"settings.sync.oauth_hint": "يفتح تسجيل دخول OAuth (إعادة توجيه حلقية)؛ يُخزَّن الرمز المميز في مخزن بيانات اعتماد نظام التشغيل.",
|
||||||
|
"settings.sync.footnote": "لا تُكتب بيانات الاعتماد أبدًا في الإعدادات أو قاعدة البيانات — فقط في مخزن بيانات اعتماد نظام التشغيل. TLS مطلوب للأهداف خارج LAN.",
|
||||||
|
|
||||||
|
"settings.ai.title": "مزوّد ملخصات الذكاء الاصطناعي",
|
||||||
|
"settings.ai.intro_1": "تعمل الملخصات على نموذج LLM محلي افتراضيًا. وجّه هذا إلى Ollama على هذا الكمبيوتر أو جهاز آخر في شبكتك المحلية (مثل",
|
||||||
|
"settings.ai.intro_2": "— كلاهما يُحسب محليًا، لذا لا يغادر أي شيء شبكتك. المزوّدون المستضافون (Anthropic) اختياريون، متوقفون افتراضيًا، ويرسلون النسخ إلى طرف ثالث بمجرد تشغيل أحدهم.",
|
||||||
|
"settings.ai.provider": "المزوّد",
|
||||||
|
"settings.ai.off": "إيقاف",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (محلي / LAN)",
|
||||||
|
"settings.ai.provider_custom": "مخصص (متوافق مع OpenAI)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — مستضاف، يغادر هذا الجهاز",
|
||||||
|
"settings.ai.endpoint": "نقطة النهاية",
|
||||||
|
"settings.ai.model": "النموذج",
|
||||||
|
"settings.ai.api_key": "مفتاح API",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (مضبوط بالفعل — اتركه فارغًا للاحتفاظ به)",
|
||||||
|
"settings.ai.show_api_key": "إظهار مفتاح API",
|
||||||
|
"settings.ai.hide_api_key": "إخفاء مفتاح API",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic خدمة مستضافة من طرف ثالث — يغادر نسخ هذا الاجتماع جهازك عندما تُنشئ ملخصًا.",
|
||||||
|
"settings.ai.endpoint_banner": "نقطة النهاية هذه ليست على جهازك أو شبكتك المحلية — سيغادر نسخك شبكتك.",
|
||||||
|
"settings.ai.saving": "جارٍ الحفظ…",
|
||||||
|
"settings.ai.save": "حفظ",
|
||||||
|
"settings.ai.test_connection": "اختبار الاتصال",
|
||||||
|
"settings.ai.this_hosted_provider": "هذا المزوّد المستضاف",
|
||||||
|
"settings.ai.reachable": "يمكن الوصول إليه",
|
||||||
|
"settings.ai.unreachable": "لا يمكن الوصول إليه",
|
||||||
|
"settings.ai.model_count_one": "· {n} نموذج",
|
||||||
|
"settings.ai.model_count_many": "· {n} نموذجًا",
|
||||||
|
"settings.ai.on_lan": "· على جهازك / LAN",
|
||||||
|
"settings.ai.leaves_network": "· يغادر شبكتك",
|
||||||
|
"settings.ai.advanced_title": "تهيئة Ollama المتقدمة",
|
||||||
|
"settings.ai.changed": "تم تغيير {n}",
|
||||||
|
"settings.ai.reset_all": "إعادة تعيين الكل إلى الافتراضيات",
|
||||||
|
"settings.ai.system_prompt": "موجّه النظام",
|
||||||
|
"settings.ai.system_prompt_placeholder": "مثال: أنت مُلخِّص اجتماعات موجز.",
|
||||||
|
"settings.ai.system_prompt_hint": "يُضاف قبل تنسيق الإخراج المطلوب من WhispAssist، بحيث لا تستطيع تعليماتك كسر تحليل الملخص/عناصر الإجراء.",
|
||||||
|
"settings.ai.think": "التفكير",
|
||||||
|
"settings.ai.think_help": "جهد الاستدلال (نماذج الاستدلال فقط).",
|
||||||
|
"settings.ai.keep_alive": "الإبقاء نشطًا",
|
||||||
|
"settings.ai.keep_alive_help": "مدة بقاء النموذج في الذاكرة. مثال: 5m، 1h، 0 (إلغاء التحميل)، -1 (للأبد).",
|
||||||
|
"settings.ai.runtime_hardware": "وقت التشغيل والعتاد",
|
||||||
|
"settings.ai.rarely_needed": "— نادرًا ما يكون مطلوبًا",
|
||||||
|
"settings.ai.save_advanced": "حفظ المتقدم",
|
||||||
|
"settings.ai.saved": "تم الحفظ",
|
||||||
|
"settings.ai.turn_off": "إيقاف",
|
||||||
|
"settings.ai.default_value": "الافتراضي {value}.",
|
||||||
|
"settings.ai.reset_default": "إعادة التعيين إلى الافتراضي",
|
||||||
|
"settings.ai.reset_field": "إعادة تعيين {name}",
|
||||||
|
|
||||||
|
"settings.mcp.title": "خادم MCP",
|
||||||
|
"settings.mcp.banner_1": "يتيح هذا لوكيل البرمجة الخاص بك (Claude Code، Codex، Copilot، OpenCode، …) سحب سياق الاجتماع على جهازك المحلي. بمجرد الاتصال،",
|
||||||
|
"settings.mcp.banner_agent": "قد يعيد ذلك الوكيل",
|
||||||
|
"settings.mcp.banner_2": "توجيه ما يقرؤه إلى سحابة مزوّد نموذجه الخاص — خارج سيطرة WhispAssist. لا يرسل WhispAssist نفسه هذه البيانات إلى أي مكان أبدًا؛ يستمع الخادم على هذا الجهاز فقط (",
|
||||||
|
"settings.mcp.banner_3": ")، وتُسجَّل كل قراءة أدناه.",
|
||||||
|
"settings.mcp.enable": "تمكين خادم MCP",
|
||||||
|
"settings.mcp.enable_hint": "متوقف افتراضيًا. حلقي فقط، محمي برمز مميز — لا شيء يمكن الوصول إليه من الشبكة.",
|
||||||
|
"settings.mcp.transport": "النقل",
|
||||||
|
"settings.mcp.transport_http": "HTTP متدفق",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (ينشئ الوكيل عملية)",
|
||||||
|
"settings.mcp.port": "المنفذ",
|
||||||
|
"settings.mcp.transport_change_hint": "لتغيير النقل/المنفذ، أوقف الخادم أولًا ثم أعد تشغيله.",
|
||||||
|
"settings.mcp.scope_title": "النطاق",
|
||||||
|
"settings.mcp.expose": "الكشف",
|
||||||
|
"settings.mcp.scope_none": "لا شيء — لا يُشارَك شيء",
|
||||||
|
"settings.mcp.scope_selected": "المحدد — فقط ملخصات الميزات التي وسمتها كمُشارَكة",
|
||||||
|
"settings.mcp.scope_all": "الكل — الاجتماعات والنسخ وعناصر الإجراء والملخصات المشتركة",
|
||||||
|
"settings.mcp.expose_recordings": "السماح أيضًا بالاجتماعات ذات التسجيل المحفوظ (متوقف افتراضيًا — يُحجب نسخ الاجتماع المسجّل حتى في نطاق «الكل» إلى أن يُفعَّل هذا)",
|
||||||
|
"settings.mcp.token_title": "رمز مصادقة جديد — يُعرض مرة واحدة، انسخه الآن",
|
||||||
|
"settings.mcp.token_hint": "لن يُعرض هذا مجددًا. يُخزَّن في مخزن بيانات اعتماد نظام التشغيل؛ إذا فقدته، أوقف الخادم وأعد تشغيله لإنشاء واحد جديد.",
|
||||||
|
"settings.mcp.copied": "تم النسخ",
|
||||||
|
"settings.mcp.copy": "نسخ",
|
||||||
|
"settings.mcp.endpoint": "نقطة النهاية",
|
||||||
|
"settings.mcp.endpoint_hint": "وجّه تهيئة عميل MCP لوكيلك إلى {kind}، مع الرمز المميز أعلاه كبيانات اعتماد bearer.",
|
||||||
|
"settings.mcp.kind_command": "أمر",
|
||||||
|
"settings.mcp.kind_url": "عنوان URL",
|
||||||
|
"settings.mcp.access_log": "سجل الوصول",
|
||||||
|
"settings.mcp.access_log_hint": "كل قراءة أداة يقوم بها وكيل، مسموحة أو مرفوضة (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "لم يقرأ أي وكيل أي شيء بعد.",
|
||||||
|
"settings.mcp.log_meeting": "اجتماع {id}",
|
||||||
|
"settings.mcp.refresh": "تحديث",
|
||||||
|
|
||||||
|
"settings.privacy.title": "الخصوصية",
|
||||||
|
"settings.privacy.intro": "ما يُسمح لـ WhispAssist فعليًا بإرساله خارج هذا الجهاز الآن. مع إيقاف كل شيء (الافتراضي)، لا يغادر أي شيء الجهاز على الإطلاق.",
|
||||||
|
"settings.privacy.llm_endpoint": "نقطة نهاية LLM",
|
||||||
|
"settings.privacy.off": "إيقاف",
|
||||||
|
"settings.privacy.local_only": "محلي فقط",
|
||||||
|
"settings.privacy.leaves_device": "يغادر هذا الجهاز",
|
||||||
|
"settings.privacy.sync_label": "المزامنة",
|
||||||
|
"settings.privacy.enabled": "مفعّلة",
|
||||||
|
"settings.privacy.mcp_label": "خادم MCP",
|
||||||
|
"settings.privacy.mcp_on": "تشغيل · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "وارد على الحلقة فقط — لا يضيف شيئًا إلى قائمة الصادر أعلاه. قد يظل وكيل متصل يعيد توجيه ما يقرؤه إلى مزوّد نموذجه الخاص؛ راجع علامة تبويب خادم MCP.",
|
||||||
|
"settings.privacy.egress_title": "قائمة السماح للصادر",
|
||||||
|
"settings.privacy.no_egress": "لا مضيفات في قائمة السماح — لا يجري WA أي إرسال للمحتوى.",
|
||||||
|
"settings.privacy.sync_targets_title": "أهداف المزامنة",
|
||||||
|
"settings.privacy.third_party": "طرف ثالث",
|
||||||
|
"settings.privacy.your_server": "خادمك",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "بلا TLS",
|
||||||
|
"settings.privacy.refresh": "تحديث",
|
||||||
|
"settings.privacy.unavailable": "فحص الخصوصية الذاتي غير متاح.",
|
||||||
|
"settings.privacy.vault_title": "خزنة التشفير",
|
||||||
|
"settings.privacy.vault_intro": "شفّر الملاحظات والنسخ والملخصات أثناء السكون بكلمة مرور. (ملفات الصوت غير مشفّرة بعد.)",
|
||||||
|
"settings.privacy.vault_password": "كلمة مرور الخزنة",
|
||||||
|
"settings.privacy.enable_vault": "تمكين الخزنة",
|
||||||
|
"settings.privacy.vault_pw_hint": "استخدم 8 أحرف على الأقل. إذا نسيتها، لا يمكن استرداد المحتوى المشفّر.",
|
||||||
|
"settings.privacy.vault_locked_1": "الخزنة ",
|
||||||
|
"settings.privacy.locked_word": "مقفلة",
|
||||||
|
"settings.privacy.vault_locked_2": ". افتح القفل لقراءة الاجتماعات المشفّرة.",
|
||||||
|
"settings.privacy.password": "كلمة المرور",
|
||||||
|
"settings.privacy.unlock": "فتح القفل",
|
||||||
|
"settings.privacy.vault_unlocked_1": "الخزنة ",
|
||||||
|
"settings.privacy.unlocked_word": "مفتوحة",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". تُشفَّر الملاحظات والنسخ والملخصات الجديدة أثناء السكون.",
|
||||||
|
"settings.privacy.lock_now": "أقفل الآن",
|
||||||
|
"settings.privacy.change_password": "تغيير كلمة المرور",
|
||||||
|
"settings.privacy.current_password": "كلمة المرور الحالية",
|
||||||
|
"settings.privacy.new_password": "كلمة المرور الجديدة",
|
||||||
|
|
||||||
|
"settings.about.title": "حول",
|
||||||
|
"settings.about.tagline": "مساعد اجتماعات محلي بالكامل، مفتوح المصدر، أصلي لنظام Windows.",
|
||||||
|
"settings.about.build_commit": "التزام البناء",
|
||||||
|
|
||||||
|
"consent.heading": "قبل أن تسجّل",
|
||||||
|
"consent.body": "قد يكون تسجيل المحادثات دون موافقة المشاركين غير قانوني في منطقتك. تحقق من قوانين التسجيل المحلية لديك. هذا تنبيه، وليس استشارة قانونية.",
|
||||||
|
"consent.accept": "أفهم — تمكين التسجيل",
|
||||||
|
"consent.cancel": "إلغاء",
|
||||||
|
|
||||||
|
"hosted.heading": "قبل استخدام {provider}",
|
||||||
|
"hosted.body_1": "الإنشاء باستخدام",
|
||||||
|
"hosted.body_2": "يرسل نسخ هذا الاجتماع إلى خوادمهم — يغادر هذا الجهاز ويخضع لسياسة الخصوصية الخاصة بهم. ليس لدى WhispAssist أي سيطرة على معالجة البيانات بمجرد مغادرتها جهازك. هذا متوقف افتراضيًا؛ أنت تختاره الآن.",
|
||||||
|
"hosted.accept": "أفهم — متابعة",
|
||||||
|
"hosted.cancel": "إلغاء",
|
||||||
|
|
||||||
|
"theme.group": "السمة",
|
||||||
|
"theme.system": "مطابقة النظام",
|
||||||
|
"theme.light": "فاتح",
|
||||||
|
"theme.dark": "داكن",
|
||||||
|
|
||||||
|
"import.dialog_label": "إضافة اجتماع من ملف أو عنوان URL",
|
||||||
|
"import.title": "إضافة اجتماع",
|
||||||
|
"import.close": "إغلاق",
|
||||||
|
"import.close_title": "إغلاق (Esc)",
|
||||||
|
"import.body": "استورد تسجيلًا موجودًا — ملف صوت/فيديو محلي، أو رابط (YouTube، أو صفحة بث، أو عنوان URL وسائط مباشر). يُنسخ ويُفصل حسب المتحدث تمامًا كتسجيل مباشر.",
|
||||||
|
"import.file_or_url": "ملف أو عنوان URL",
|
||||||
|
"import.source_placeholder": "الصق عنوان URL، أو استعرض بحثًا عن ملف…",
|
||||||
|
"import.choose_file": "اختر ملفًا محليًا",
|
||||||
|
"import.browse": "استعراض…",
|
||||||
|
"import.title_label": "العنوان",
|
||||||
|
"import.optional": "اختياري",
|
||||||
|
"import.title_placeholder": "افتراضيًا اسم الملف",
|
||||||
|
"import.filter_av": "صوت / فيديو",
|
||||||
|
"import.requires_1": "يتطلب",
|
||||||
|
"import.requires_2": "مثبّتًا وفي PATH لديك (بالإضافة إلى",
|
||||||
|
"import.requires_3": "للعناوين URL). لا يرفق WhispAssist بهما.",
|
||||||
|
"import.importing": "جارٍ الاستيراد… قد يستغرق هذا بعض الوقت",
|
||||||
|
"import.import": "استيراد",
|
||||||
|
"import.cancel": "إلغاء",
|
||||||
|
|
||||||
|
"tagchip.filter": "تصفية الاجتماعات الموسومة بـ «{tag}»",
|
||||||
|
"tagchip.remove": "إزالة الوسم {tag}",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "مستوى إدخال النظام والميكروفون",
|
||||||
|
"levelmeter.system": "مستوى إدخال النظام",
|
||||||
|
|
||||||
|
"app.tagline": "محلي · خاص",
|
||||||
|
"app.note_template": "قالب الملاحظات",
|
||||||
|
"app.no_template": "بلا قالب",
|
||||||
|
"app.add_meeting": "إضافة اجتماع",
|
||||||
|
"app.add_meeting_title": "إضافة اجتماع من ملف أو عنوان URL",
|
||||||
|
"app.record": "تسجيل",
|
||||||
|
"app.record_title": "بدء التسجيل (Ctrl+Shift+R)",
|
||||||
|
"app.stop": "إيقاف",
|
||||||
|
"app.stop_title": "إيقاف التسجيل (Ctrl+Shift+R)",
|
||||||
|
"app.cancel": "إلغاء",
|
||||||
|
"app.cancel_title": "تجاهل هذا التسجيل وحذفه",
|
||||||
|
"app.recording": "جارٍ التسجيل…",
|
||||||
|
"app.backend_title": "خلفية النسخ النشطة",
|
||||||
|
"app.retention_title": "حفظ الصوت كـ .wav لهذا الاجتماع",
|
||||||
|
"app.saving": "جارٍ الحفظ",
|
||||||
|
"app.not_saved": "غير محفوظ",
|
||||||
|
"app.reconnecting": "جارٍ إعادة توصيل جهاز الصوت…",
|
||||||
|
"app.settings": "الإعدادات",
|
||||||
|
"app.settings_title": "الإعدادات (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "تجاهل هذا التسجيل؟ سيُحذف صوته ونسخه.",
|
||||||
|
"app.announce_started": "بدأ التسجيل",
|
||||||
|
"app.announce_paused": "توقف التسجيل مؤقتًا",
|
||||||
|
"app.announce_stopped": "توقف التسجيل",
|
||||||
|
"app.consent_dialog": "موافقة التسجيل",
|
||||||
|
"app.vault_dialog": "فتح خزنة التشفير",
|
||||||
|
"app.vault_title": "فتح خزنة التشفير",
|
||||||
|
"app.vault_desc": "اجتماعاتك مشفّرة أثناء السكون. أدخل كلمة مرورك لقراءتها.",
|
||||||
|
"app.vault_password": "كلمة مرور الخزنة",
|
||||||
|
"app.vault_incorrect": "كلمة مرور غير صحيحة",
|
||||||
|
"app.unlock": "فتح القفل",
|
||||||
|
"app.continue_locked": "المتابعة مقفلًا",
|
||||||
|
"app.show_meetings": "إظهار قائمة الاجتماعات",
|
||||||
|
"app.hide_meetings": "إخفاء قائمة الاجتماعات",
|
||||||
|
"app.show_summary": "إظهار لوحة الملخص",
|
||||||
|
"app.hide_summary": "إخفاء لوحة الملخص",
|
||||||
|
"app.resize_meetings": "تغيير حجم قائمة الاجتماعات",
|
||||||
|
"app.resize_summary": "تغيير حجم لوحة الملخص",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "البحث في الاجتماعات…",
|
||||||
|
"meetings.search_aria": "البحث في الاجتماعات",
|
||||||
|
"meetings.filter_tag_aria": "التصفية حسب الوسم",
|
||||||
|
"meetings.all_tags": "كل الوسوم",
|
||||||
|
"meetings.from_date_aria": "من تاريخ",
|
||||||
|
"meetings.to_date_aria": "إلى تاريخ",
|
||||||
|
"meetings.bulk_format_aria": "تنسيق التصدير الجماعي",
|
||||||
|
"meetings.bulk_export": "تصدير جماعي",
|
||||||
|
"meetings.bulk_export_title": "تصدير كل اجتماع يطابق مرشحات الوسم/التاريخ أعلاه",
|
||||||
|
"meetings.exporting": "جارٍ التصدير…",
|
||||||
|
"meetings.exported_one": "تم تصدير {n} اجتماع.",
|
||||||
|
"meetings.exported_many": "تم تصدير {n} اجتماعًا.",
|
||||||
|
"meetings.searching": "جارٍ البحث…",
|
||||||
|
"meetings.loading": "جارٍ تحميل الاجتماعات…",
|
||||||
|
"meetings.no_matches": "لا نتائج مطابقة.",
|
||||||
|
"meetings.empty": "لا اجتماعات بعد — انقر على تسجيل أعلاه للبدء.",
|
||||||
|
"meetings.status.recording": "جارٍ التسجيل",
|
||||||
|
"meetings.status.transcribing": "جارٍ النسخ",
|
||||||
|
"meetings.status.recovering": "جارٍ الاسترداد",
|
||||||
|
"meetings.status.error": "خطأ",
|
||||||
|
"meetings.resume": "استئناف النسخ",
|
||||||
|
"meetings.delete_aria": "حذف الاجتماع",
|
||||||
|
"meetings.delete_confirm": "حذف هذا الاجتماع؟ يؤدي ذلك إلى إزالة تسجيله ونسخه وملاحظاته.",
|
||||||
|
|
||||||
|
"transcript.heading": "النسخ",
|
||||||
|
"transcript.title_aria": "عنوان الاجتماع",
|
||||||
|
"transcript.lang_title": "لغة النسخ",
|
||||||
|
"transcript.lang_auto": "جارٍ الكشف…",
|
||||||
|
"transcript.show": "إظهار النسخ",
|
||||||
|
"transcript.hide": "إخفاء النسخ",
|
||||||
|
"transcript.empty": "لا يوجد نسخ لهذا الاجتماع.",
|
||||||
|
"transcript.will_appear": "سيظهر النسخ هنا أثناء تسجيلك.",
|
||||||
|
"transcript.play_from_here": "التشغيل من هنا",
|
||||||
|
"transcript.resize": "تغيير حجم النسخ والملاحظات",
|
||||||
|
"transcript.reprocess_placeholder": "إعادة النسخ باستخدام…",
|
||||||
|
"transcript.reprocess_lang_aria": "لغة إعادة المعالجة",
|
||||||
|
"transcript.reprocess_keep_lang": "الاحتفاظ باللغة الحالية",
|
||||||
|
"transcript.reprocess_go": "تنفيذ",
|
||||||
|
"transcript.retranscribing": "جارٍ إعادة النسخ…",
|
||||||
|
"transcript.has_note": "به ملاحظة",
|
||||||
|
"transcript.note_placeholder": "أضف ملاحظة لهذه اللحظة…",
|
||||||
|
"transcript.note_aria": "ملاحظة لسطر النسخ هذا",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "مستند Word",
|
||||||
|
"transcript.filter_obsidian": "ملاحظة Obsidian",
|
||||||
|
|
||||||
|
"notes.heading": "الملاحظات",
|
||||||
|
"notes.show": "إظهار الملاحظات",
|
||||||
|
"notes.hide": "إخفاء الملاحظات",
|
||||||
|
"notes.placeholder": "ملاحظات…",
|
||||||
|
"notes.live_placeholder": "اكتب ملاحظات أثناء حديثك…",
|
||||||
|
"notes.toolbar_aria": "تنسيق الملاحظات",
|
||||||
|
"notes.bold": "غامق",
|
||||||
|
"notes.italic": "مائل",
|
||||||
|
"notes.h1": "عنوان 1",
|
||||||
|
"notes.h2": "عنوان 2",
|
||||||
|
"notes.bullet": "قائمة نقطية",
|
||||||
|
"notes.checkbox_title": "مربع اختيار",
|
||||||
|
"notes.checkbox_aria": "عنصر قائمة بمربع اختيار",
|
||||||
|
"notes.edit_raw": "تحرير markdown الخام",
|
||||||
|
"notes.render": "عرض markdown",
|
||||||
|
"notes.editor": "المحرر",
|
||||||
|
"notes.preview": "معاينة",
|
||||||
|
"notes.export_md_title": "تصدير الملاحظات كـ .md",
|
||||||
|
"notes.export_pdf_title": "تصدير الملاحظات كـ .pdf",
|
||||||
|
"notes.export_docx_title": "تصدير الملاحظات كـ .docx",
|
||||||
|
"notes.export_bundle_title": "تصدير الصوت + النسخ + الملاحظات إلى مجلد",
|
||||||
|
"notes.export_obsidian_title": "تصدير ملاحظة Obsidian واحدة (ملاحظات + ملخص + نسخ، بلا صوت)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "التسجيل",
|
||||||
|
"summary.loading_recording": "جارٍ تحميل التسجيل…",
|
||||||
|
"summary.sync_heading": "المزامنة",
|
||||||
|
"summary.uploading": "جارٍ الرفع…",
|
||||||
|
"summary.upload_now": "الرفع الآن",
|
||||||
|
"summary.no_uploads": "لا عمليات رفع لهذا الاجتماع بعد.",
|
||||||
|
"summary.retry": "إعادة المحاولة",
|
||||||
|
"summary.tags_heading": "الوسوم",
|
||||||
|
"summary.tags_select": "اختر اجتماعًا لوسمه.",
|
||||||
|
"summary.tag_add_placeholder": "إضافة وسم…",
|
||||||
|
"summary.tag_first_placeholder": "مشروع، عميل، موضوع…",
|
||||||
|
"summary.generating": "جارٍ الإنشاء…",
|
||||||
|
"summary.generate_tags": "إنشاء وسوم",
|
||||||
|
"summary.saving": "جارٍ الحفظ…",
|
||||||
|
"summary.save_tags": "حفظ الوسوم",
|
||||||
|
"summary.summary_heading": "الملخص",
|
||||||
|
"summary.ai_provider": "مزوّد الذكاء الاصطناعي",
|
||||||
|
"summary.provider_off": "إيقاف",
|
||||||
|
"summary.provider_custom": "مخصص",
|
||||||
|
"summary.local": "محلي",
|
||||||
|
"summary.leaves_device": "يغادر هذا الجهاز",
|
||||||
|
"summary.this_hosted_provider": "هذا المزوّد المستضاف",
|
||||||
|
"summary.select_generate": "اختر اجتماعًا لإنشاء ملخص.",
|
||||||
|
"summary.decisions_heading": "القرارات",
|
||||||
|
"summary.regenerate": "إعادة الإنشاء",
|
||||||
|
"summary.generated_hint": "أُنشئ محليًا بعد الاجتماع (يتطلب مزوّد LLM محلي).",
|
||||||
|
"summary.no_provider": "لا مزوّد ذكاء اصطناعي مُهيّأ — فعّل واحدًا في الإعدادات.",
|
||||||
|
"summary.generate_summary": "إنشاء ملخص",
|
||||||
|
"summary.briefs_heading": "ملخصات الميزات",
|
||||||
|
"summary.briefs_select": "اختر اجتماعًا لإنشاء ملخصات ميزات أو عرضها.",
|
||||||
|
"summary.briefs_desc": "يقطّر هذا الاجتماع إلى مواصفة جاهزة للوكيل (يتطلب مزوّد LLM محلي) — سلّمها إلى وكيل برمجة، أو قدّمها عبر خادم MCP بمجرد تشغيله.",
|
||||||
|
"summary.brief_repo_placeholder": "المستودع الهدف (اختياري)، مثل acme/reporting-web",
|
||||||
|
"summary.distilling": "جارٍ التقطير…",
|
||||||
|
"summary.create_brief": "إنشاء ملخص ميزة",
|
||||||
|
"summary.brief_mcp_title": "متاح عندما يكون خادم MCP قيد التشغيل",
|
||||||
|
"summary.copied": "تم النسخ",
|
||||||
|
"summary.copy_md": "نسخ كـ Markdown",
|
||||||
|
"summary.copy_json": "نسخ كـ JSON",
|
||||||
|
"summary.brief_problem": "المشكلة",
|
||||||
|
"summary.brief_outcome": "النتيجة المرجوة",
|
||||||
|
"summary.brief_criteria": "معايير القبول",
|
||||||
|
"summary.brief_none": "لم يُلتقط شيء.",
|
||||||
|
"summary.brief_context": "السياق",
|
||||||
|
"summary.action_items_heading": "عناصر الإجراء",
|
||||||
|
"summary.ai_select": "اختر اجتماعًا لرؤية عناصر الإجراء الخاصة به.",
|
||||||
|
"summary.ai_empty": "لا شيء بعد — أضف واحدًا أدناه، أو أنشئ ملخصًا لاستخراجها تلقائيًا.",
|
||||||
|
"summary.confirmed": "مؤكَّد",
|
||||||
|
"summary.ai_text_placeholder": "عنصر إجراء…",
|
||||||
|
"summary.ai_text_aria": "نص عنصر الإجراء",
|
||||||
|
"summary.owner": "المسؤول",
|
||||||
|
"summary.due_date": "تاريخ الاستحقاق",
|
||||||
|
"summary.reminder_title": "جدولة تذكير محلي لتاريخ الاستحقاق هذا",
|
||||||
|
"summary.ai_delete_title": "حذف عنصر الإجراء هذا",
|
||||||
|
"summary.ai_delete_aria": "حذف عنصر الإجراء",
|
||||||
|
"summary.add_action_item": "إضافة عنصر إجراء",
|
||||||
|
"summary.save_action_items": "حفظ عناصر الإجراء",
|
||||||
|
"summary.calendar_heading": "حدث التقويم",
|
||||||
|
"summary.cal_select": "اختر اجتماعًا لربطه بحدث تقويم.",
|
||||||
|
"summary.event_search_placeholder": "البحث في عنوان الحدث…",
|
||||||
|
"summary.linked_event": "الحدث المرتبط",
|
||||||
|
"summary.change_event": "تغيير الحدث…",
|
||||||
|
"summary.link_event": "ربط حدث…",
|
||||||
|
"summary.untitled_event": "(بلا عنوان)",
|
||||||
|
"summary.no_events_1": "لم تُستورد أحداث بعد — استورد",
|
||||||
|
"summary.no_events_2": "من الإعدادات → التقويم.",
|
||||||
|
"summary.no_events_match": "لا حدث يطابق هذا البحث/التاريخ — حاول مسح أحدهما.",
|
||||||
|
"summary.organizer_label": "المنظّم: {name}",
|
||||||
|
"summary.participants_heading": "المشاركون",
|
||||||
|
"summary.participants_hint": "يُملأ من حدث التقويم المرتبط.",
|
||||||
|
"summary.speakers_heading": "المتحدثون",
|
||||||
|
"summary.speakers_select": "اختر اجتماعًا لتسمية المتحدثين فيه.",
|
||||||
|
"summary.no_speakers": "لم يُكتشف متحدثون بعد.",
|
||||||
|
"summary.speaker_name_placeholder": "اسم المتحدث",
|
||||||
|
"summary.save": "حفظ",
|
||||||
|
"summary.cancel": "إلغاء",
|
||||||
|
"summary.name_speaker": "تسمية هذا المتحدث…",
|
||||||
|
"summary.add_new_name": "+ إضافة اسم جديد…",
|
||||||
|
|
||||||
|
"settings.recording.title": "التسجيل",
|
||||||
|
"settings.recording.record_default": "تسجيل الاجتماعات افتراضيًا",
|
||||||
|
"settings.recording.save_audio_as": "حفظ الصوت كـ",
|
||||||
|
"settings.recording.default_hint": "متوقف افتراضيًا. عند الإيقاف، يُستخدم الصوت فقط لإنتاج النسخ ويُحذف عند إنهاء الاجتماع. يمكنك أيضًا تبديل التسجيل لكل اجتماع.",
|
||||||
|
"settings.recording.consent_label": "الموافقة: {status}.",
|
||||||
|
"settings.recording.consent_ack": "مُقرّة",
|
||||||
|
"settings.recording.consent_not": "لم تُقرّ بعد",
|
||||||
|
"settings.recording.auto_label": "بدء التسجيل تلقائيًا عند بدء حدث تقويم",
|
||||||
|
"settings.recording.auto_hint": "فقط أثناء فتح WhispAssist. عند حلول وقت بدء حدث تقويم مستورد، يبدأ التسجيل تلقائيًا (باستخدام إعداد الاحتفاظ الافتراضي أعلاه). لا شيء يعمل في الخلفية — يُسلَّح المؤقّت فقط أثناء تشغيل التطبيق. استورد الأحداث من الإعدادات → التقويم."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "রেকর্ডিং",
|
||||||
|
"nav.hardware": "হার্ডওয়্যার",
|
||||||
|
"nav.storage": "স্টোরেজ",
|
||||||
|
"nav.calendar": "ক্যালেন্ডার",
|
||||||
|
"nav.sync": "সিঙ্ক",
|
||||||
|
"nav.ai": "এআই",
|
||||||
|
"nav.mcp": "MCP সার্ভার",
|
||||||
|
"nav.privacy": "গোপনীয়তা",
|
||||||
|
"nav.about": "সম্পর্কে",
|
||||||
|
"nav.language": "ভাষা",
|
||||||
|
|
||||||
|
"settings.language.title": "ভাষা",
|
||||||
|
"settings.language.display": "প্রদর্শন ভাষা",
|
||||||
|
"settings.language.display_hint": "অ্যাপ ইন্টারফেসের ভাষা। একটি ভাষা যোগ করা একটি অনুবাদ ফাইল রাখার মতোই সহজ।",
|
||||||
|
|
||||||
|
"settings.transcription.title": "ট্রান্সক্রিপশন ভাষা",
|
||||||
|
"settings.transcription.label": "ভাষা",
|
||||||
|
"settings.transcription.auto": "স্বয়ংক্রিয়ভাবে শনাক্ত করুন",
|
||||||
|
"settings.transcription.applies_hint": "পরবর্তী রেকর্ডিংয়ে প্রযোজ্য। প্রকৃতপক্ষে ব্যবহৃত ভাষা পরে প্রতিটি মিটিংয়ে দেখানো হয়।",
|
||||||
|
"settings.transcription.english_only": "শুধু ইংরেজি",
|
||||||
|
"settings.transcription.model_english_only": "“{model}” শুধুমাত্র ইংরেজিতে।",
|
||||||
|
"settings.transcription.no_model": "কোনো মডেল নির্বাচিত নেই।",
|
||||||
|
"settings.transcription.switch_multilingual": "একটি ভাষা বেছে নিতে উপরে একটি বহুভাষিক মডেলে স্যুইচ করুন।",
|
||||||
|
|
||||||
|
"settings.hardware.title": "হার্ডওয়্যার",
|
||||||
|
"settings.hardware.active_backend": "সক্রিয় ব্যাকএন্ড",
|
||||||
|
"settings.hardware.model_meta": "· মডেল {size}",
|
||||||
|
"settings.hardware.preferred_backend": "পছন্দসই ব্যাকএন্ড",
|
||||||
|
"settings.hardware.auto_backend": "স্বয়ংক্রিয় (সেরা উপলব্ধ)",
|
||||||
|
"settings.hardware.not_available": "উপলব্ধ নয়",
|
||||||
|
"settings.hardware.fallback_hint": "নির্বাচিত ব্যাকএন্ড লোড না হলে স্বয়ংক্রিয়ভাবে ফলব্যাক করে (NPU → NVIDIA → AMD → Intel → CPU)।",
|
||||||
|
"settings.hardware.audio_devices": "অডিও ডিভাইস",
|
||||||
|
"settings.hardware.recording_device": "রেকর্ডিং ডিভাইস",
|
||||||
|
"settings.hardware.default_system_audio": "ডিফল্ট সিস্টেম অডিও",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist এই ডিভাইসটি যা বাজায় তা রেকর্ড করে (লুপব্যাক) — কলের অপর প্রান্ত। আপনি যদি চান না এটি Windows-এর সিস্টেম ডিফল্ট অনুসরণ করুক, তবে একটি নির্দিষ্ট আউটপুট বেছে নিন।",
|
||||||
|
"settings.hardware.microphone": "মাইক্রোফোন",
|
||||||
|
"settings.hardware.mic_off": "বন্ধ — আমার মাইক্রোফোন ক্যাপচার করবেন না",
|
||||||
|
"settings.hardware.default_mic": "ডিফল্ট মাইক্রোফোন",
|
||||||
|
"settings.hardware.mic_hint": "লাইভ ট্রান্সক্রিপ্টে আপনার নিজের কণ্ঠস্বর যোগ করে যাতে মিটিংয়ের উভয় পক্ষ ক্যাপচার হয়। আপনার ডিভাইসে থাকে — কিছুই আপলোড হয় না। শুধু উপরের সিস্টেম অডিও ট্রান্সক্রাইব করতে “বন্ধ” বেছে নিন।",
|
||||||
|
"settings.hardware.npu_detected": "NPU অ্যাক্সিলারেটর শনাক্ত হয়েছে",
|
||||||
|
"settings.hardware.ready": "প্রস্তুত",
|
||||||
|
"settings.hardware.downloading": "ডাউনলোড হচ্ছে…",
|
||||||
|
"settings.hardware.package_needed": "প্যাকেজ প্রয়োজন",
|
||||||
|
"settings.hardware.npu_hint": "আপনার NPU-এর একটি এককালীন অ্যাক্সিলারেশন প্যাকেজ (Whisper ONNX মডেল{extra}) প্রয়োজন। এটি পটভূমিতে ডাউনলোড হয়; আপনি এখনই এটি শুরুও করতে পারেন।",
|
||||||
|
"settings.hardware.plus_openvino": " + OpenVINO রানটাইম",
|
||||||
|
"settings.hardware.download_npu": "NPU প্যাকেজ ডাউনলোড করুন",
|
||||||
|
"settings.hardware.dml_title": "GPU অ্যাক্সিলারেশন (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "আপনার GPU Vulkan ছাড়াই DirectML-এর মাধ্যমে অ্যাক্সিলারেট করা যায় — একটি এককালীন প্যাকেজ (Whisper ONNX মডেল{extra})।",
|
||||||
|
"settings.hardware.plus_directml": " + DirectML রানটাইম",
|
||||||
|
"settings.hardware.download_dml": "DirectML প্যাকেজ ডাউনলোড করুন",
|
||||||
|
"settings.hardware.detection_unavailable": "হার্ডওয়্যার শনাক্তকরণ উপলব্ধ নয়।",
|
||||||
|
"settings.hardware.low_overhead": "কম ওভারহেড প্রিসেট",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + সবচেয়ে ছোট মডেল, ব্যাটারি/পটভূমি ব্যবহারের জন্য",
|
||||||
|
"settings.hardware.models": "মডেল",
|
||||||
|
"settings.hardware.downloading_model": "{name} ডাউনলোড হচ্ছে",
|
||||||
|
"settings.hardware.active_badge": "সক্রিয়",
|
||||||
|
"settings.hardware.installed": "ইনস্টল করা হয়েছে",
|
||||||
|
"settings.hardware.use": "ব্যবহার করুন",
|
||||||
|
"settings.hardware.remove": "সরান",
|
||||||
|
"settings.hardware.cant_remove_active": "সক্রিয় মডেল সরানো যায় না",
|
||||||
|
"settings.hardware.download": "ডাউনলোড করুন",
|
||||||
|
"settings.hardware.diarization": "বক্তা ডায়ারাইজেশন",
|
||||||
|
"settings.hardware.diarization_hint": "উভয় মডেল ইনস্টল করুন যাতে সম্পূর্ণ রেকর্ডিং একটি চলমান ট্রান্সক্রিপ্টের পরিবর্তে বক্তা অনুযায়ী (বক্তা ১, বক্তা ২, …) আলাদা হয়। সম্পূর্ণ অফলাইনে চলে, থামার পর প্রতি মিটিংয়ে একবার। এগুলি ছাড়া, প্রতিটি লাইন একক বক্তাকে দেওয়া হয়।",
|
||||||
|
|
||||||
|
"settings.storage.title": "স্টোরেজ",
|
||||||
|
"settings.storage.location_label": "অবস্থান",
|
||||||
|
"settings.storage.location_hint": "স্টোরেজ অবস্থান পরিবর্তন এখনও সমর্থিত নয় — বর্তমানে মিটিং, মডেল এবং ডেটাবেস এখানেই থাকে।",
|
||||||
|
"settings.storage.retention_title": "ধারণ",
|
||||||
|
"settings.storage.retention_hint": "একটি সীমার বাইরে থাকা সবচেয়ে পুরনো মিটিং স্বয়ংক্রিয়ভাবে মুছে ফেলে। স্টার্টআপে একবার পরীক্ষা করা হয়; খালি মানে কোনো সীমা নেই। বর্তমানে রেকর্ড হচ্ছে এমন মিটিং কখনও স্পর্শ করে না।",
|
||||||
|
"settings.storage.max_age": "সর্বোচ্চ বয়স (দিন)",
|
||||||
|
"settings.storage.max_size": "সর্বোচ্চ আকার (GB)",
|
||||||
|
"settings.storage.no_limit": "কোনো সীমা নেই",
|
||||||
|
"settings.storage.export_import_title": "রপ্তানি ও আমদানি",
|
||||||
|
"settings.storage.export_hint_1": "রেকর্ডিংকে বহনযোগ্য বান্ডল ফোল্ডার হিসেবে কম্পিউটারের মধ্যে সরান (অডিও, ট্রান্সক্রিপ্ট, নোট, সারাংশ এবং একটি",
|
||||||
|
"settings.storage.export_hint_2": "ম্যানিফেস্ট)। যেকোনো ফোল্ডারে রপ্তানি করুন — একটি সিঙ্ক করা ড্রাইভ, একটি USB স্টিক, বা কোনো সিঙ্ক লক্ষ্যের স্থানীয় মাউন্টে — তারপর অন্য মেশিনে এটি আমদানি করুন। আমদানি করা মিটিং একটি নতুন id পায়, তাই পুনরায় আমদানি কখনও কিছু ওভাররাইট করে না।",
|
||||||
|
"settings.storage.export_dialog_title": "সব মিটিং এতে রপ্তানি করুন…",
|
||||||
|
"settings.storage.import_dialog_title": "একটি বান্ডল (বা বান্ডলের একটি ফোল্ডার) আমদানি করুন…",
|
||||||
|
"settings.storage.exporting": "রপ্তানি হচ্ছে…",
|
||||||
|
"settings.storage.export_all": "সব মিটিং রপ্তানি করুন…",
|
||||||
|
"settings.storage.importing": "আমদানি হচ্ছে…",
|
||||||
|
"settings.storage.import_all": "মিটিং আমদানি করুন…",
|
||||||
|
"settings.storage.exported_one": "{n}টি মিটিং রপ্তানি করা হয়েছে।",
|
||||||
|
"settings.storage.exported_many": "{n}টি মিটিং রপ্তানি করা হয়েছে।",
|
||||||
|
"settings.storage.imported_one": "{n}টি মিটিং আমদানি করা হয়েছে।",
|
||||||
|
"settings.storage.imported_many": "{n}টি মিটিং আমদানি করা হয়েছে।",
|
||||||
|
|
||||||
|
"settings.calendar.title": "ক্যালেন্ডার ও Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "একটি স্থানীয় Outlook ব্যাকআপ থেকে ইভেন্ট ও অংশগ্রহণকারীদের আমদানি করুন",
|
||||||
|
"settings.calendar.intro_2": "— শুধু পড়া, ফাইলে কিছু ফিরে লেখা হয় না। এই ডিভাইস থেকে কিছুই বেরোয় না।",
|
||||||
|
"settings.calendar.pst_file": ".pst ফাইল",
|
||||||
|
"settings.calendar.no_file": "কোনো ফাইল নির্বাচিত নেই",
|
||||||
|
"settings.calendar.browse": "ব্রাউজ করুন…",
|
||||||
|
"settings.calendar.password": "পাসওয়ার্ড",
|
||||||
|
"settings.calendar.password_note": "খুব কমই প্রয়োজন",
|
||||||
|
"settings.calendar.optional": "ঐচ্ছিক",
|
||||||
|
"settings.calendar.import_range": "আমদানি পরিসর",
|
||||||
|
"settings.calendar.range_30": "গত ৩০ দিন",
|
||||||
|
"settings.calendar.range_90": "গত ৯০ দিন",
|
||||||
|
"settings.calendar.range_180": "গত ৬ মাস",
|
||||||
|
"settings.calendar.range_365": "গত ১ বছর",
|
||||||
|
"settings.calendar.range_all": "সব সময়",
|
||||||
|
"settings.calendar.range_hint": "একটি দীর্ঘস্থায়ী মেইলবক্সে বছরের পর বছর পুনরাবৃত্ত/ছুটির এন্ট্রি থাকতে পারে — পরিসর সংকীর্ণ করলে আমদানি করা ক্যালেন্ডার প্রকৃতপক্ষে প্রাসঙ্গিক বিষয়েই সীমাবদ্ধ থাকে। এটি এই আমদানি বোতাম এবং চালুর সময় স্বয়ংক্রিয় পুনরায় আমদানি উভয়ে প্রযোজ্য।",
|
||||||
|
"settings.calendar.importing": "আমদানি হচ্ছে…",
|
||||||
|
"settings.calendar.import": "আমদানি করুন",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} ইভেন্ট",
|
||||||
|
"settings.calendar.auto_reimport": "চালুর সময় এই ফাইলটি স্বয়ংক্রিয়ভাবে পুনরায় আমদানি করুন",
|
||||||
|
"settings.calendar.auto_reimport_hint": "স্টার্টআপে একবার চলে, টাইমারে নয় — পুনরায় আমদানি নিরাপদে পুনরাবৃত্তি করা যায় (বিদ্যমান ইভেন্ট মিলিয়ে আপডেট করা হয়, ডুপ্লিকেট হয় না)।",
|
||||||
|
"settings.calendar.import_failed": "আমদানি ব্যর্থ: {error} — ফাইলটি নিজে অক্ষত থাকে; পথ পরীক্ষা করে আবার চেষ্টা করুন।",
|
||||||
|
"settings.calendar.cleanup_title": "পরিষ্কার করুন",
|
||||||
|
"settings.calendar.cleanup_hint": "কোনো রেকর্ড করা মিটিংয়ের সাথে ইতিমধ্যে সংযুক্ত ইভেন্ট সবসময় রাখা হয়, নিচে যেকোনো বিকল্প বেছে নিলেও।",
|
||||||
|
"settings.calendar.cleanup_30": "৩০ দিনের চেয়ে পুরনো",
|
||||||
|
"settings.calendar.cleanup_90": "৯০ দিনের চেয়ে পুরনো",
|
||||||
|
"settings.calendar.cleanup_180": "৬ মাসের চেয়ে পুরনো",
|
||||||
|
"settings.calendar.cleanup_365": "১ বছরের চেয়ে পুরনো",
|
||||||
|
"settings.calendar.cleanup_all": "সব মুছুন",
|
||||||
|
"settings.calendar.cleaning": "পরিষ্কার করা হচ্ছে…",
|
||||||
|
"settings.calendar.cleanup_btn": "ক্যালেন্ডার পরিষ্কার করুন",
|
||||||
|
"settings.calendar.cleanup_result": "{deleted}{extra} মোছা হয়েছে।",
|
||||||
|
"settings.calendar.cleanup_kept": ", {n}টি রাখা হয়েছে (একটি মিটিংয়ের সাথে সংযুক্ত)",
|
||||||
|
"settings.calendar.cleanup_failed": "পরিষ্কার করা ব্যর্থ: {error}",
|
||||||
|
"settings.calendar.imported_events": "আমদানি করা ইভেন্ট",
|
||||||
|
"settings.calendar.no_events": "এখনও কোনো ইভেন্ট আমদানি করা হয়নি।",
|
||||||
|
"settings.calendar.search_title": "শিরোনাম অনুসন্ধান করুন",
|
||||||
|
"settings.calendar.search_placeholder": "মিটিংয়ের নাম…",
|
||||||
|
"settings.calendar.date_label": "তারিখ",
|
||||||
|
"settings.calendar.events_count": "{total}টির মধ্যে {shown}টি ইভেন্ট",
|
||||||
|
"settings.calendar.untitled": "(শিরোনামহীন)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "সেটিংস",
|
||||||
|
"settings.close": "বন্ধ করুন",
|
||||||
|
"settings.close_title": "বন্ধ করুন (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "প্রিভিউ মোড — সিঙ্ক ব্যাকএন্ড এখনও বাস্তবায়িত হয়নি (পর্যায় ৯)। পরিবর্তনগুলি শুধু ইন্টারফেসে রাখা হয়।",
|
||||||
|
"settings.sync.title": "সিঙ্ক ও আপলোড",
|
||||||
|
"settings.sync.enable_label": "কনফিগার করা লক্ষ্যে মিটিং আর্টিফ্যাক্ট আপলোড সক্ষম করুন",
|
||||||
|
"settings.sync.enable_hint": "ডিফল্টভাবে বন্ধ। এটি চালু না থাকলে এবং একটি লক্ষ্য সক্ষম না থাকলে কিছুই আপলোড হয় না। স্ব-হোস্ট করা লক্ষ্য ডেটা আপনার নিজের সার্ভারে রাখে; তৃতীয়-পক্ষের ক্লাউড স্পষ্টভাবে চিহ্নিত।",
|
||||||
|
"settings.sync.targets_title": "লক্ষ্য",
|
||||||
|
"settings.sync.no_targets": "এখনও কোনো লক্ষ্য নেই। নিচে একটি যোগ করুন।",
|
||||||
|
"settings.sync.third_party": "তৃতীয়-পক্ষ",
|
||||||
|
"settings.sync.your_server": "আপনার সার্ভার",
|
||||||
|
"settings.sync.edit": "সম্পাদনা করুন",
|
||||||
|
"settings.sync.remove": "সরান",
|
||||||
|
"settings.sync.edit_target": "লক্ষ্য সম্পাদনা করুন",
|
||||||
|
"settings.sync.add_target": "একটি লক্ষ্য যোগ করুন",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV Nextcloud, ownCloud, Cloudreve, Seafile এবং Synology কভার করে। (Seafile: সার্ভার-সাইডে SeafDAV সক্ষম করুন।)",
|
||||||
|
"settings.sync.name": "নাম",
|
||||||
|
"settings.sync.name_placeholder": "হোম Nextcloud",
|
||||||
|
"settings.sync.provider": "প্রদানকারী",
|
||||||
|
"settings.sync.server_url": "সার্ভার URL",
|
||||||
|
"settings.sync.server_url_note_1": "শুধু আপনার সার্ভার URL — WhispAssist স্বয়ংক্রিয়ভাবে",
|
||||||
|
"settings.sync.server_url_note_2": "যোগ করে।",
|
||||||
|
"settings.sync.remote_folder": "দূরবর্তী ফোল্ডার",
|
||||||
|
"settings.sync.username": "ব্যবহারকারীর নাম",
|
||||||
|
"settings.sync.app_password": "অ্যাপ পাসওয়ার্ড",
|
||||||
|
"settings.sync.pw_keep": "বর্তমান পাসওয়ার্ড রাখতে খালি রাখুন",
|
||||||
|
"settings.sync.pw_store": "OS ক্রেডেনশিয়াল স্টোরে সংরক্ষিত",
|
||||||
|
"settings.sync.upload_legend": "আপলোড",
|
||||||
|
"settings.sync.artifact_transcript": "ট্রান্সক্রিপ্ট",
|
||||||
|
"settings.sync.artifact_notes": "নোট",
|
||||||
|
"settings.sync.artifact_summary": "সারাংশ",
|
||||||
|
"settings.sync.artifact_recording": "রেকর্ডিং (.wav, যদি রাখা হয়)",
|
||||||
|
"settings.sync.allow_plaintext": "একটি LAN ঠিকানার জন্য প্লেইনটেক্সট http অনুমতি দিন (প্রস্তাবিত নয়)",
|
||||||
|
"settings.sync.encrypt_before": "আপলোডের আগে এনক্রিপ্ট করুন (গন্তব্য শুধু সাইফারটেক্সট সংরক্ষণ করে)",
|
||||||
|
"settings.sync.requires_vault": "— একটি আনলক করা ভল্ট প্রয়োজন",
|
||||||
|
"settings.sync.test_connection": "সংযোগ পরীক্ষা করুন",
|
||||||
|
"settings.sync.save_changes": "পরিবর্তন সংরক্ষণ করুন",
|
||||||
|
"settings.sync.add_target_btn": "লক্ষ্য যোগ করুন",
|
||||||
|
"settings.sync.cancel": "বাতিল করুন",
|
||||||
|
"settings.sync.third_party_banner": "{kind} একটি তৃতীয়-পক্ষের ক্লাউড — আপলোড করলে আপনার ডেটা আপনার ডিভাইসের বাইরে {kind}-এ পাঠানো হয়।",
|
||||||
|
"settings.sync.link_account": "{kind} অ্যাকাউন্ট লিঙ্ক করুন…",
|
||||||
|
"settings.sync.oauth_hint": "একটি OAuth সাইন-ইন খোলে (লুপব্যাক রিডাইরেক্ট); টোকেন আপনার OS ক্রেডেনশিয়াল স্টোরে সংরক্ষিত হয়।",
|
||||||
|
"settings.sync.footnote": "ক্রেডেনশিয়াল কখনও সেটিংস বা ডেটাবেসে লেখা হয় না — শুধু OS ক্রেডেনশিয়াল স্টোরে। LAN-বহির্ভূত লক্ষ্যের জন্য TLS প্রয়োজন।",
|
||||||
|
|
||||||
|
"settings.ai.title": "এআই সারাংশ প্রদানকারী",
|
||||||
|
"settings.ai.intro_1": "সারাংশ ডিফল্টভাবে একটি স্থানীয় LLM-এ চলে। এটিকে এই PC বা আপনার LAN-এর অন্য একটি মেশিনে Ollama-এর দিকে নির্দেশ করুন (যেমন",
|
||||||
|
"settings.ai.intro_2": "— উভয়ই স্থানীয় হিসেবে গণ্য হয়, তাই আপনার নেটওয়ার্ক থেকে কিছুই বেরোয় না। হোস্ট করা প্রদানকারী (Anthropic) ঐচ্ছিক, ডিফল্টভাবে বন্ধ, এবং আপনি একটি চালু করামাত্র ট্রান্সক্রিপ্ট একটি তৃতীয় পক্ষে পাঠায়।",
|
||||||
|
"settings.ai.provider": "প্রদানকারী",
|
||||||
|
"settings.ai.off": "বন্ধ",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (স্থানীয় / LAN)",
|
||||||
|
"settings.ai.provider_custom": "কাস্টম (OpenAI-সামঞ্জস্যপূর্ণ)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — হোস্ট করা, এই ডিভাইস ছেড়ে যায়",
|
||||||
|
"settings.ai.endpoint": "এন্ডপয়েন্ট",
|
||||||
|
"settings.ai.model": "মডেল",
|
||||||
|
"settings.ai.api_key": "API কী",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (ইতিমধ্যে সেট — রাখতে খালি রাখুন)",
|
||||||
|
"settings.ai.show_api_key": "API কী দেখান",
|
||||||
|
"settings.ai.hide_api_key": "API কী লুকান",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic একটি হোস্ট করা তৃতীয়-পক্ষের সেবা — আপনি সারাংশ তৈরি করলে এই মিটিংয়ের ট্রান্সক্রিপ্ট আপনার ডিভাইস ছেড়ে যায়।",
|
||||||
|
"settings.ai.endpoint_banner": "এই এন্ডপয়েন্টটি আপনার মেশিন বা LAN-এ নেই — আপনার ট্রান্সক্রিপ্ট আপনার নেটওয়ার্ক ছেড়ে যাবে।",
|
||||||
|
"settings.ai.saving": "সংরক্ষণ হচ্ছে…",
|
||||||
|
"settings.ai.save": "সংরক্ষণ করুন",
|
||||||
|
"settings.ai.test_connection": "সংযোগ পরীক্ষা করুন",
|
||||||
|
"settings.ai.this_hosted_provider": "এই হোস্ট করা প্রদানকারী",
|
||||||
|
"settings.ai.reachable": "পৌঁছানো যায়",
|
||||||
|
"settings.ai.unreachable": "পৌঁছানো যায় না",
|
||||||
|
"settings.ai.model_count_one": "· {n}টি মডেল",
|
||||||
|
"settings.ai.model_count_many": "· {n}টি মডেল",
|
||||||
|
"settings.ai.on_lan": "· আপনার মেশিন / LAN-এ",
|
||||||
|
"settings.ai.leaves_network": "· আপনার নেটওয়ার্ক ছেড়ে যায়",
|
||||||
|
"settings.ai.advanced_title": "উন্নত Ollama কনফিগারেশন",
|
||||||
|
"settings.ai.changed": "{n}টি পরিবর্তিত",
|
||||||
|
"settings.ai.reset_all": "সব ডিফল্টে রিসেট করুন",
|
||||||
|
"settings.ai.system_prompt": "সিস্টেম প্রম্পট",
|
||||||
|
"settings.ai.system_prompt_placeholder": "যেমন আপনি একজন সংক্ষিপ্ত মিটিং সারাংশকারী।",
|
||||||
|
"settings.ai.system_prompt_hint": "WhispAssist-এর প্রয়োজনীয় আউটপুট বিন্যাসের আগে যোগ করা হয়, যাতে আপনার নির্দেশনা সারাংশ/অ্যাকশন-আইটেম পার্সিং ভাঙতে না পারে।",
|
||||||
|
"settings.ai.think": "চিন্তা করুন",
|
||||||
|
"settings.ai.think_help": "যুক্তি প্রচেষ্টা (শুধু যুক্তি মডেল)।",
|
||||||
|
"settings.ai.keep_alive": "সক্রিয় রাখুন",
|
||||||
|
"settings.ai.keep_alive_help": "মডেল কতক্ষণ RAM-এ থাকে। যেমন 5m, 1h, 0 (আনলোড), -1 (চিরকাল)।",
|
||||||
|
"settings.ai.runtime_hardware": "রানটাইম ও হার্ডওয়্যার",
|
||||||
|
"settings.ai.rarely_needed": "— খুব কমই প্রয়োজন",
|
||||||
|
"settings.ai.save_advanced": "উন্নত সংরক্ষণ করুন",
|
||||||
|
"settings.ai.saved": "সংরক্ষিত",
|
||||||
|
"settings.ai.turn_off": "বন্ধ করুন",
|
||||||
|
"settings.ai.default_value": "ডিফল্ট {value}।",
|
||||||
|
"settings.ai.reset_default": "ডিফল্টে রিসেট করুন",
|
||||||
|
"settings.ai.reset_field": "{name} রিসেট করুন",
|
||||||
|
|
||||||
|
"settings.mcp.title": "MCP সার্ভার",
|
||||||
|
"settings.mcp.banner_1": "এটি আপনার নিজের কোডিং এজেন্টকে (Claude Code, Codex, Copilot, OpenCode, …) আপনার স্থানীয় মেশিনে মিটিং প্রসঙ্গ টানতে দেয়। সংযুক্ত হওয়ার পর,",
|
||||||
|
"settings.mcp.banner_agent": "সেই এজেন্ট",
|
||||||
|
"settings.mcp.banner_2": "যা পড়ে তা তার নিজস্ব মডেল প্রদানকারীর ক্লাউডে ফরোয়ার্ড করতে পারে — WhispAssist-এর নিয়ন্ত্রণের বাইরে। WhispAssist নিজে কখনও এই ডেটা কোথাও পাঠায় না; সার্ভার শুধু এই ডিভাইসে শোনে (",
|
||||||
|
"settings.mcp.banner_3": ") এবং প্রতিটি রিড নিচে লগ করা হয়।",
|
||||||
|
"settings.mcp.enable": "MCP সার্ভার সক্ষম করুন",
|
||||||
|
"settings.mcp.enable_hint": "ডিফল্টভাবে বন্ধ। শুধু লুপব্যাক, টোকেন-সুরক্ষিত — নেটওয়ার্ক থেকে কিছুই পৌঁছানো যায় না।",
|
||||||
|
"settings.mcp.transport": "ট্রান্সপোর্ট",
|
||||||
|
"settings.mcp.transport_http": "স্ট্রিমিং HTTP",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (এজেন্ট একটি প্রক্রিয়া তৈরি করে)",
|
||||||
|
"settings.mcp.port": "পোর্ট",
|
||||||
|
"settings.mcp.transport_change_hint": "ট্রান্সপোর্ট/পোর্ট পরিবর্তন করতে, প্রথমে সার্ভার বন্ধ করুন, তারপর আবার চালু করুন।",
|
||||||
|
"settings.mcp.scope_title": "পরিধি",
|
||||||
|
"settings.mcp.expose": "উন্মুক্ত করুন",
|
||||||
|
"settings.mcp.scope_none": "কিছু নয় — কিছুই ভাগ করা হয় না",
|
||||||
|
"settings.mcp.scope_selected": "নির্বাচিত — শুধু সেই ফিচার ব্রিফ যা আপনি ভাগ করা হিসেবে চিহ্নিত করেছেন",
|
||||||
|
"settings.mcp.scope_all": "সব — মিটিং, ট্রান্সক্রিপ্ট, অ্যাকশন-আইটেম এবং ভাগ করা ব্রিফ",
|
||||||
|
"settings.mcp.expose_recordings": "সংরক্ষিত রেকর্ডিং সহ মিটিংও অনুমতি দিন (ডিফল্টভাবে বন্ধ — এটি চালু না হওয়া পর্যন্ত একটি রেকর্ড করা মিটিংয়ের ট্রান্সক্রিপ্ট “সব” পরিধিতেও আটকে রাখা হয়)",
|
||||||
|
"settings.mcp.token_title": "নতুন প্রমাণীকরণ টোকেন — একবার দেখানো হয়, এখনই এটি কপি করুন",
|
||||||
|
"settings.mcp.token_hint": "এটি আর দেখানো হবে না। এটি আপনার OS ক্রেডেনশিয়াল স্টোরে সংরক্ষিত; আপনি হারালে, একটি নতুন তৈরি করতে সার্ভার বন্ধ করে আবার চালু করুন।",
|
||||||
|
"settings.mcp.copied": "কপি করা হয়েছে",
|
||||||
|
"settings.mcp.copy": "কপি করুন",
|
||||||
|
"settings.mcp.endpoint": "এন্ডপয়েন্ট",
|
||||||
|
"settings.mcp.endpoint_hint": "আপনার এজেন্টের MCP ক্লায়েন্ট কনফিগারেশন {kind}-এর দিকে নির্দেশ করুন, উপরের টোকেনটিকে bearer ক্রেডেনশিয়াল হিসেবে ব্যবহার করে।",
|
||||||
|
"settings.mcp.kind_command": "কমান্ড",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "অ্যাক্সেস লগ",
|
||||||
|
"settings.mcp.access_log_hint": "একজন এজেন্টের করা প্রতিটি টুল রিড, অনুমোদিত বা প্রত্যাখ্যাত (FR-MCP-5)।",
|
||||||
|
"settings.mcp.no_reads": "এখনও কোনো এজেন্ট কিছু পড়েনি।",
|
||||||
|
"settings.mcp.log_meeting": "মিটিং {id}",
|
||||||
|
"settings.mcp.refresh": "রিফ্রেশ করুন",
|
||||||
|
|
||||||
|
"settings.privacy.title": "গোপনীয়তা",
|
||||||
|
"settings.privacy.intro": "WhispAssist-কে এখন প্রকৃতপক্ষে এই ডিভাইস থেকে বাইরে কী পাঠানোর অনুমতি আছে। সব বন্ধ থাকলে (ডিফল্ট), ডিভাইস থেকে কিছুই বেরোয় না।",
|
||||||
|
"settings.privacy.llm_endpoint": "LLM এন্ডপয়েন্ট",
|
||||||
|
"settings.privacy.off": "বন্ধ",
|
||||||
|
"settings.privacy.local_only": "শুধু স্থানীয়",
|
||||||
|
"settings.privacy.leaves_device": "এই ডিভাইস ছেড়ে যায়",
|
||||||
|
"settings.privacy.sync_label": "সিঙ্ক",
|
||||||
|
"settings.privacy.enabled": "সক্ষম",
|
||||||
|
"settings.privacy.mcp_label": "MCP সার্ভার",
|
||||||
|
"settings.privacy.mcp_on": "চালু · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "শুধু লুপব্যাকে ইনবাউন্ড — এটি উপরের আউটগোয়িং তালিকায় কিছু যোগ করে না। একটি সংযুক্ত এজেন্ট এখনও যা পড়ে তা তার নিজস্ব মডেল প্রদানকারীকে ফরোয়ার্ড করতে পারে; MCP সার্ভার ট্যাব দেখুন।",
|
||||||
|
"settings.privacy.egress_title": "আউটগোয়িং অনুমতি তালিকা",
|
||||||
|
"settings.privacy.no_egress": "কোনো হোস্ট অনুমতি তালিকায় নেই — WA কোনো কনটেন্ট আউটগোয়িং করে না।",
|
||||||
|
"settings.privacy.sync_targets_title": "সিঙ্ক লক্ষ্য",
|
||||||
|
"settings.privacy.third_party": "তৃতীয়-পক্ষ",
|
||||||
|
"settings.privacy.your_server": "আপনার সার্ভার",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "কোনো TLS নেই",
|
||||||
|
"settings.privacy.refresh": "রিফ্রেশ করুন",
|
||||||
|
"settings.privacy.unavailable": "গোপনীয়তা স্ব-পরীক্ষা উপলব্ধ নয়।",
|
||||||
|
"settings.privacy.vault_title": "এনক্রিপশন ভল্ট",
|
||||||
|
"settings.privacy.vault_intro": "একটি পাসওয়ার্ড দিয়ে নোট, ট্রান্সক্রিপ্ট এবং সারাংশ বিশ্রামাবস্থায় এনক্রিপ্ট করুন। (অডিও ফাইল এখনও এনক্রিপ্ট করা হয় না।)",
|
||||||
|
"settings.privacy.vault_password": "ভল্ট পাসওয়ার্ড",
|
||||||
|
"settings.privacy.enable_vault": "ভল্ট সক্ষম করুন",
|
||||||
|
"settings.privacy.vault_pw_hint": "কমপক্ষে ৮টি অক্ষর ব্যবহার করুন। ভুলে গেলে, এনক্রিপ্ট করা কনটেন্ট পুনরুদ্ধার করা যায় না।",
|
||||||
|
"settings.privacy.vault_locked_1": "ভল্ট ",
|
||||||
|
"settings.privacy.locked_word": "লক করা",
|
||||||
|
"settings.privacy.vault_locked_2": "। এনক্রিপ্ট করা মিটিং পড়তে আনলক করুন।",
|
||||||
|
"settings.privacy.password": "পাসওয়ার্ড",
|
||||||
|
"settings.privacy.unlock": "আনলক করুন",
|
||||||
|
"settings.privacy.vault_unlocked_1": "ভল্ট ",
|
||||||
|
"settings.privacy.unlocked_word": "আনলক করা",
|
||||||
|
"settings.privacy.vault_unlocked_2": "। নতুন নোট, ট্রান্সক্রিপ্ট এবং সারাংশ বিশ্রামাবস্থায় এনক্রিপ্ট করা হয়।",
|
||||||
|
"settings.privacy.lock_now": "এখনই লক করুন",
|
||||||
|
"settings.privacy.change_password": "পাসওয়ার্ড পরিবর্তন করুন",
|
||||||
|
"settings.privacy.current_password": "বর্তমান পাসওয়ার্ড",
|
||||||
|
"settings.privacy.new_password": "নতুন পাসওয়ার্ড",
|
||||||
|
|
||||||
|
"settings.about.title": "সম্পর্কে",
|
||||||
|
"settings.about.tagline": "একটি সম্পূর্ণ স্থানীয়, ওপেন-সোর্স, Windows-নেটিভ মিটিং সহকারী।",
|
||||||
|
"settings.about.build_commit": "বিল্ড কমিট",
|
||||||
|
|
||||||
|
"consent.heading": "রেকর্ড করার আগে",
|
||||||
|
"consent.body": "অংশগ্রহণকারীদের সম্মতি ছাড়া কথোপকথন রেকর্ড করা আপনার অঞ্চলে বেআইনি হতে পারে। আপনার স্থানীয় রেকর্ডিং আইন পরীক্ষা করুন। এটি একটি সতর্কতা, আইনি পরামর্শ নয়।",
|
||||||
|
"consent.accept": "আমি বুঝেছি — রেকর্ডিং সক্ষম করুন",
|
||||||
|
"consent.cancel": "বাতিল করুন",
|
||||||
|
|
||||||
|
"hosted.heading": "{provider} ব্যবহারের আগে",
|
||||||
|
"hosted.body_1": "দিয়ে তৈরি করা",
|
||||||
|
"hosted.body_2": "এই মিটিংয়ের ট্রান্সক্রিপ্ট তাদের সার্ভারে পাঠায় — এটি এই ডিভাইস ছেড়ে যায় এবং তাদের গোপনীয়তা নীতির অধীন। ডেটা আপনার ডিভাইস ছাড়ার পর WhispAssist-এর ডেটা হ্যান্ডলিংয়ের উপর কোনো নিয়ন্ত্রণ নেই। এটি ডিফল্টভাবে বন্ধ; আপনি এখন এটি বেছে নিচ্ছেন।",
|
||||||
|
"hosted.accept": "আমি বুঝেছি — চালিয়ে যান",
|
||||||
|
"hosted.cancel": "বাতিল করুন",
|
||||||
|
|
||||||
|
"theme.group": "থিম",
|
||||||
|
"theme.system": "সিস্টেমের সাথে মেলান",
|
||||||
|
"theme.light": "হালকা",
|
||||||
|
"theme.dark": "গাঢ়",
|
||||||
|
|
||||||
|
"import.dialog_label": "একটি ফাইল বা URL থেকে মিটিং যোগ করুন",
|
||||||
|
"import.title": "মিটিং যোগ করুন",
|
||||||
|
"import.close": "বন্ধ করুন",
|
||||||
|
"import.close_title": "বন্ধ করুন (Esc)",
|
||||||
|
"import.body": "একটি বিদ্যমান রেকর্ডিং আমদানি করুন — একটি স্থানীয় অডিও/ভিডিও ফাইল, বা একটি লিঙ্ক (YouTube, একটি স্ট্রিমিং পৃষ্ঠা, বা একটি সরাসরি মিডিয়া URL)। এটি লাইভ রেকর্ডিংয়ের মতোই ট্রান্সক্রাইব ও ডায়ারাইজ করা হয়।",
|
||||||
|
"import.file_or_url": "ফাইল বা URL",
|
||||||
|
"import.source_placeholder": "একটি URL পেস্ট করুন, বা একটি ফাইলের জন্য ব্রাউজ করুন…",
|
||||||
|
"import.choose_file": "একটি স্থানীয় ফাইল বেছে নিন",
|
||||||
|
"import.browse": "ব্রাউজ করুন…",
|
||||||
|
"import.title_label": "শিরোনাম",
|
||||||
|
"import.optional": "ঐচ্ছিক",
|
||||||
|
"import.title_placeholder": "ডিফল্টভাবে ফাইলের নাম",
|
||||||
|
"import.filter_av": "অডিও / ভিডিও",
|
||||||
|
"import.requires_1": "প্রয়োজন",
|
||||||
|
"import.requires_2": "ইনস্টল করা এবং আপনার PATH-এ (সাথে",
|
||||||
|
"import.requires_3": "URL-এর জন্য)। WhispAssist এগুলি বান্ডল করে না।",
|
||||||
|
"import.importing": "আমদানি হচ্ছে… এতে কিছু সময় লাগতে পারে",
|
||||||
|
"import.import": "আমদানি করুন",
|
||||||
|
"import.cancel": "বাতিল করুন",
|
||||||
|
|
||||||
|
"tagchip.filter": "“{tag}” ট্যাগযুক্ত মিটিং ফিল্টার করুন",
|
||||||
|
"tagchip.remove": "ট্যাগ {tag} সরান",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "সিস্টেম ও মাইক্রোফোন ইনপুট স্তর",
|
||||||
|
"levelmeter.system": "সিস্টেম ইনপুট স্তর",
|
||||||
|
|
||||||
|
"app.tagline": "স্থানীয় · ব্যক্তিগত",
|
||||||
|
"app.note_template": "নোট টেমপ্লেট",
|
||||||
|
"app.no_template": "কোনো টেমপ্লেট নেই",
|
||||||
|
"app.add_meeting": "মিটিং যোগ করুন",
|
||||||
|
"app.add_meeting_title": "একটি ফাইল বা URL থেকে মিটিং যোগ করুন",
|
||||||
|
"app.record": "রেকর্ড করুন",
|
||||||
|
"app.record_title": "রেকর্ডিং শুরু করুন (Ctrl+Shift+R)",
|
||||||
|
"app.stop": "থামান",
|
||||||
|
"app.stop_title": "রেকর্ডিং থামান (Ctrl+Shift+R)",
|
||||||
|
"app.cancel": "বাতিল করুন",
|
||||||
|
"app.cancel_title": "এই রেকর্ডিং বাতিল করে মুছুন",
|
||||||
|
"app.recording": "রেকর্ড হচ্ছে…",
|
||||||
|
"app.backend_title": "সক্রিয় ট্রান্সক্রিপশন ব্যাকএন্ড",
|
||||||
|
"app.retention_title": "এই মিটিংয়ের জন্য অডিও .wav হিসেবে সংরক্ষণ করুন",
|
||||||
|
"app.saving": "সংরক্ষণ হচ্ছে",
|
||||||
|
"app.not_saved": "সংরক্ষিত নয়",
|
||||||
|
"app.reconnecting": "অডিও ডিভাইস পুনরায় সংযুক্ত হচ্ছে…",
|
||||||
|
"app.settings": "সেটিংস",
|
||||||
|
"app.settings_title": "সেটিংস (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "এই রেকর্ডিং বাতিল করবেন? এর অডিও ও ট্রান্সক্রিপ্ট মুছে ফেলা হবে।",
|
||||||
|
"app.announce_started": "রেকর্ডিং শুরু হয়েছে",
|
||||||
|
"app.announce_paused": "রেকর্ডিং বিরতি দেওয়া হয়েছে",
|
||||||
|
"app.announce_stopped": "রেকর্ডিং থামানো হয়েছে",
|
||||||
|
"app.consent_dialog": "রেকর্ডিং সম্মতি",
|
||||||
|
"app.vault_dialog": "এনক্রিপশন ভল্ট আনলক করুন",
|
||||||
|
"app.vault_title": "এনক্রিপশন ভল্ট আনলক করুন",
|
||||||
|
"app.vault_desc": "আপনার মিটিং বিশ্রামাবস্থায় এনক্রিপ্ট করা। সেগুলি পড়তে আপনার পাসওয়ার্ড দিন।",
|
||||||
|
"app.vault_password": "ভল্ট পাসওয়ার্ড",
|
||||||
|
"app.vault_incorrect": "ভুল পাসওয়ার্ড",
|
||||||
|
"app.unlock": "আনলক করুন",
|
||||||
|
"app.continue_locked": "লক অবস্থায় চালিয়ে যান",
|
||||||
|
"app.show_meetings": "মিটিং তালিকা দেখান",
|
||||||
|
"app.hide_meetings": "মিটিং তালিকা লুকান",
|
||||||
|
"app.show_summary": "সারাংশ প্যানেল দেখান",
|
||||||
|
"app.hide_summary": "সারাংশ প্যানেল লুকান",
|
||||||
|
"app.resize_meetings": "মিটিং তালিকার আকার পরিবর্তন করুন",
|
||||||
|
"app.resize_summary": "সারাংশ প্যানেলের আকার পরিবর্তন করুন",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "মিটিং অনুসন্ধান করুন…",
|
||||||
|
"meetings.search_aria": "মিটিং অনুসন্ধান করুন",
|
||||||
|
"meetings.filter_tag_aria": "ট্যাগ অনুসারে ফিল্টার করুন",
|
||||||
|
"meetings.all_tags": "সব ট্যাগ",
|
||||||
|
"meetings.from_date_aria": "তারিখ থেকে",
|
||||||
|
"meetings.to_date_aria": "তারিখ পর্যন্ত",
|
||||||
|
"meetings.bulk_format_aria": "বাল্ক রপ্তানি বিন্যাস",
|
||||||
|
"meetings.bulk_export": "বাল্ক রপ্তানি",
|
||||||
|
"meetings.bulk_export_title": "উপরের ট্যাগ/তারিখ ফিল্টারের সাথে মিলে যাওয়া প্রতিটি মিটিং রপ্তানি করুন",
|
||||||
|
"meetings.exporting": "রপ্তানি হচ্ছে…",
|
||||||
|
"meetings.exported_one": "{n}টি মিটিং রপ্তানি করা হয়েছে।",
|
||||||
|
"meetings.exported_many": "{n}টি মিটিং রপ্তানি করা হয়েছে।",
|
||||||
|
"meetings.searching": "অনুসন্ধান হচ্ছে…",
|
||||||
|
"meetings.loading": "মিটিং লোড হচ্ছে…",
|
||||||
|
"meetings.no_matches": "কোনো মিল নেই।",
|
||||||
|
"meetings.empty": "এখনও কোনো মিটিং নেই — শুরু করতে উপরে রেকর্ড ক্লিক করুন।",
|
||||||
|
"meetings.status.recording": "রেকর্ড হচ্ছে",
|
||||||
|
"meetings.status.transcribing": "ট্রান্সক্রাইব হচ্ছে",
|
||||||
|
"meetings.status.recovering": "পুনরুদ্ধার হচ্ছে",
|
||||||
|
"meetings.status.error": "ত্রুটি",
|
||||||
|
"meetings.resume": "ট্রান্সক্রিপশন পুনরায় শুরু করুন",
|
||||||
|
"meetings.delete_aria": "মিটিং মুছুন",
|
||||||
|
"meetings.delete_confirm": "এই মিটিং মুছবেন? এটি এর রেকর্ডিং, ট্রান্সক্রিপ্ট এবং নোট সরিয়ে ফেলে।",
|
||||||
|
|
||||||
|
"transcript.heading": "ট্রান্সক্রিপ্ট",
|
||||||
|
"transcript.title_aria": "মিটিং শিরোনাম",
|
||||||
|
"transcript.lang_title": "ট্রান্সক্রিপশন ভাষা",
|
||||||
|
"transcript.lang_auto": "শনাক্ত হচ্ছে…",
|
||||||
|
"transcript.show": "ট্রান্সক্রিপ্ট দেখান",
|
||||||
|
"transcript.hide": "ট্রান্সক্রিপ্ট লুকান",
|
||||||
|
"transcript.empty": "এই মিটিংয়ের জন্য কোনো ট্রান্সক্রিপ্ট নেই।",
|
||||||
|
"transcript.will_appear": "আপনি রেকর্ড করার সাথে সাথে ট্রান্সক্রিপ্ট এখানে দেখা যাবে।",
|
||||||
|
"transcript.play_from_here": "এখান থেকে চালান",
|
||||||
|
"transcript.resize": "ট্রান্সক্রিপ্ট ও নোটের আকার পরিবর্তন করুন",
|
||||||
|
"transcript.reprocess_placeholder": "এটি দিয়ে পুনরায় ট্রান্সক্রাইব করুন…",
|
||||||
|
"transcript.reprocess_lang_aria": "পুনঃপ্রক্রিয়াকরণ ভাষা",
|
||||||
|
"transcript.reprocess_keep_lang": "বর্তমান ভাষা রাখুন",
|
||||||
|
"transcript.reprocess_go": "চালান",
|
||||||
|
"transcript.retranscribing": "পুনরায় ট্রান্সক্রাইব হচ্ছে…",
|
||||||
|
"transcript.has_note": "একটি নোট আছে",
|
||||||
|
"transcript.note_placeholder": "এই মুহূর্তের জন্য একটি নোট যোগ করুন…",
|
||||||
|
"transcript.note_aria": "এই ট্রান্সক্রিপ্ট লাইনের জন্য নোট",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Word নথি",
|
||||||
|
"transcript.filter_obsidian": "Obsidian নোট",
|
||||||
|
|
||||||
|
"notes.heading": "নোট",
|
||||||
|
"notes.show": "নোট দেখান",
|
||||||
|
"notes.hide": "নোট লুকান",
|
||||||
|
"notes.placeholder": "নোট…",
|
||||||
|
"notes.live_placeholder": "কথা বলার সময় নোট টাইপ করুন…",
|
||||||
|
"notes.toolbar_aria": "নোট বিন্যাস",
|
||||||
|
"notes.bold": "গাঢ়",
|
||||||
|
"notes.italic": "তির্যক",
|
||||||
|
"notes.h1": "শিরোনাম ১",
|
||||||
|
"notes.h2": "শিরোনাম ২",
|
||||||
|
"notes.bullet": "বুলেট তালিকা",
|
||||||
|
"notes.checkbox_title": "চেকবক্স",
|
||||||
|
"notes.checkbox_aria": "চেকবক্স তালিকা আইটেম",
|
||||||
|
"notes.edit_raw": "কাঁচা markdown সম্পাদনা করুন",
|
||||||
|
"notes.render": "markdown রেন্ডার করুন",
|
||||||
|
"notes.editor": "সম্পাদক",
|
||||||
|
"notes.preview": "প্রিভিউ",
|
||||||
|
"notes.export_md_title": "নোট .md হিসেবে রপ্তানি করুন",
|
||||||
|
"notes.export_pdf_title": "নোট .pdf হিসেবে রপ্তানি করুন",
|
||||||
|
"notes.export_docx_title": "নোট .docx হিসেবে রপ্তানি করুন",
|
||||||
|
"notes.export_bundle_title": "অডিও + ট্রান্সক্রিপ্ট + নোট একটি ফোল্ডারে রপ্তানি করুন",
|
||||||
|
"notes.export_obsidian_title": "একটি Obsidian নোট রপ্তানি করুন (নোট + সারাংশ + ট্রান্সক্রিপ্ট, কোনো অডিও নয়)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "রেকর্ডিং",
|
||||||
|
"summary.loading_recording": "রেকর্ডিং লোড হচ্ছে…",
|
||||||
|
"summary.sync_heading": "সিঙ্ক",
|
||||||
|
"summary.uploading": "আপলোড হচ্ছে…",
|
||||||
|
"summary.upload_now": "এখন আপলোড করুন",
|
||||||
|
"summary.no_uploads": "এই মিটিংয়ের জন্য এখনও কোনো আপলোড নেই।",
|
||||||
|
"summary.retry": "আবার চেষ্টা করুন",
|
||||||
|
"summary.tags_heading": "ট্যাগ",
|
||||||
|
"summary.tags_select": "একটি মিটিং ট্যাগ করতে সেটি নির্বাচন করুন।",
|
||||||
|
"summary.tag_add_placeholder": "ট্যাগ যোগ করুন…",
|
||||||
|
"summary.tag_first_placeholder": "প্রকল্প, ক্লায়েন্ট, বিষয়…",
|
||||||
|
"summary.generating": "তৈরি হচ্ছে…",
|
||||||
|
"summary.generate_tags": "ট্যাগ তৈরি করুন",
|
||||||
|
"summary.saving": "সংরক্ষণ হচ্ছে…",
|
||||||
|
"summary.save_tags": "ট্যাগ সংরক্ষণ করুন",
|
||||||
|
"summary.summary_heading": "সারাংশ",
|
||||||
|
"summary.ai_provider": "এআই প্রদানকারী",
|
||||||
|
"summary.provider_off": "বন্ধ",
|
||||||
|
"summary.provider_custom": "কাস্টম",
|
||||||
|
"summary.local": "স্থানীয়",
|
||||||
|
"summary.leaves_device": "এই ডিভাইস ছেড়ে যায়",
|
||||||
|
"summary.this_hosted_provider": "এই হোস্ট করা প্রদানকারী",
|
||||||
|
"summary.select_generate": "একটি সারাংশ তৈরি করতে একটি মিটিং নির্বাচন করুন।",
|
||||||
|
"summary.decisions_heading": "সিদ্ধান্ত",
|
||||||
|
"summary.regenerate": "পুনরায় তৈরি করুন",
|
||||||
|
"summary.generated_hint": "মিটিংয়ের পর স্থানীয়ভাবে তৈরি (একটি স্থানীয় LLM প্রদানকারী প্রয়োজন)।",
|
||||||
|
"summary.no_provider": "কোনো এআই প্রদানকারী কনফিগার করা নেই — সেটিংসে একটি সক্ষম করুন।",
|
||||||
|
"summary.generate_summary": "সারাংশ তৈরি করুন",
|
||||||
|
"summary.briefs_heading": "ফিচার ব্রিফ",
|
||||||
|
"summary.briefs_select": "ফিচার ব্রিফ তৈরি বা দেখতে একটি মিটিং নির্বাচন করুন।",
|
||||||
|
"summary.briefs_desc": "এই মিটিংকে একটি এজেন্ট-প্রস্তুত স্পেসিফিকেশনে পরিশ্রুত করে (একটি স্থানীয় LLM প্রদানকারী প্রয়োজন) — এটি একটি কোডিং এজেন্টকে দিন, বা MCP সার্ভার চালু হলে এর মাধ্যমে পরিবেশন করুন।",
|
||||||
|
"summary.brief_repo_placeholder": "লক্ষ্য রেপো (ঐচ্ছিক), যেমন acme/reporting-web",
|
||||||
|
"summary.distilling": "পরিশ্রুত হচ্ছে…",
|
||||||
|
"summary.create_brief": "ফিচার ব্রিফ তৈরি করুন",
|
||||||
|
"summary.brief_mcp_title": "MCP সার্ভার চালু থাকলে উপলব্ধ",
|
||||||
|
"summary.copied": "কপি করা হয়েছে",
|
||||||
|
"summary.copy_md": "Markdown হিসেবে কপি করুন",
|
||||||
|
"summary.copy_json": "JSON হিসেবে কপি করুন",
|
||||||
|
"summary.brief_problem": "সমস্যা",
|
||||||
|
"summary.brief_outcome": "কাঙ্ক্ষিত ফলাফল",
|
||||||
|
"summary.brief_criteria": "গ্রহণযোগ্যতার মানদণ্ড",
|
||||||
|
"summary.brief_none": "কিছু ক্যাপচার করা হয়নি।",
|
||||||
|
"summary.brief_context": "প্রসঙ্গ",
|
||||||
|
"summary.action_items_heading": "অ্যাকশন-আইটেম",
|
||||||
|
"summary.ai_select": "এর অ্যাকশন-আইটেম দেখতে একটি মিটিং নির্বাচন করুন।",
|
||||||
|
"summary.ai_empty": "এখনও কোনোটি নেই — নিচে একটি যোগ করুন, বা সেগুলি স্বয়ংক্রিয়ভাবে বের করতে একটি সারাংশ তৈরি করুন।",
|
||||||
|
"summary.confirmed": "নিশ্চিত",
|
||||||
|
"summary.ai_text_placeholder": "অ্যাকশন-আইটেম…",
|
||||||
|
"summary.ai_text_aria": "অ্যাকশন-আইটেম টেক্সট",
|
||||||
|
"summary.owner": "মালিক",
|
||||||
|
"summary.due_date": "নির্ধারিত তারিখ",
|
||||||
|
"summary.reminder_title": "এই নির্ধারিত তারিখের জন্য একটি স্থানীয় অনুস্মারক নির্ধারণ করুন",
|
||||||
|
"summary.ai_delete_title": "এই অ্যাকশন-আইটেম মুছুন",
|
||||||
|
"summary.ai_delete_aria": "অ্যাকশন-আইটেম মুছুন",
|
||||||
|
"summary.add_action_item": "অ্যাকশন-আইটেম যোগ করুন",
|
||||||
|
"summary.save_action_items": "অ্যাকশন-আইটেম সংরক্ষণ করুন",
|
||||||
|
"summary.calendar_heading": "ক্যালেন্ডার ইভেন্ট",
|
||||||
|
"summary.cal_select": "একটি মিটিংকে ক্যালেন্ডার ইভেন্টের সাথে লিঙ্ক করতে সেটি নির্বাচন করুন।",
|
||||||
|
"summary.event_search_placeholder": "ইভেন্ট শিরোনাম অনুসন্ধান করুন…",
|
||||||
|
"summary.linked_event": "লিঙ্ক করা ইভেন্ট",
|
||||||
|
"summary.change_event": "ইভেন্ট পরিবর্তন করুন…",
|
||||||
|
"summary.link_event": "একটি ইভেন্ট লিঙ্ক করুন…",
|
||||||
|
"summary.untitled_event": "(শিরোনামহীন)",
|
||||||
|
"summary.no_events_1": "এখনও কোনো ইভেন্ট আমদানি করা হয়নি — একটি",
|
||||||
|
"summary.no_events_2": "সেটিংস → ক্যালেন্ডার থেকে আমদানি করুন।",
|
||||||
|
"summary.no_events_match": "এই অনুসন্ধান/তারিখের সাথে কোনো ইভেন্ট মেলে না — একটি সাফ করার চেষ্টা করুন।",
|
||||||
|
"summary.organizer_label": "আয়োজক: {name}",
|
||||||
|
"summary.participants_heading": "অংশগ্রহণকারী",
|
||||||
|
"summary.participants_hint": "লিঙ্ক করা ক্যালেন্ডার ইভেন্ট থেকে পূরণ করা হয়।",
|
||||||
|
"summary.speakers_heading": "বক্তা",
|
||||||
|
"summary.speakers_select": "এর বক্তাদের নাম দিতে একটি মিটিং নির্বাচন করুন।",
|
||||||
|
"summary.no_speakers": "এখনও কোনো বক্তা শনাক্ত হয়নি।",
|
||||||
|
"summary.speaker_name_placeholder": "বক্তার নাম",
|
||||||
|
"summary.save": "সংরক্ষণ করুন",
|
||||||
|
"summary.cancel": "বাতিল করুন",
|
||||||
|
"summary.name_speaker": "এই বক্তার নাম দিন…",
|
||||||
|
"summary.add_new_name": "+ নতুন নাম যোগ করুন…",
|
||||||
|
|
||||||
|
"settings.recording.title": "রেকর্ডিং",
|
||||||
|
"settings.recording.record_default": "ডিফল্টভাবে মিটিং রেকর্ড করুন",
|
||||||
|
"settings.recording.save_audio_as": "অডিও এই হিসেবে সংরক্ষণ করুন",
|
||||||
|
"settings.recording.default_hint": "ডিফল্টভাবে বন্ধ। বন্ধ থাকলে, অডিও শুধু ট্রান্সক্রিপ্ট তৈরিতে ব্যবহৃত হয় এবং মিটিং চূড়ান্ত হলে মুছে ফেলা হয়। আপনি প্রতি মিটিংয়ে রেকর্ডিং টগলও করতে পারেন।",
|
||||||
|
"settings.recording.consent_label": "সম্মতি: {status}।",
|
||||||
|
"settings.recording.consent_ack": "স্বীকৃত",
|
||||||
|
"settings.recording.consent_not": "এখনও স্বীকৃত নয়",
|
||||||
|
"settings.recording.auto_label": "কোনো ক্যালেন্ডার ইভেন্ট শুরু হলে স্বয়ংক্রিয়ভাবে রেকর্ডিং শুরু করুন",
|
||||||
|
"settings.recording.auto_hint": "শুধু WhispAssist খোলা থাকাকালীন। কোনো আমদানি করা ক্যালেন্ডার ইভেন্টের শুরুর সময় এলে, একটি রেকর্ডিং স্বয়ংক্রিয়ভাবে শুরু হয় (উপরের আপনার ডিফল্ট ধারণ সেটিং ব্যবহার করে)। পটভূমিতে কিছুই চলে না — টাইমার শুধু অ্যাপ চলাকালীন সক্রিয় থাকে। সেটিংস → ক্যালেন্ডার-এর অধীনে ইভেন্ট আমদানি করুন।"
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "Aufnahme",
|
||||||
|
"nav.hardware": "Hardware",
|
||||||
|
"nav.storage": "Speicher",
|
||||||
|
"nav.calendar": "Kalender",
|
||||||
|
"nav.sync": "Synchronisierung",
|
||||||
|
"nav.ai": "KI",
|
||||||
|
"nav.mcp": "MCP-Server",
|
||||||
|
"nav.privacy": "Datenschutz",
|
||||||
|
"nav.about": "Über",
|
||||||
|
"nav.language": "Sprache",
|
||||||
|
|
||||||
|
"settings.language.title": "Sprache",
|
||||||
|
"settings.language.display": "Anzeigesprache",
|
||||||
|
"settings.language.display_hint": "Die Sprache der App-Oberfläche. Eine Sprache hinzuzufügen ist so einfach wie eine einzige Übersetzungsdatei abzulegen.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "Transkriptionssprache",
|
||||||
|
"settings.transcription.label": "Sprache",
|
||||||
|
"settings.transcription.auto": "Automatisch erkennen",
|
||||||
|
"settings.transcription.applies_hint": "Gilt für die nächste Aufnahme. Die tatsächlich verwendete Sprache wird danach bei jedem Meeting angezeigt.",
|
||||||
|
"settings.transcription.english_only": "Nur Englisch",
|
||||||
|
"settings.transcription.model_english_only": "„{model}“ ist nur auf Englisch verfügbar.",
|
||||||
|
"settings.transcription.no_model": "Kein Modell ausgewählt.",
|
||||||
|
"settings.transcription.switch_multilingual": "Wechseln Sie oben zu einem mehrsprachigen Modell, um eine Sprache zu wählen.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "Hardware",
|
||||||
|
"settings.hardware.active_backend": "Aktives Backend",
|
||||||
|
"settings.hardware.model_meta": "· Modell {size}",
|
||||||
|
"settings.hardware.preferred_backend": "Bevorzugtes Backend",
|
||||||
|
"settings.hardware.auto_backend": "Automatisch (bestes verfügbares)",
|
||||||
|
"settings.hardware.not_available": "nicht verfügbar",
|
||||||
|
"settings.hardware.fallback_hint": "Fällt automatisch zurück (NPU → NVIDIA → AMD → Intel → CPU), wenn das gewählte Backend nicht geladen werden kann.",
|
||||||
|
"settings.hardware.audio_devices": "Audiogeräte",
|
||||||
|
"settings.hardware.recording_device": "Aufnahmegerät",
|
||||||
|
"settings.hardware.default_system_audio": "Standard-Systemaudio",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist nimmt auf, was dieses Gerät wiedergibt (Loopback) — die Gegenseite des Gesprächs. Wählen Sie eine bestimmte Ausgabe, wenn sie nicht dem Windows-Systemstandard folgen soll.",
|
||||||
|
"settings.hardware.microphone": "Mikrofon",
|
||||||
|
"settings.hardware.mic_off": "Aus — mein Mikrofon nicht erfassen",
|
||||||
|
"settings.hardware.default_mic": "Standardmikrofon",
|
||||||
|
"settings.hardware.mic_hint": "Fügt Ihre eigene Stimme zum Live-Transkript hinzu, damit beide Seiten des Meetings erfasst werden. Bleibt auf Ihrem Gerät — nichts wird hochgeladen. Wählen Sie „Aus“, um nur das obige Systemaudio zu transkribieren.",
|
||||||
|
"settings.hardware.npu_detected": "NPU-Beschleuniger erkannt",
|
||||||
|
"settings.hardware.ready": "Bereit",
|
||||||
|
"settings.hardware.downloading": "Wird heruntergeladen…",
|
||||||
|
"settings.hardware.package_needed": "Paket erforderlich",
|
||||||
|
"settings.hardware.npu_hint": "Ihre NPU benötigt ein einmaliges Beschleunigungspaket (Whisper-ONNX-Modell{extra}). Es wird im Hintergrund heruntergeladen; Sie können es auch jetzt starten.",
|
||||||
|
"settings.hardware.plus_openvino": " + OpenVINO-Laufzeit",
|
||||||
|
"settings.hardware.download_npu": "NPU-Paket herunterladen",
|
||||||
|
"settings.hardware.dml_title": "GPU-Beschleunigung (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "Ihre GPU kann ohne Vulkan über DirectML beschleunigt werden — ein einmaliges Paket (Whisper-ONNX-Modell{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + DirectML-Laufzeit",
|
||||||
|
"settings.hardware.download_dml": "DirectML-Paket herunterladen",
|
||||||
|
"settings.hardware.detection_unavailable": "Hardware-Erkennung nicht verfügbar.",
|
||||||
|
"settings.hardware.low_overhead": "Sparsam-Voreinstellung",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + kleinstes Modell, für Akku-/Hintergrundnutzung",
|
||||||
|
"settings.hardware.models": "Modelle",
|
||||||
|
"settings.hardware.downloading_model": "{name} wird heruntergeladen",
|
||||||
|
"settings.hardware.active_badge": "Aktiv",
|
||||||
|
"settings.hardware.installed": "Installiert",
|
||||||
|
"settings.hardware.use": "Verwenden",
|
||||||
|
"settings.hardware.remove": "Entfernen",
|
||||||
|
"settings.hardware.cant_remove_active": "Das aktive Modell kann nicht entfernt werden",
|
||||||
|
"settings.hardware.download": "Herunterladen",
|
||||||
|
"settings.hardware.diarization": "Sprechertrennung",
|
||||||
|
"settings.hardware.diarization_hint": "Installieren Sie beide Modelle, damit fertige Aufnahmen nach Sprecher getrennt werden (Sprecher 1, Sprecher 2, …) statt eines durchgehenden Transkripts. Läuft vollständig offline, einmal pro Meeting nach dem Stopp. Ohne sie wird jede Zeile einem einzigen Sprecher zugeordnet.",
|
||||||
|
|
||||||
|
"settings.storage.title": "Speicher",
|
||||||
|
"settings.storage.location_label": "Speicherort",
|
||||||
|
"settings.storage.location_hint": "Das Ändern des Speicherorts wird noch nicht unterstützt — hier befinden sich derzeit Meetings, Modelle und die Datenbank.",
|
||||||
|
"settings.storage.retention_title": "Aufbewahrung",
|
||||||
|
"settings.storage.retention_hint": "Löscht automatisch die ältesten Meetings über einem Limit. Wird einmal beim Start geprüft; leer bedeutet kein Limit. Betrifft nie ein Meeting, das gerade aufgenommen wird.",
|
||||||
|
"settings.storage.max_age": "Höchstalter (Tage)",
|
||||||
|
"settings.storage.max_size": "Maximale Größe (GB)",
|
||||||
|
"settings.storage.no_limit": "kein Limit",
|
||||||
|
"settings.storage.export_import_title": "Export & Import",
|
||||||
|
"settings.storage.export_hint_1": "Verschieben Sie Aufnahmen zwischen Computern als portable Bündelordner (Audio, Transkript, Notizen, Zusammenfassung und ein",
|
||||||
|
"settings.storage.export_hint_2": "Manifest). Exportieren Sie in einen beliebigen Ordner — ein synchronisiertes Laufwerk, einen USB-Stick oder den lokalen Einhängepunkt eines Sync-Ziels — und importieren Sie es dann auf dem anderen Rechner. Importierte Meetings erhalten eine neue ID, sodass ein erneuter Import nie etwas überschreibt.",
|
||||||
|
"settings.storage.export_dialog_title": "Alle Meetings exportieren nach…",
|
||||||
|
"settings.storage.import_dialog_title": "Ein Bündel (oder einen Ordner mit Bündeln) importieren…",
|
||||||
|
"settings.storage.exporting": "Wird exportiert…",
|
||||||
|
"settings.storage.export_all": "Alle Meetings exportieren…",
|
||||||
|
"settings.storage.importing": "Wird importiert…",
|
||||||
|
"settings.storage.import_all": "Meetings importieren…",
|
||||||
|
"settings.storage.exported_one": "{n} Meeting exportiert.",
|
||||||
|
"settings.storage.exported_many": "{n} Meetings exportiert.",
|
||||||
|
"settings.storage.imported_one": "{n} Meeting importiert.",
|
||||||
|
"settings.storage.imported_many": "{n} Meetings importiert.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "Kalender & Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "Importieren Sie Termine und Teilnehmer aus einem lokalen Outlook-",
|
||||||
|
"settings.calendar.intro_2": "Backup — schreibgeschützt, es wird nichts in die Datei zurückgeschrieben. Nichts verlässt dieses Gerät.",
|
||||||
|
"settings.calendar.pst_file": ".pst-Datei",
|
||||||
|
"settings.calendar.no_file": "Keine Datei ausgewählt",
|
||||||
|
"settings.calendar.browse": "Durchsuchen…",
|
||||||
|
"settings.calendar.password": "Passwort",
|
||||||
|
"settings.calendar.password_note": "selten benötigt",
|
||||||
|
"settings.calendar.optional": "optional",
|
||||||
|
"settings.calendar.import_range": "Importzeitraum",
|
||||||
|
"settings.calendar.range_30": "Letzte 30 Tage",
|
||||||
|
"settings.calendar.range_90": "Letzte 90 Tage",
|
||||||
|
"settings.calendar.range_180": "Letzte 6 Monate",
|
||||||
|
"settings.calendar.range_365": "Letztes Jahr",
|
||||||
|
"settings.calendar.range_all": "Gesamter Zeitraum",
|
||||||
|
"settings.calendar.range_hint": "Ein langlebiges Postfach kann Jahre an wiederkehrenden/Feiertagseinträgen enthalten — ein engerer Zeitraum beschränkt den importierten Kalender auf das tatsächlich Relevante. Gilt sowohl für diese Import-Schaltfläche als auch für den automatischen erneuten Import beim Start.",
|
||||||
|
"settings.calendar.importing": "Wird importiert…",
|
||||||
|
"settings.calendar.import": "Importieren",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} Termine",
|
||||||
|
"settings.calendar.auto_reimport": "Diese Datei beim Start automatisch erneut importieren",
|
||||||
|
"settings.calendar.auto_reimport_hint": "Läuft einmal beim Start, nicht per Timer — der erneute Import kann gefahrlos wiederholt werden (bestehende Termine werden abgeglichen und aktualisiert, nicht dupliziert).",
|
||||||
|
"settings.calendar.import_failed": "Import fehlgeschlagen: {error} — die Datei selbst bleibt unberührt; prüfen Sie den Pfad und versuchen Sie es erneut.",
|
||||||
|
"settings.calendar.cleanup_title": "Aufräumen",
|
||||||
|
"settings.calendar.cleanup_hint": "Termine, die bereits an ein aufgenommenes Meeting angehängt sind, werden immer behalten, unabhängig von der unten gewählten Option.",
|
||||||
|
"settings.calendar.cleanup_30": "Älter als 30 Tage",
|
||||||
|
"settings.calendar.cleanup_90": "Älter als 90 Tage",
|
||||||
|
"settings.calendar.cleanup_180": "Älter als 6 Monate",
|
||||||
|
"settings.calendar.cleanup_365": "Älter als 1 Jahr",
|
||||||
|
"settings.calendar.cleanup_all": "Alle löschen",
|
||||||
|
"settings.calendar.cleaning": "Wird aufgeräumt…",
|
||||||
|
"settings.calendar.cleanup_btn": "Kalender aufräumen",
|
||||||
|
"settings.calendar.cleanup_result": "{deleted}{extra} gelöscht.",
|
||||||
|
"settings.calendar.cleanup_kept": ", {n} behalten (mit einem Meeting verknüpft)",
|
||||||
|
"settings.calendar.cleanup_failed": "Aufräumen fehlgeschlagen: {error}",
|
||||||
|
"settings.calendar.imported_events": "Importierte Termine",
|
||||||
|
"settings.calendar.no_events": "Noch keine Termine importiert.",
|
||||||
|
"settings.calendar.search_title": "Titel durchsuchen",
|
||||||
|
"settings.calendar.search_placeholder": "Meetingname…",
|
||||||
|
"settings.calendar.date_label": "Datum",
|
||||||
|
"settings.calendar.events_count": "{shown} von {total} Terminen",
|
||||||
|
"settings.calendar.untitled": "(ohne Titel)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "Einstellungen",
|
||||||
|
"settings.close": "Schließen",
|
||||||
|
"settings.close_title": "Schließen (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "Vorschaumodus — das Sync-Backend ist noch nicht implementiert (Phase 9). Änderungen werden nur in der Oberfläche gehalten.",
|
||||||
|
"settings.sync.title": "Synchronisierung & Upload",
|
||||||
|
"settings.sync.enable_label": "Hochladen von Meeting-Artefakten zu konfigurierten Zielen aktivieren",
|
||||||
|
"settings.sync.enable_hint": "Standardmäßig aus. Es wird nichts hochgeladen, sofern dies nicht aktiviert und ein Ziel eingeschaltet ist. Selbst gehostete Ziele behalten Daten auf Ihrem eigenen Server; Drittanbieter-Clouds sind deutlich gekennzeichnet.",
|
||||||
|
"settings.sync.targets_title": "Ziele",
|
||||||
|
"settings.sync.no_targets": "Noch keine Ziele. Fügen Sie unten eines hinzu.",
|
||||||
|
"settings.sync.third_party": "Drittanbieter",
|
||||||
|
"settings.sync.your_server": "Ihr Server",
|
||||||
|
"settings.sync.edit": "Bearbeiten",
|
||||||
|
"settings.sync.remove": "Entfernen",
|
||||||
|
"settings.sync.edit_target": "Ziel bearbeiten",
|
||||||
|
"settings.sync.add_target": "Ein Ziel hinzufügen",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV deckt Nextcloud, ownCloud, Cloudreve, Seafile und Synology ab. (Seafile: SeafDAV serverseitig aktivieren.)",
|
||||||
|
"settings.sync.name": "Name",
|
||||||
|
"settings.sync.name_placeholder": "Heim-Nextcloud",
|
||||||
|
"settings.sync.provider": "Anbieter",
|
||||||
|
"settings.sync.server_url": "Server-URL",
|
||||||
|
"settings.sync.server_url_note_1": "Nur Ihre Server-URL — WhispAssist ergänzt",
|
||||||
|
"settings.sync.server_url_note_2": "automatisch.",
|
||||||
|
"settings.sync.remote_folder": "Remote-Ordner",
|
||||||
|
"settings.sync.username": "Benutzername",
|
||||||
|
"settings.sync.app_password": "App-Passwort",
|
||||||
|
"settings.sync.pw_keep": "leer lassen, um das aktuelle Passwort zu behalten",
|
||||||
|
"settings.sync.pw_store": "im Anmeldedatenspeicher des Betriebssystems gespeichert",
|
||||||
|
"settings.sync.upload_legend": "Upload",
|
||||||
|
"settings.sync.artifact_transcript": "Transkript",
|
||||||
|
"settings.sync.artifact_notes": "Notizen",
|
||||||
|
"settings.sync.artifact_summary": "Zusammenfassung",
|
||||||
|
"settings.sync.artifact_recording": "Aufnahme (.wav, falls beibehalten)",
|
||||||
|
"settings.sync.allow_plaintext": "Klartext-http für eine LAN-Adresse zulassen (nicht empfohlen)",
|
||||||
|
"settings.sync.encrypt_before": "Vor dem Upload verschlüsseln (Ziel speichert nur Chiffretext)",
|
||||||
|
"settings.sync.requires_vault": "— erfordert einen entsperrten Tresor",
|
||||||
|
"settings.sync.test_connection": "Verbindung testen",
|
||||||
|
"settings.sync.save_changes": "Änderungen speichern",
|
||||||
|
"settings.sync.add_target_btn": "Ziel hinzufügen",
|
||||||
|
"settings.sync.cancel": "Abbrechen",
|
||||||
|
"settings.sync.third_party_banner": "{kind} ist eine Drittanbieter-Cloud — durch das Hochladen werden Ihre Daten von Ihrem Gerät an {kind} gesendet.",
|
||||||
|
"settings.sync.link_account": "{kind}-Konto verknüpfen…",
|
||||||
|
"settings.sync.oauth_hint": "Öffnet eine OAuth-Anmeldung (Loopback-Weiterleitung); das Token wird im Anmeldedatenspeicher des Betriebssystems gespeichert.",
|
||||||
|
"settings.sync.footnote": "Anmeldedaten werden nie in die Einstellungen oder die Datenbank geschrieben — nur in den Anmeldedatenspeicher des Betriebssystems. Für Nicht-LAN-Ziele ist TLS erforderlich.",
|
||||||
|
|
||||||
|
"settings.ai.title": "KI-Zusammenfassungsanbieter",
|
||||||
|
"settings.ai.intro_1": "Zusammenfassungen laufen standardmäßig auf einem lokalen LLM. Richten Sie dies auf Ollama auf diesem PC oder einem anderen Rechner in Ihrem LAN (z. B.",
|
||||||
|
"settings.ai.intro_2": "— beide gelten als lokal, sodass nichts Ihr Netzwerk verlässt. Gehostete Anbieter (Anthropic) sind optional, standardmäßig aus und senden das Transkript an einen Dritten, sobald Sie einen aktivieren.",
|
||||||
|
"settings.ai.provider": "Anbieter",
|
||||||
|
"settings.ai.off": "Aus",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (lokal / LAN)",
|
||||||
|
"settings.ai.provider_custom": "Benutzerdefiniert (OpenAI-kompatibel)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — gehostet, verlässt dieses Gerät",
|
||||||
|
"settings.ai.endpoint": "Endpunkt",
|
||||||
|
"settings.ai.model": "Modell",
|
||||||
|
"settings.ai.api_key": "API-Schlüssel",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (bereits gesetzt — leer lassen, um ihn zu behalten)",
|
||||||
|
"settings.ai.show_api_key": "API-Schlüssel anzeigen",
|
||||||
|
"settings.ai.hide_api_key": "API-Schlüssel ausblenden",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic ist ein gehosteter Drittanbieterdienst — das Transkript dieses Meetings verlässt Ihr Gerät, wenn Sie eine Zusammenfassung erstellen.",
|
||||||
|
"settings.ai.endpoint_banner": "Dieser Endpunkt befindet sich nicht auf Ihrem Rechner oder LAN — Ihr Transkript würde Ihr Netzwerk verlassen.",
|
||||||
|
"settings.ai.saving": "Wird gespeichert…",
|
||||||
|
"settings.ai.save": "Speichern",
|
||||||
|
"settings.ai.test_connection": "Verbindung testen",
|
||||||
|
"settings.ai.this_hosted_provider": "dieser gehostete Anbieter",
|
||||||
|
"settings.ai.reachable": "Erreichbar",
|
||||||
|
"settings.ai.unreachable": "Nicht erreichbar",
|
||||||
|
"settings.ai.model_count_one": "· {n} Modell",
|
||||||
|
"settings.ai.model_count_many": "· {n} Modelle",
|
||||||
|
"settings.ai.on_lan": "· auf Ihrem Rechner / LAN",
|
||||||
|
"settings.ai.leaves_network": "· verlässt Ihr Netzwerk",
|
||||||
|
"settings.ai.advanced_title": "Erweiterte Ollama-Konfiguration",
|
||||||
|
"settings.ai.changed": "{n} geändert",
|
||||||
|
"settings.ai.reset_all": "Alle auf Standard zurücksetzen",
|
||||||
|
"settings.ai.system_prompt": "System-Prompt",
|
||||||
|
"settings.ai.system_prompt_placeholder": "z. B. Du bist ein prägnanter Meeting-Zusammenfasser.",
|
||||||
|
"settings.ai.system_prompt_hint": "Wird vor WhispAssists erforderlichem Ausgabeformat hinzugefügt, sodass Ihre Anweisungen das Parsen von Zusammenfassung/Aktionspunkten nicht stören können.",
|
||||||
|
"settings.ai.think": "Nachdenken",
|
||||||
|
"settings.ai.think_help": "Denkaufwand (nur bei Reasoning-Modellen).",
|
||||||
|
"settings.ai.keep_alive": "Aktiv halten",
|
||||||
|
"settings.ai.keep_alive_help": "Wie lange das Modell im RAM bleibt. z. B. 5m, 1h, 0 (entladen), -1 (dauerhaft).",
|
||||||
|
"settings.ai.runtime_hardware": "Laufzeit & Hardware",
|
||||||
|
"settings.ai.rarely_needed": "— selten benötigt",
|
||||||
|
"settings.ai.save_advanced": "Erweitert speichern",
|
||||||
|
"settings.ai.saved": "Gespeichert",
|
||||||
|
"settings.ai.turn_off": "Ausschalten",
|
||||||
|
"settings.ai.default_value": "Standard {value}.",
|
||||||
|
"settings.ai.reset_default": "Auf Standard zurücksetzen",
|
||||||
|
"settings.ai.reset_field": "{name} zurücksetzen",
|
||||||
|
|
||||||
|
"settings.mcp.title": "MCP-Server",
|
||||||
|
"settings.mcp.banner_1": "Dies lässt Ihren eigenen Coding-Agenten (Claude Code, Codex, Copilot, OpenCode, …) Meeting-Kontext auf Ihrem lokalen Rechner abrufen. Nach der Verbindung",
|
||||||
|
"settings.mcp.banner_agent": "kann dieser Agent",
|
||||||
|
"settings.mcp.banner_2": "das Gelesene an die Cloud seines eigenen Modellanbieters weiterleiten — außerhalb der Kontrolle von WhispAssist. WhispAssist selbst sendet diese Daten nirgendwohin; der Server lauscht nur auf diesem Gerät (",
|
||||||
|
"settings.mcp.banner_3": ") und jeder Lesevorgang wird unten protokolliert.",
|
||||||
|
"settings.mcp.enable": "Den MCP-Server aktivieren",
|
||||||
|
"settings.mcp.enable_hint": "Standardmäßig aus. Nur Loopback, token-gesichert — vom Netzwerk aus ist nichts erreichbar.",
|
||||||
|
"settings.mcp.transport": "Transport",
|
||||||
|
"settings.mcp.transport_http": "Streamable HTTP",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (Agent startet einen Prozess)",
|
||||||
|
"settings.mcp.port": "Port",
|
||||||
|
"settings.mcp.transport_change_hint": "Um Transport/Port zu ändern, schalten Sie den Server zuerst aus und dann wieder ein.",
|
||||||
|
"settings.mcp.scope_title": "Umfang",
|
||||||
|
"settings.mcp.expose": "Freigeben",
|
||||||
|
"settings.mcp.scope_none": "Keiner — nichts wird geteilt",
|
||||||
|
"settings.mcp.scope_selected": "Ausgewählt — nur Feature-Briefs, die Sie als geteilt markiert haben",
|
||||||
|
"settings.mcp.scope_all": "Alle — Meetings, Transkripte, Aktionspunkte und geteilte Briefs",
|
||||||
|
"settings.mcp.expose_recordings": "Auch Meetings mit gespeicherter Aufnahme zulassen (standardmäßig aus — das Transkript eines aufgenommenen Meetings wird selbst im „Alle“-Umfang zurückgehalten, bis dies aktiviert ist)",
|
||||||
|
"settings.mcp.token_title": "Neues Auth-Token — einmalig angezeigt, kopieren Sie es jetzt",
|
||||||
|
"settings.mcp.token_hint": "Dies wird nicht erneut angezeigt. Es wird im Anmeldedatenspeicher des Betriebssystems gespeichert; falls Sie es verlieren, schalten Sie den Server aus und wieder ein, um ein neues zu erzeugen.",
|
||||||
|
"settings.mcp.copied": "Kopiert",
|
||||||
|
"settings.mcp.copy": "Kopieren",
|
||||||
|
"settings.mcp.endpoint": "Endpunkt",
|
||||||
|
"settings.mcp.endpoint_hint": "Richten Sie die MCP-Client-Konfiguration Ihres Agenten auf {kind} aus, mit dem obigen Token als Bearer-Anmeldedatum.",
|
||||||
|
"settings.mcp.kind_command": "Befehl",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "Zugriffsprotokoll",
|
||||||
|
"settings.mcp.access_log_hint": "Jeder Tool-Lesevorgang eines Agenten, erlaubt oder verweigert (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "Noch hat kein Agent etwas gelesen.",
|
||||||
|
"settings.mcp.log_meeting": "Meeting {id}",
|
||||||
|
"settings.mcp.refresh": "Aktualisieren",
|
||||||
|
|
||||||
|
"settings.privacy.title": "Datenschutz",
|
||||||
|
"settings.privacy.intro": "Was WhispAssist derzeit tatsächlich von diesem Gerät senden darf. Wenn alles aus ist (Standard), verlässt nichts das Gerät.",
|
||||||
|
"settings.privacy.llm_endpoint": "LLM-Endpunkt",
|
||||||
|
"settings.privacy.off": "aus",
|
||||||
|
"settings.privacy.local_only": "nur lokal",
|
||||||
|
"settings.privacy.leaves_device": "verlässt dieses Gerät",
|
||||||
|
"settings.privacy.sync_label": "Synchronisierung",
|
||||||
|
"settings.privacy.enabled": "aktiviert",
|
||||||
|
"settings.privacy.mcp_label": "MCP-Server",
|
||||||
|
"settings.privacy.mcp_on": "an · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "Eingehend nur auf Loopback — es fügt der obigen Egress-Liste nichts hinzu. Ein verbundener Agent kann das Gelesene dennoch an seinen eigenen Modellanbieter weiterleiten; siehe den MCP-Server-Tab.",
|
||||||
|
"settings.privacy.egress_title": "Egress-Zulassungsliste",
|
||||||
|
"settings.privacy.no_egress": "Keine Hosts auf der Zulassungsliste — WA macht keinen Inhalts-Egress.",
|
||||||
|
"settings.privacy.sync_targets_title": "Sync-Ziele",
|
||||||
|
"settings.privacy.third_party": "Drittanbieter",
|
||||||
|
"settings.privacy.your_server": "Ihr Server",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "kein TLS",
|
||||||
|
"settings.privacy.refresh": "Aktualisieren",
|
||||||
|
"settings.privacy.unavailable": "Datenschutz-Selbstprüfung nicht verfügbar.",
|
||||||
|
"settings.privacy.vault_title": "Verschlüsselungstresor",
|
||||||
|
"settings.privacy.vault_intro": "Verschlüsseln Sie Notizen, Transkripte und Zusammenfassungen im Ruhezustand mit einem Passwort. (Audiodateien sind noch nicht verschlüsselt.)",
|
||||||
|
"settings.privacy.vault_password": "Tresor-Passwort",
|
||||||
|
"settings.privacy.enable_vault": "Tresor aktivieren",
|
||||||
|
"settings.privacy.vault_pw_hint": "Verwenden Sie mindestens 8 Zeichen. Wenn Sie es vergessen, können verschlüsselte Inhalte nicht wiederhergestellt werden.",
|
||||||
|
"settings.privacy.vault_locked_1": "Der Tresor ist ",
|
||||||
|
"settings.privacy.locked_word": "gesperrt",
|
||||||
|
"settings.privacy.vault_locked_2": ". Entsperren Sie, um verschlüsselte Meetings zu lesen.",
|
||||||
|
"settings.privacy.password": "Passwort",
|
||||||
|
"settings.privacy.unlock": "Entsperren",
|
||||||
|
"settings.privacy.vault_unlocked_1": "Der Tresor ist ",
|
||||||
|
"settings.privacy.unlocked_word": "entsperrt",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". Neue Notizen, Transkripte und Zusammenfassungen werden im Ruhezustand verschlüsselt.",
|
||||||
|
"settings.privacy.lock_now": "Jetzt sperren",
|
||||||
|
"settings.privacy.change_password": "Passwort ändern",
|
||||||
|
"settings.privacy.current_password": "Aktuelles Passwort",
|
||||||
|
"settings.privacy.new_password": "Neues Passwort",
|
||||||
|
|
||||||
|
"settings.about.title": "Über",
|
||||||
|
"settings.about.tagline": "Ein vollständig lokaler, quelloffener, Windows-nativer Meeting-Assistent.",
|
||||||
|
"settings.about.build_commit": "Build-Commit",
|
||||||
|
|
||||||
|
"consent.heading": "Bevor Sie aufnehmen",
|
||||||
|
"consent.body": "Das Aufzeichnen von Gesprächen ohne Einwilligung der Teilnehmer kann in Ihrer Region illegal sein. Prüfen Sie Ihre lokalen Aufnahmegesetze. Dies ist ein Hinweis, keine Rechtsberatung.",
|
||||||
|
"consent.accept": "Ich verstehe — Aufnahme aktivieren",
|
||||||
|
"consent.cancel": "Abbrechen",
|
||||||
|
|
||||||
|
"hosted.heading": "Bevor Sie {provider} verwenden",
|
||||||
|
"hosted.body_1": "Das Generieren mit",
|
||||||
|
"hosted.body_2": "sendet das Transkript dieses Meetings an deren Server — es verlässt dieses Gerät und unterliegt deren Datenschutzrichtlinie. WhispAssist hat keine Kontrolle über die Datenverarbeitung, sobald sie Ihr Gerät verlassen. Dies ist standardmäßig aus; Sie wählen es jetzt.",
|
||||||
|
"hosted.accept": "Ich verstehe — fortfahren",
|
||||||
|
"hosted.cancel": "Abbrechen",
|
||||||
|
|
||||||
|
"theme.group": "Design",
|
||||||
|
"theme.system": "System anpassen",
|
||||||
|
"theme.light": "Hell",
|
||||||
|
"theme.dark": "Dunkel",
|
||||||
|
|
||||||
|
"import.dialog_label": "Ein Meeting aus einer Datei oder URL hinzufügen",
|
||||||
|
"import.title": "Ein Meeting hinzufügen",
|
||||||
|
"import.close": "Schließen",
|
||||||
|
"import.close_title": "Schließen (Esc)",
|
||||||
|
"import.body": "Importieren Sie eine vorhandene Aufnahme — eine lokale Audio-/Videodatei oder einen Link (YouTube, eine Streaming-Seite oder eine direkte Medien-URL). Sie wird wie eine Live-Aufnahme transkribiert und nach Sprechern getrennt.",
|
||||||
|
"import.file_or_url": "Datei oder URL",
|
||||||
|
"import.source_placeholder": "Fügen Sie eine URL ein oder suchen Sie nach einer Datei…",
|
||||||
|
"import.choose_file": "Eine lokale Datei wählen",
|
||||||
|
"import.browse": "Durchsuchen…",
|
||||||
|
"import.title_label": "Titel",
|
||||||
|
"import.optional": "optional",
|
||||||
|
"import.title_placeholder": "Standardmäßig der Dateiname",
|
||||||
|
"import.filter_av": "Audio / Video",
|
||||||
|
"import.requires_1": "Erfordert",
|
||||||
|
"import.requires_2": "installiert und in Ihrem PATH (plus",
|
||||||
|
"import.requires_3": "für URLs). WhispAssist liefert sie nicht mit.",
|
||||||
|
"import.importing": "Wird importiert… das kann eine Weile dauern",
|
||||||
|
"import.import": "Importieren",
|
||||||
|
"import.cancel": "Abbrechen",
|
||||||
|
|
||||||
|
"tagchip.filter": "Meetings mit dem Tag „{tag}“ filtern",
|
||||||
|
"tagchip.remove": "Tag {tag} entfernen",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "System- und Mikrofoneingangspegel",
|
||||||
|
"levelmeter.system": "Systemeingangspegel",
|
||||||
|
|
||||||
|
"app.tagline": "lokal · privat",
|
||||||
|
"app.note_template": "Notizvorlage",
|
||||||
|
"app.no_template": "Keine Vorlage",
|
||||||
|
"app.add_meeting": "Meeting hinzufügen",
|
||||||
|
"app.add_meeting_title": "Ein Meeting aus einer Datei oder URL hinzufügen",
|
||||||
|
"app.record": "Aufnehmen",
|
||||||
|
"app.record_title": "Aufnahme starten (Strg+Umschalt+R)",
|
||||||
|
"app.stop": "Stopp",
|
||||||
|
"app.stop_title": "Aufnahme stoppen (Strg+Umschalt+R)",
|
||||||
|
"app.cancel": "Abbrechen",
|
||||||
|
"app.cancel_title": "Diese Aufnahme verwerfen und löschen",
|
||||||
|
"app.recording": "Aufnahme läuft…",
|
||||||
|
"app.backend_title": "Aktives Transkriptions-Backend",
|
||||||
|
"app.retention_title": "Audio als .wav für dieses Meeting speichern",
|
||||||
|
"app.saving": "wird gespeichert",
|
||||||
|
"app.not_saved": "nicht gespeichert",
|
||||||
|
"app.reconnecting": "Audiogerät wird neu verbunden…",
|
||||||
|
"app.settings": "Einstellungen",
|
||||||
|
"app.settings_title": "Einstellungen (Strg+,)",
|
||||||
|
"app.discard_confirm": "Diese Aufnahme verwerfen? Ihr Audio und Transkript werden gelöscht.",
|
||||||
|
"app.announce_started": "Aufnahme gestartet",
|
||||||
|
"app.announce_paused": "Aufnahme pausiert",
|
||||||
|
"app.announce_stopped": "Aufnahme gestoppt",
|
||||||
|
"app.consent_dialog": "Aufnahme-Einwilligung",
|
||||||
|
"app.vault_dialog": "Verschlüsselungstresor entsperren",
|
||||||
|
"app.vault_title": "Verschlüsselungstresor entsperren",
|
||||||
|
"app.vault_desc": "Ihre Meetings sind im Ruhezustand verschlüsselt. Geben Sie Ihr Passwort ein, um sie zu lesen.",
|
||||||
|
"app.vault_password": "Tresor-Passwort",
|
||||||
|
"app.vault_incorrect": "Falsches Passwort",
|
||||||
|
"app.unlock": "Entsperren",
|
||||||
|
"app.continue_locked": "Gesperrt fortfahren",
|
||||||
|
"app.show_meetings": "Meetingliste anzeigen",
|
||||||
|
"app.hide_meetings": "Meetingliste ausblenden",
|
||||||
|
"app.show_summary": "Zusammenfassungsbereich anzeigen",
|
||||||
|
"app.hide_summary": "Zusammenfassungsbereich ausblenden",
|
||||||
|
"app.resize_meetings": "Meetingliste anpassen",
|
||||||
|
"app.resize_summary": "Zusammenfassungsbereich anpassen",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "Meetings durchsuchen…",
|
||||||
|
"meetings.search_aria": "Meetings durchsuchen",
|
||||||
|
"meetings.filter_tag_aria": "Nach Tag filtern",
|
||||||
|
"meetings.all_tags": "Alle Tags",
|
||||||
|
"meetings.from_date_aria": "Von Datum",
|
||||||
|
"meetings.to_date_aria": "Bis Datum",
|
||||||
|
"meetings.bulk_format_aria": "Sammelexportformat",
|
||||||
|
"meetings.bulk_export": "Sammelexport",
|
||||||
|
"meetings.bulk_export_title": "Jedes Meeting exportieren, das den obigen Tag-/Datumsfiltern entspricht",
|
||||||
|
"meetings.exporting": "Wird exportiert…",
|
||||||
|
"meetings.exported_one": "{n} Meeting exportiert.",
|
||||||
|
"meetings.exported_many": "{n} Meetings exportiert.",
|
||||||
|
"meetings.searching": "Wird gesucht…",
|
||||||
|
"meetings.loading": "Meetings werden geladen…",
|
||||||
|
"meetings.no_matches": "Keine Treffer.",
|
||||||
|
"meetings.empty": "Noch keine Meetings — klicken Sie oben auf Aufnehmen, um zu beginnen.",
|
||||||
|
"meetings.status.recording": "Aufnahme",
|
||||||
|
"meetings.status.transcribing": "wird transkribiert",
|
||||||
|
"meetings.status.recovering": "Wiederherstellung",
|
||||||
|
"meetings.status.error": "Fehler",
|
||||||
|
"meetings.resume": "Transkription fortsetzen",
|
||||||
|
"meetings.delete_aria": "Meeting löschen",
|
||||||
|
"meetings.delete_confirm": "Dieses Meeting löschen? Dadurch werden seine Aufnahme, sein Transkript und seine Notizen entfernt.",
|
||||||
|
|
||||||
|
"transcript.heading": "Transkript",
|
||||||
|
"transcript.title_aria": "Meetingtitel",
|
||||||
|
"transcript.lang_title": "Transkriptionssprache",
|
||||||
|
"transcript.lang_auto": "wird erkannt…",
|
||||||
|
"transcript.show": "Transkript anzeigen",
|
||||||
|
"transcript.hide": "Transkript ausblenden",
|
||||||
|
"transcript.empty": "Kein Transkript für dieses Meeting.",
|
||||||
|
"transcript.will_appear": "Das Transkript erscheint hier, während Sie aufnehmen.",
|
||||||
|
"transcript.play_from_here": "Ab hier abspielen",
|
||||||
|
"transcript.resize": "Transkript und Notizen anpassen",
|
||||||
|
"transcript.reprocess_placeholder": "Neu transkribieren mit…",
|
||||||
|
"transcript.reprocess_lang_aria": "Sprache für Neuverarbeitung",
|
||||||
|
"transcript.reprocess_keep_lang": "Aktuelle Sprache beibehalten",
|
||||||
|
"transcript.reprocess_go": "Los",
|
||||||
|
"transcript.retranscribing": "Wird neu transkribiert…",
|
||||||
|
"transcript.has_note": "Hat eine Notiz",
|
||||||
|
"transcript.note_placeholder": "Eine Notiz für diesen Moment hinzufügen…",
|
||||||
|
"transcript.note_aria": "Notiz für diese Transkriptzeile",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Word-Dokument",
|
||||||
|
"transcript.filter_obsidian": "Obsidian-Notiz",
|
||||||
|
|
||||||
|
"notes.heading": "Notizen",
|
||||||
|
"notes.show": "Notizen anzeigen",
|
||||||
|
"notes.hide": "Notizen ausblenden",
|
||||||
|
"notes.placeholder": "Notizen…",
|
||||||
|
"notes.live_placeholder": "Tippen Sie Notizen, während Sie sprechen…",
|
||||||
|
"notes.toolbar_aria": "Notizformatierung",
|
||||||
|
"notes.bold": "Fett",
|
||||||
|
"notes.italic": "Kursiv",
|
||||||
|
"notes.h1": "Überschrift 1",
|
||||||
|
"notes.h2": "Überschrift 2",
|
||||||
|
"notes.bullet": "Aufzählungsliste",
|
||||||
|
"notes.checkbox_title": "Kontrollkästchen",
|
||||||
|
"notes.checkbox_aria": "Kontrollkästchen-Listenelement",
|
||||||
|
"notes.edit_raw": "Rohes Markdown bearbeiten",
|
||||||
|
"notes.render": "Markdown rendern",
|
||||||
|
"notes.editor": "Editor",
|
||||||
|
"notes.preview": "Vorschau",
|
||||||
|
"notes.export_md_title": "Notizen als .md exportieren",
|
||||||
|
"notes.export_pdf_title": "Notizen als .pdf exportieren",
|
||||||
|
"notes.export_docx_title": "Notizen als .docx exportieren",
|
||||||
|
"notes.export_bundle_title": "Audio + Transkript + Notizen in einen Ordner exportieren",
|
||||||
|
"notes.export_obsidian_title": "Eine Obsidian-Notiz exportieren (Notizen + Zusammenfassung + Transkript, kein Audio)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "Aufnahme",
|
||||||
|
"summary.loading_recording": "Aufnahme wird geladen…",
|
||||||
|
"summary.sync_heading": "Synchronisierung",
|
||||||
|
"summary.uploading": "Wird hochgeladen…",
|
||||||
|
"summary.upload_now": "Jetzt hochladen",
|
||||||
|
"summary.no_uploads": "Noch keine Uploads für dieses Meeting.",
|
||||||
|
"summary.retry": "erneut versuchen",
|
||||||
|
"summary.tags_heading": "Tags",
|
||||||
|
"summary.tags_select": "Wählen Sie ein Meeting, um es zu taggen.",
|
||||||
|
"summary.tag_add_placeholder": "Tag hinzufügen…",
|
||||||
|
"summary.tag_first_placeholder": "Projekt, Kunde, Thema…",
|
||||||
|
"summary.generating": "Wird generiert…",
|
||||||
|
"summary.generate_tags": "Tags generieren",
|
||||||
|
"summary.saving": "Wird gespeichert…",
|
||||||
|
"summary.save_tags": "Tags speichern",
|
||||||
|
"summary.summary_heading": "Zusammenfassung",
|
||||||
|
"summary.ai_provider": "KI-Anbieter",
|
||||||
|
"summary.provider_off": "Aus",
|
||||||
|
"summary.provider_custom": "Benutzerdefiniert",
|
||||||
|
"summary.local": "lokal",
|
||||||
|
"summary.leaves_device": "verlässt dieses Gerät",
|
||||||
|
"summary.this_hosted_provider": "dieser gehostete Anbieter",
|
||||||
|
"summary.select_generate": "Wählen Sie ein Meeting, um eine Zusammenfassung zu erstellen.",
|
||||||
|
"summary.decisions_heading": "Entscheidungen",
|
||||||
|
"summary.regenerate": "Neu generieren",
|
||||||
|
"summary.generated_hint": "Lokal nach dem Meeting generiert (erfordert einen lokalen LLM-Anbieter).",
|
||||||
|
"summary.no_provider": "Kein KI-Anbieter konfiguriert — aktivieren Sie einen in den Einstellungen.",
|
||||||
|
"summary.generate_summary": "Zusammenfassung erstellen",
|
||||||
|
"summary.briefs_heading": "Feature-Briefs",
|
||||||
|
"summary.briefs_select": "Wählen Sie ein Meeting, um Feature-Briefs zu erstellen oder anzuzeigen.",
|
||||||
|
"summary.briefs_desc": "Destilliert dieses Meeting zu einer agentenbereiten Spezifikation (erfordert einen lokalen LLM-Anbieter) — übergeben Sie sie einem Coding-Agenten oder stellen Sie sie über den MCP-Server bereit, sobald dieser an ist.",
|
||||||
|
"summary.brief_repo_placeholder": "Ziel-Repo (optional), z. B. acme/reporting-web",
|
||||||
|
"summary.distilling": "Wird destilliert…",
|
||||||
|
"summary.create_brief": "Feature-Brief erstellen",
|
||||||
|
"summary.brief_mcp_title": "Verfügbar, wenn der MCP-Server an ist",
|
||||||
|
"summary.copied": "Kopiert",
|
||||||
|
"summary.copy_md": "Als Markdown kopieren",
|
||||||
|
"summary.copy_json": "Als JSON kopieren",
|
||||||
|
"summary.brief_problem": "Problem",
|
||||||
|
"summary.brief_outcome": "Gewünschtes Ergebnis",
|
||||||
|
"summary.brief_criteria": "Akzeptanzkriterien",
|
||||||
|
"summary.brief_none": "Nichts erfasst.",
|
||||||
|
"summary.brief_context": "Kontext",
|
||||||
|
"summary.action_items_heading": "Aktionspunkte",
|
||||||
|
"summary.ai_select": "Wählen Sie ein Meeting, um seine Aktionspunkte zu sehen.",
|
||||||
|
"summary.ai_empty": "Noch keine — fügen Sie unten einen hinzu oder erstellen Sie eine Zusammenfassung, um sie automatisch zu extrahieren.",
|
||||||
|
"summary.confirmed": "Bestätigt",
|
||||||
|
"summary.ai_text_placeholder": "Aktionspunkt…",
|
||||||
|
"summary.ai_text_aria": "Aktionspunkttext",
|
||||||
|
"summary.owner": "Verantwortlich",
|
||||||
|
"summary.due_date": "Fälligkeitsdatum",
|
||||||
|
"summary.reminder_title": "Eine lokale Erinnerung für dieses Fälligkeitsdatum planen",
|
||||||
|
"summary.ai_delete_title": "Diesen Aktionspunkt löschen",
|
||||||
|
"summary.ai_delete_aria": "Aktionspunkt löschen",
|
||||||
|
"summary.add_action_item": "Aktionspunkt hinzufügen",
|
||||||
|
"summary.save_action_items": "Aktionspunkte speichern",
|
||||||
|
"summary.calendar_heading": "Kalendertermin",
|
||||||
|
"summary.cal_select": "Wählen Sie ein Meeting, um es mit einem Kalendertermin zu verknüpfen.",
|
||||||
|
"summary.event_search_placeholder": "Termintitel durchsuchen…",
|
||||||
|
"summary.linked_event": "Verknüpfter Termin",
|
||||||
|
"summary.change_event": "Termin ändern…",
|
||||||
|
"summary.link_event": "Einen Termin verknüpfen…",
|
||||||
|
"summary.untitled_event": "(ohne Titel)",
|
||||||
|
"summary.no_events_1": "Noch keine Termine importiert — importieren Sie einen",
|
||||||
|
"summary.no_events_2": "unter Einstellungen → Kalender.",
|
||||||
|
"summary.no_events_match": "Keine Termine passen zu dieser Suche/diesem Datum — versuchen Sie, eines zu löschen.",
|
||||||
|
"summary.organizer_label": "Organisator: {name}",
|
||||||
|
"summary.participants_heading": "Teilnehmer",
|
||||||
|
"summary.participants_hint": "Aus dem verknüpften Kalendertermin übernommen.",
|
||||||
|
"summary.speakers_heading": "Sprecher",
|
||||||
|
"summary.speakers_select": "Wählen Sie ein Meeting, um seine Sprecher zu benennen.",
|
||||||
|
"summary.no_speakers": "Noch keine Sprecher erkannt.",
|
||||||
|
"summary.speaker_name_placeholder": "Sprechername",
|
||||||
|
"summary.save": "Speichern",
|
||||||
|
"summary.cancel": "Abbrechen",
|
||||||
|
"summary.name_speaker": "Diesen Sprecher benennen…",
|
||||||
|
"summary.add_new_name": "+ Neuen Namen hinzufügen…",
|
||||||
|
|
||||||
|
"settings.recording.title": "Aufnahme",
|
||||||
|
"settings.recording.record_default": "Meetings standardmäßig aufnehmen",
|
||||||
|
"settings.recording.save_audio_as": "Audio speichern als",
|
||||||
|
"settings.recording.default_hint": "Standardmäßig aus. Wenn aus, wird Audio nur zur Erstellung des Transkripts verwendet und beim Finalisieren des Meetings gelöscht. Sie können die Aufnahme auch pro Meeting umschalten.",
|
||||||
|
"settings.recording.consent_label": "Einwilligung: {status}.",
|
||||||
|
"settings.recording.consent_ack": "bestätigt",
|
||||||
|
"settings.recording.consent_not": "noch nicht bestätigt",
|
||||||
|
"settings.recording.auto_label": "Aufnahme automatisch starten, wenn ein Kalendertermin beginnt",
|
||||||
|
"settings.recording.auto_hint": "Nur während WhispAssist geöffnet ist. Wenn die Startzeit eines importierten Kalendertermins erreicht ist, beginnt automatisch eine Aufnahme (mit Ihrer obigen Standard-Aufbewahrungseinstellung). Nichts läuft im Hintergrund — der Timer ist nur aktiv, während die App läuft. Importieren Sie Termine unter Einstellungen → Kalender."
|
||||||
|
}
|
||||||
@@ -0,0 +1,580 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "Recording",
|
||||||
|
"nav.hardware": "Hardware",
|
||||||
|
"nav.storage": "Storage",
|
||||||
|
"nav.calendar": "Calendar",
|
||||||
|
"nav.sync": "Sync",
|
||||||
|
"nav.ai": "AI",
|
||||||
|
"nav.mcp": "MCP server",
|
||||||
|
"nav.privacy": "Privacy",
|
||||||
|
"nav.about": "About",
|
||||||
|
"nav.language": "Language",
|
||||||
|
|
||||||
|
"settings.language.title": "Language",
|
||||||
|
"settings.language.display": "Display language",
|
||||||
|
"settings.language.display_hint": "The language of the app interface. Adding a language is as simple as dropping in one translation file.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "Transcription Language",
|
||||||
|
"settings.transcription.label": "Language",
|
||||||
|
"settings.transcription.auto": "Auto-detect",
|
||||||
|
"settings.transcription.applies_hint": "Applies to the next recording. The language actually used is shown on each meeting afterward.",
|
||||||
|
"settings.transcription.english_only": "English only",
|
||||||
|
"settings.transcription.model_english_only": "\"{model}\" is English-only.",
|
||||||
|
"settings.transcription.no_model": "No model selected.",
|
||||||
|
"settings.transcription.switch_multilingual": "Switch to a multilingual model above to choose a language.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "Hardware",
|
||||||
|
"settings.hardware.refresh": "Refresh",
|
||||||
|
"settings.hardware.test_output": "Test",
|
||||||
|
"settings.hardware.test_mic": "Test",
|
||||||
|
"settings.hardware.testing": "Listening…",
|
||||||
|
"settings.hardware.play_tone": "Play tone",
|
||||||
|
"settings.hardware.stress_title": "Quick stress test",
|
||||||
|
"settings.hardware.stress_hint": "Benchmarks your installed models on each available backend and recommends the most accurate one that still keeps up with live speech. Takes a moment.",
|
||||||
|
"settings.hardware.stress_run": "Run stress test",
|
||||||
|
"settings.hardware.stress_running": "Running…",
|
||||||
|
"settings.hardware.stress_progress": "Benchmarking {pair}…",
|
||||||
|
"settings.hardware.stress_recommend": "Recommended: {backend} + {model}",
|
||||||
|
"settings.hardware.stress_apply": "Apply",
|
||||||
|
"settings.hardware.stress_none": "No installed model keeps up with live speech on this hardware — try a smaller model.",
|
||||||
|
"settings.hardware.stress_backend": "Backend",
|
||||||
|
"settings.hardware.stress_model": "Model",
|
||||||
|
"settings.hardware.stress_rtf": "Speed (×real-time)",
|
||||||
|
"settings.hardware.stress_realtime": "Live?",
|
||||||
|
"settings.hardware.stress_yes": "Yes",
|
||||||
|
"settings.hardware.stress_no": "No",
|
||||||
|
"settings.hardware.active_backend": "Active backend",
|
||||||
|
"settings.hardware.model_meta": "· model {size}",
|
||||||
|
"settings.hardware.preferred_backend": "Preferred backend",
|
||||||
|
"settings.hardware.auto_backend": "Auto (best available)",
|
||||||
|
"settings.hardware.not_available": "not available",
|
||||||
|
"settings.hardware.fallback_hint": "Falls back automatically (NPU → NVIDIA → AMD → Intel → CPU) if the chosen backend fails to load.",
|
||||||
|
"settings.hardware.audio_devices": "Audio Devices",
|
||||||
|
"settings.hardware.recording_device": "Recording device",
|
||||||
|
"settings.hardware.default_system_audio": "Default system audio",
|
||||||
|
"settings.hardware.recording_device_hint": "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.",
|
||||||
|
"settings.hardware.microphone": "Microphone",
|
||||||
|
"settings.hardware.mic_off": "Off — don't capture my microphone",
|
||||||
|
"settings.hardware.default_mic": "Default microphone",
|
||||||
|
"settings.hardware.mic_hint": "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.",
|
||||||
|
"settings.hardware.npu_detected": "NPU accelerator detected",
|
||||||
|
"settings.hardware.ready": "Ready",
|
||||||
|
"settings.hardware.downloading": "Downloading…",
|
||||||
|
"settings.hardware.package_needed": "Package needed",
|
||||||
|
"settings.hardware.npu_hint": "Your NPU needs a one-time acceleration package (Whisper ONNX model{extra}). It downloads in the background; you can also start it now.",
|
||||||
|
"settings.hardware.plus_openvino": " + OpenVINO runtime",
|
||||||
|
"settings.hardware.download_npu": "Download NPU package",
|
||||||
|
"settings.hardware.dml_title": "GPU acceleration (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "Your GPU can be accelerated without Vulkan via DirectML — a one-time package (Whisper ONNX model{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + DirectML runtime",
|
||||||
|
"settings.hardware.download_dml": "Download DirectML package",
|
||||||
|
"settings.hardware.detection_unavailable": "Hardware detection unavailable.",
|
||||||
|
"settings.hardware.low_overhead": "Low overhead preset",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + smallest model, for battery/background use",
|
||||||
|
"settings.hardware.models": "Models",
|
||||||
|
"settings.hardware.downloading_model": "Downloading {name}",
|
||||||
|
"settings.hardware.active_badge": "Active",
|
||||||
|
"settings.hardware.installed": "Installed",
|
||||||
|
"settings.hardware.use": "Use",
|
||||||
|
"settings.hardware.remove": "Remove",
|
||||||
|
"settings.hardware.cant_remove_active": "Can't remove the active model",
|
||||||
|
"settings.hardware.download": "Download",
|
||||||
|
"settings.hardware.diarization": "Speaker diarization",
|
||||||
|
"settings.hardware.diarization_hint": "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.",
|
||||||
|
|
||||||
|
"settings.storage.title": "Storage",
|
||||||
|
"settings.storage.location_label": "Location",
|
||||||
|
"settings.storage.location_hint": "Changing the storage location isn't supported yet — this is where meetings, models, and the database currently live.",
|
||||||
|
"settings.storage.retention_title": "Retention",
|
||||||
|
"settings.storage.retention_hint": "Automatically delete the oldest meetings past a limit. Checked once at startup; empty means no limit. Never touches a meeting that's currently recording.",
|
||||||
|
"settings.storage.max_age": "Max age (days)",
|
||||||
|
"settings.storage.max_size": "Max size (GB)",
|
||||||
|
"settings.storage.no_limit": "no limit",
|
||||||
|
"settings.storage.export_import_title": "Export & import",
|
||||||
|
"settings.storage.export_hint_1": "Move recordings between computers as portable bundle folders (audio, transcript, notes, summary, and a",
|
||||||
|
"settings.storage.export_hint_2": "manifest). Export to any folder — a synced drive, a USB stick, or a sync target's local mount — then import it on the other machine. Imported meetings get a fresh id, so re-importing never overwrites anything.",
|
||||||
|
"settings.storage.export_dialog_title": "Export all meetings into…",
|
||||||
|
"settings.storage.import_dialog_title": "Import a bundle (or a folder of bundles)…",
|
||||||
|
"settings.storage.exporting": "Exporting…",
|
||||||
|
"settings.storage.export_all": "Export all meetings…",
|
||||||
|
"settings.storage.importing": "Importing…",
|
||||||
|
"settings.storage.import_all": "Import meetings…",
|
||||||
|
"settings.storage.exported_one": "Exported {n} meeting.",
|
||||||
|
"settings.storage.exported_many": "Exported {n} meetings.",
|
||||||
|
"settings.storage.imported_one": "Imported {n} meeting.",
|
||||||
|
"settings.storage.imported_many": "Imported {n} meetings.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "Calendar & Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "Import events and attendees from a local Outlook",
|
||||||
|
"settings.calendar.intro_2": "backup — read-only, nothing is written back to the file. Nothing leaves this device.",
|
||||||
|
"settings.calendar.pst_file": ".pst file",
|
||||||
|
"settings.calendar.no_file": "No file selected",
|
||||||
|
"settings.calendar.browse": "Browse…",
|
||||||
|
"settings.calendar.password": "Password",
|
||||||
|
"settings.calendar.password_note": "rarely needed",
|
||||||
|
"settings.calendar.optional": "optional",
|
||||||
|
"settings.calendar.import_range": "Import range",
|
||||||
|
"settings.calendar.range_30": "Last 30 days",
|
||||||
|
"settings.calendar.range_90": "Last 90 days",
|
||||||
|
"settings.calendar.range_180": "Last 6 months",
|
||||||
|
"settings.calendar.range_365": "Last 1 year",
|
||||||
|
"settings.calendar.range_all": "All time",
|
||||||
|
"settings.calendar.range_hint": "A long-lived mailbox can hold years of recurring/holiday entries — narrowing the range keeps the imported calendar to what's actually relevant. Applies to both this Import button and automatic re-import on launch.",
|
||||||
|
"settings.calendar.importing": "Importing…",
|
||||||
|
"settings.calendar.import": "Import",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} events",
|
||||||
|
"settings.calendar.auto_reimport": "Re-import this file automatically on launch",
|
||||||
|
"settings.calendar.auto_reimport_hint": "Runs once at startup, not on a timer — re-import is safe to repeat (existing events are matched and updated, not duplicated).",
|
||||||
|
"settings.calendar.import_failed": "Import failed: {error} — the file itself is untouched; check the path and try again.",
|
||||||
|
"settings.calendar.cleanup_title": "Clean up",
|
||||||
|
"settings.calendar.cleanup_hint": "Events already attached to a recorded meeting are always kept, no matter which option below is picked.",
|
||||||
|
"settings.calendar.cleanup_30": "Older than 30 days",
|
||||||
|
"settings.calendar.cleanup_90": "Older than 90 days",
|
||||||
|
"settings.calendar.cleanup_180": "Older than 6 months",
|
||||||
|
"settings.calendar.cleanup_365": "Older than 1 year",
|
||||||
|
"settings.calendar.cleanup_all": "Delete all",
|
||||||
|
"settings.calendar.cleaning": "Cleaning up…",
|
||||||
|
"settings.calendar.cleanup_btn": "Clean up calendar",
|
||||||
|
"settings.calendar.cleanup_result": "Deleted {deleted}{extra}.",
|
||||||
|
"settings.calendar.cleanup_kept": ", kept {n} (linked to a meeting)",
|
||||||
|
"settings.calendar.cleanup_failed": "Clean up failed: {error}",
|
||||||
|
"settings.calendar.imported_events": "Imported events",
|
||||||
|
"settings.calendar.no_events": "No events imported yet.",
|
||||||
|
"settings.calendar.search_title": "Search title",
|
||||||
|
"settings.calendar.search_placeholder": "Meeting name…",
|
||||||
|
"settings.calendar.date_label": "Date",
|
||||||
|
"settings.calendar.events_count": "{shown} of {total} events",
|
||||||
|
"settings.calendar.untitled": "(untitled)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "Settings",
|
||||||
|
"settings.close": "Close",
|
||||||
|
"settings.close_title": "Close (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "Preview mode — the sync backend isn't implemented yet (Phase 9). Changes are kept in the UI only.",
|
||||||
|
"settings.sync.title": "Sync & upload",
|
||||||
|
"settings.sync.enable_label": "Enable uploading meeting artifacts to configured targets",
|
||||||
|
"settings.sync.enable_hint": "Off by default. Nothing is uploaded unless this is on and a target is enabled. Self-hosted targets keep data on your own server; third-party clouds are clearly labeled.",
|
||||||
|
"settings.sync.targets_title": "Targets",
|
||||||
|
"settings.sync.no_targets": "No targets yet. Add one below.",
|
||||||
|
"settings.sync.third_party": "third-party",
|
||||||
|
"settings.sync.your_server": "your server",
|
||||||
|
"settings.sync.edit": "Edit",
|
||||||
|
"settings.sync.remove": "Remove",
|
||||||
|
"settings.sync.edit_target": "Edit target",
|
||||||
|
"settings.sync.add_target": "Add a target",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV covers Nextcloud, ownCloud, Cloudreve, Seafile, and Synology. (Seafile: enable SeafDAV server-side.)",
|
||||||
|
"settings.sync.name": "Name",
|
||||||
|
"settings.sync.name_placeholder": "Home Nextcloud",
|
||||||
|
"settings.sync.provider": "Provider",
|
||||||
|
"settings.sync.server_url": "Server URL",
|
||||||
|
"settings.sync.server_url_note_1": "Just your server URL — WhispAssist adds",
|
||||||
|
"settings.sync.server_url_note_2": "automatically.",
|
||||||
|
"settings.sync.remote_folder": "Remote folder",
|
||||||
|
"settings.sync.username": "Username",
|
||||||
|
"settings.sync.app_password": "App password",
|
||||||
|
"settings.sync.pw_keep": "leave blank to keep current password",
|
||||||
|
"settings.sync.pw_store": "stored in OS credential store",
|
||||||
|
"settings.sync.upload_legend": "Upload",
|
||||||
|
"settings.sync.artifact_transcript": "transcript",
|
||||||
|
"settings.sync.artifact_notes": "notes",
|
||||||
|
"settings.sync.artifact_summary": "summary",
|
||||||
|
"settings.sync.artifact_recording": "recording (.wav, if retained)",
|
||||||
|
"settings.sync.allow_plaintext": "Allow plaintext http for a LAN address (not recommended)",
|
||||||
|
"settings.sync.encrypt_before": "Encrypt before upload (destination stores only ciphertext)",
|
||||||
|
"settings.sync.requires_vault": "— requires an unlocked vault",
|
||||||
|
"settings.sync.test_connection": "Test connection",
|
||||||
|
"settings.sync.save_changes": "Save changes",
|
||||||
|
"settings.sync.add_target_btn": "Add target",
|
||||||
|
"settings.sync.cancel": "Cancel",
|
||||||
|
"settings.sync.third_party_banner": "{kind} is a third-party cloud — uploading sends your data off your device to {kind}.",
|
||||||
|
"settings.sync.link_account": "Link {kind} account…",
|
||||||
|
"settings.sync.oauth_hint": "Opens an OAuth sign-in (loopback redirect); the token is stored in your OS credential store.",
|
||||||
|
"settings.sync.footnote": "Credentials are never written to settings or the database — only the OS credential store. TLS is required for non-LAN targets.",
|
||||||
|
|
||||||
|
"settings.ai.title": "AI summary provider",
|
||||||
|
"settings.ai.intro_1": "Summaries run on a local LLM by default. Point this at Ollama on this PC or another machine on your LAN (e.g.",
|
||||||
|
"settings.ai.intro_2": "— both count as local, so nothing leaves your network. Hosted providers (Anthropic) are optional, off by default, and send the transcript to a third party once you turn one on.",
|
||||||
|
"settings.ai.provider": "Provider",
|
||||||
|
"settings.ai.off": "Off",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (local / LAN)",
|
||||||
|
"settings.ai.provider_custom": "Custom (OpenAI-compatible)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — hosted, leaves this device",
|
||||||
|
"settings.ai.endpoint": "Endpoint",
|
||||||
|
"settings.ai.model": "Model",
|
||||||
|
"settings.ai.api_key": "API key",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (already set — leave blank to keep it)",
|
||||||
|
"settings.ai.show_api_key": "Show API key",
|
||||||
|
"settings.ai.hide_api_key": "Hide API key",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic is a hosted, third-party service — this meeting's transcript leaves your device when you generate a summary.",
|
||||||
|
"settings.ai.endpoint_banner": "This endpoint isn't on your machine or LAN — your transcript would leave your network.",
|
||||||
|
"settings.ai.saving": "Saving…",
|
||||||
|
"settings.ai.save": "Save",
|
||||||
|
"settings.ai.test_connection": "Test connection",
|
||||||
|
"settings.ai.this_hosted_provider": "this hosted provider",
|
||||||
|
"settings.ai.reachable": "Reachable",
|
||||||
|
"settings.ai.unreachable": "Unreachable",
|
||||||
|
"settings.ai.model_count_one": "· {n} model",
|
||||||
|
"settings.ai.model_count_many": "· {n} models",
|
||||||
|
"settings.ai.on_lan": "· on your machine / LAN",
|
||||||
|
"settings.ai.leaves_network": "· leaves your network",
|
||||||
|
"settings.ai.advanced_title": "Advanced Ollama configuration",
|
||||||
|
"settings.ai.changed": "{n} changed",
|
||||||
|
"settings.ai.reset_all": "Reset all to defaults",
|
||||||
|
"settings.ai.system_prompt": "System prompt",
|
||||||
|
"settings.ai.system_prompt_placeholder": "e.g. You are a concise meeting summarizer.",
|
||||||
|
"settings.ai.system_prompt_hint": "Added before WhispAssist's required output format, so your instructions can't break summary/action-item parsing.",
|
||||||
|
"settings.ai.think": "Think",
|
||||||
|
"settings.ai.think_help": "Reasoning effort (reasoning models only).",
|
||||||
|
"settings.ai.keep_alive": "Keep alive",
|
||||||
|
"settings.ai.keep_alive_help": "How long the model stays in RAM. e.g. 5m, 1h, 0 (unload), -1 (forever).",
|
||||||
|
"settings.ai.runtime_hardware": "Runtime & hardware",
|
||||||
|
"settings.ai.rarely_needed": "— rarely needed",
|
||||||
|
"settings.ai.save_advanced": "Save advanced",
|
||||||
|
"settings.ai.saved": "Saved",
|
||||||
|
"settings.ai.turn_off": "Turn off",
|
||||||
|
"settings.ai.default_value": "Default {value}.",
|
||||||
|
"settings.ai.reset_default": "Reset to default",
|
||||||
|
"settings.ai.reset_field": "Reset {name}",
|
||||||
|
|
||||||
|
"settings.mcp.title": "MCP server",
|
||||||
|
"settings.mcp.banner_1": "This lets your own coding agent (Claude Code, Codex, Copilot, OpenCode, …) pull meeting context on your local machine. Once connected,",
|
||||||
|
"settings.mcp.banner_agent": "that agent",
|
||||||
|
"settings.mcp.banner_2": "may forward what it reads to its own model provider's cloud — outside WhispAssist's control. WhispAssist itself never sends this data anywhere; the server only listens on this device (",
|
||||||
|
"settings.mcp.banner_3": ") and every read is logged below.",
|
||||||
|
"settings.mcp.enable": "Enable the MCP server",
|
||||||
|
"settings.mcp.enable_hint": "Off by default. Loopback-only, token-gated — nothing is reachable from the network.",
|
||||||
|
"settings.mcp.transport": "Transport",
|
||||||
|
"settings.mcp.transport_http": "Streamable HTTP",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (agent spawns a process)",
|
||||||
|
"settings.mcp.port": "Port",
|
||||||
|
"settings.mcp.transport_change_hint": "To change transport/port, turn the server off first, then back on.",
|
||||||
|
"settings.mcp.scope_title": "Scope",
|
||||||
|
"settings.mcp.expose": "Expose",
|
||||||
|
"settings.mcp.scope_none": "None — nothing is shared",
|
||||||
|
"settings.mcp.scope_selected": "Selected — only feature briefs you've marked shared",
|
||||||
|
"settings.mcp.scope_all": "All — meetings, transcripts, action items, and shared briefs",
|
||||||
|
"settings.mcp.expose_recordings": "Also allow meetings with a saved recording (off by default — a recorded meeting's transcript is withheld even in \"All\" scope until this is on)",
|
||||||
|
"settings.mcp.token_title": "New auth token — shown once, copy it now",
|
||||||
|
"settings.mcp.token_hint": "This won't be shown again. It's stored in your OS credential store; if you lose it, turn the server off and back on to mint a new one.",
|
||||||
|
"settings.mcp.copied": "Copied",
|
||||||
|
"settings.mcp.copy": "Copy",
|
||||||
|
"settings.mcp.endpoint": "Endpoint",
|
||||||
|
"settings.mcp.endpoint_hint": "Point your agent's MCP client config at this {kind}, with the token above as a bearer credential.",
|
||||||
|
"settings.mcp.kind_command": "command",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "Access log",
|
||||||
|
"settings.mcp.access_log_hint": "Every tool read an agent makes, allowed or denied (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "No agent has read anything yet.",
|
||||||
|
"settings.mcp.log_meeting": "meeting {id}",
|
||||||
|
"settings.mcp.refresh": "Refresh",
|
||||||
|
|
||||||
|
"settings.privacy.title": "Privacy",
|
||||||
|
"settings.privacy.intro": "What WhispAssist is actually allowed to send off this device right now. With everything off (the default), nothing leaves the device at all.",
|
||||||
|
"settings.privacy.llm_endpoint": "LLM endpoint",
|
||||||
|
"settings.privacy.off": "off",
|
||||||
|
"settings.privacy.local_only": "local-only",
|
||||||
|
"settings.privacy.leaves_device": "leaves this device",
|
||||||
|
"settings.privacy.sync_label": "Sync",
|
||||||
|
"settings.privacy.enabled": "enabled",
|
||||||
|
"settings.privacy.mcp_label": "MCP server",
|
||||||
|
"settings.privacy.mcp_on": "on · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "Inbound on loopback only — it adds nothing to the egress list above. A connected agent may still forward what it reads to its own model provider; see the MCP server tab.",
|
||||||
|
"settings.privacy.egress_title": "Egress allowlist",
|
||||||
|
"settings.privacy.no_egress": "No hosts are allowlisted — WA makes no content egress.",
|
||||||
|
"settings.privacy.sync_targets_title": "Sync targets",
|
||||||
|
"settings.privacy.third_party": "third-party",
|
||||||
|
"settings.privacy.your_server": "your server",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "no TLS",
|
||||||
|
"settings.privacy.refresh": "Refresh",
|
||||||
|
"settings.privacy.unavailable": "Privacy self-check unavailable.",
|
||||||
|
"settings.privacy.vault_title": "Encryption vault",
|
||||||
|
"settings.privacy.vault_intro": "Encrypt notes, transcripts, and summaries at rest with a password. (Audio files are not encrypted yet.)",
|
||||||
|
"settings.privacy.vault_password": "Vault password",
|
||||||
|
"settings.privacy.enable_vault": "Enable vault",
|
||||||
|
"settings.privacy.vault_pw_hint": "Use at least 8 characters. If you forget it, encrypted content can't be recovered.",
|
||||||
|
"settings.privacy.vault_locked_1": "Vault is ",
|
||||||
|
"settings.privacy.locked_word": "locked",
|
||||||
|
"settings.privacy.vault_locked_2": ". Unlock to read encrypted meetings.",
|
||||||
|
"settings.privacy.password": "Password",
|
||||||
|
"settings.privacy.show_password": "Show password",
|
||||||
|
"settings.privacy.unlock": "Unlock",
|
||||||
|
"settings.privacy.vault_unlocked_1": "Vault is ",
|
||||||
|
"settings.privacy.unlocked_word": "unlocked",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". New notes, transcripts, and summaries are encrypted at rest.",
|
||||||
|
"settings.privacy.lock_now": "Lock now",
|
||||||
|
"settings.privacy.change_password": "Change password",
|
||||||
|
"settings.privacy.current_password": "Current password",
|
||||||
|
"settings.privacy.new_password": "New password",
|
||||||
|
|
||||||
|
"settings.about.title": "About",
|
||||||
|
"settings.about.tagline": "A fully local, open-source, Windows-native meeting assistant.",
|
||||||
|
"settings.about.build_commit": "Build commit",
|
||||||
|
|
||||||
|
"consent.heading": "Before you record",
|
||||||
|
"consent.body": "Recording conversations without the consent of participants may be illegal in your region. Check your local recording laws. This is a caution, not legal advice.",
|
||||||
|
"consent.accept": "I understand — enable recording",
|
||||||
|
"consent.cancel": "Cancel",
|
||||||
|
|
||||||
|
"hosted.heading": "Before using {provider}",
|
||||||
|
"hosted.body_1": "Generating with",
|
||||||
|
"hosted.body_2": "sends this meeting's transcript to their servers — it leaves this device and is subject to their privacy policy. WhispAssist has no control over data handling once it leaves your device. This is off by default; you're choosing it now.",
|
||||||
|
"hosted.accept": "I understand — continue",
|
||||||
|
"hosted.cancel": "Cancel",
|
||||||
|
|
||||||
|
"theme.group": "Theme",
|
||||||
|
"theme.system": "Match system",
|
||||||
|
"theme.light": "Light",
|
||||||
|
"theme.dark": "Dark",
|
||||||
|
|
||||||
|
"import.dialog_label": "Add a meeting from a file or URL",
|
||||||
|
"import.title": "Add a meeting",
|
||||||
|
"import.close": "Close",
|
||||||
|
"import.close_title": "Close (Esc)",
|
||||||
|
"import.body": "Import an existing recording — a local audio/video file, or a link (YouTube, a streaming page, or a direct media URL). It's transcribed and diarized just like a live recording.",
|
||||||
|
"import.file_or_url": "File or URL",
|
||||||
|
"import.source_placeholder": "Paste a URL, or browse for a file…",
|
||||||
|
"import.choose_file": "Choose a local file",
|
||||||
|
"import.browse": "Browse…",
|
||||||
|
"import.title_label": "Title",
|
||||||
|
"import.optional": "optional",
|
||||||
|
"import.title_placeholder": "Defaults to the file name",
|
||||||
|
"import.filter_av": "Audio / video",
|
||||||
|
"import.model_label": "Transcription model",
|
||||||
|
"import.model_hint": "Recorded with the meeting so you can see how it was transcribed.",
|
||||||
|
"import.requires": "Needs these on your PATH (not bundled):",
|
||||||
|
"import.background_note": "Runs in the background — track it in the list.",
|
||||||
|
"import.importing": "Starting…",
|
||||||
|
"import.import": "Import",
|
||||||
|
"import.cancel": "Cancel",
|
||||||
|
"import.tracker.label": "Import progress",
|
||||||
|
"import.tracker.running": "Importing…",
|
||||||
|
"import.tracker.done": "Import complete",
|
||||||
|
"import.tracker.failed": "Import failed",
|
||||||
|
"import.tracker.working": "working…",
|
||||||
|
"import.tracker.stopped": "stopped",
|
||||||
|
"import.phase.prepare": "Transcode",
|
||||||
|
"import.phase.transcribe": "Transcribe",
|
||||||
|
"import.phase.diarize": "Identify speakers",
|
||||||
|
"import.phase.finalize": "Finalize",
|
||||||
|
|
||||||
|
"tagchip.filter": "Filter meetings tagged \"{tag}\"",
|
||||||
|
"tagchip.remove": "Remove tag {tag}",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "System and microphone input level",
|
||||||
|
"levelmeter.system": "System input level",
|
||||||
|
|
||||||
|
"app.tagline": "local · private",
|
||||||
|
"app.note_template": "Note template",
|
||||||
|
"app.no_template": "No template",
|
||||||
|
"app.add_meeting": "Add meeting",
|
||||||
|
"app.add_meeting_title": "Add a meeting from a file or URL",
|
||||||
|
"app.record": "Record",
|
||||||
|
"app.record_title": "Start recording (Ctrl+Shift+R)",
|
||||||
|
"app.stop": "Stop",
|
||||||
|
"app.stop_title": "Stop recording (Ctrl+Shift+R)",
|
||||||
|
"app.cancel": "Cancel",
|
||||||
|
"app.cancel_title": "Discard this recording and delete it",
|
||||||
|
"app.recording": "Recording…",
|
||||||
|
"app.mute": "Mute microphone",
|
||||||
|
"app.unmute": "Unmute microphone",
|
||||||
|
"app.mute_title": "Mute microphone (M)",
|
||||||
|
"app.unmute_title": "Unmute microphone (M)",
|
||||||
|
"app.backend_title": "Active transcription backend",
|
||||||
|
"app.retention_title": "Save audio as .wav for this meeting",
|
||||||
|
"app.saving": "saving",
|
||||||
|
"app.not_saved": "not saved",
|
||||||
|
"app.reconnecting": "reconnecting audio device…",
|
||||||
|
"app.settings": "Settings",
|
||||||
|
"app.settings_title": "Settings (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "Discard this recording? Its audio and transcript will be deleted.",
|
||||||
|
"app.announce_started": "Recording started",
|
||||||
|
"app.announce_paused": "Recording paused",
|
||||||
|
"app.announce_stopped": "Recording stopped",
|
||||||
|
"app.consent_dialog": "Recording consent",
|
||||||
|
"app.vault_dialog": "Unlock encryption vault",
|
||||||
|
"app.vault_title": "Unlock encryption vault",
|
||||||
|
"app.vault_desc": "Your meetings are encrypted at rest. Enter your password to read them.",
|
||||||
|
"app.vault_password": "Vault password",
|
||||||
|
"app.vault_incorrect": "Incorrect password",
|
||||||
|
"app.unlock": "Unlock",
|
||||||
|
"app.continue_locked": "Continue locked",
|
||||||
|
"app.show_meetings": "Show meetings list",
|
||||||
|
"app.hide_meetings": "Hide meetings list",
|
||||||
|
"app.show_summary": "Show summary panel",
|
||||||
|
"app.hide_summary": "Hide summary panel",
|
||||||
|
"app.resize_meetings": "Resize meetings list",
|
||||||
|
"app.resize_summary": "Resize summary panel",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "Search meetings…",
|
||||||
|
"meetings.search_aria": "Search meetings",
|
||||||
|
"meetings.filter_tag_aria": "Filter by tag",
|
||||||
|
"meetings.all_tags": "All tags",
|
||||||
|
"meetings.from_date_aria": "From date",
|
||||||
|
"meetings.to_date_aria": "To date",
|
||||||
|
"meetings.bulk_format_aria": "Bulk export format",
|
||||||
|
"meetings.bulk_export": "Bulk export",
|
||||||
|
"meetings.bulk_export_title": "Export every meeting matching the tag/date filters above",
|
||||||
|
"meetings.exporting": "Exporting…",
|
||||||
|
"meetings.exported_one": "Exported {n} meeting.",
|
||||||
|
"meetings.exported_many": "Exported {n} meetings.",
|
||||||
|
"meetings.searching": "Searching…",
|
||||||
|
"meetings.loading": "Loading meetings…",
|
||||||
|
"meetings.no_matches": "No matches.",
|
||||||
|
"meetings.empty": "No meetings yet — click Record above to start.",
|
||||||
|
"meetings.status.recording": "recording",
|
||||||
|
"meetings.status.transcribing": "transcribing",
|
||||||
|
"meetings.status.recovering": "recovering",
|
||||||
|
"meetings.status.error": "error",
|
||||||
|
"meetings.resume": "Resume transcription",
|
||||||
|
"meetings.delete_aria": "Delete meeting",
|
||||||
|
"meetings.delete_confirm": "Delete this meeting? This removes its recording, transcript, and notes.",
|
||||||
|
|
||||||
|
"transcript.heading": "Transcript",
|
||||||
|
"transcript.title_aria": "Meeting title",
|
||||||
|
"transcript.transcribed_with": "Transcribed with",
|
||||||
|
"transcript.lang_title": "Transcription language",
|
||||||
|
"transcript.lang_auto": "auto-detecting…",
|
||||||
|
"transcript.show": "Show transcript",
|
||||||
|
"transcript.hide": "Hide transcript",
|
||||||
|
"transcript.empty": "No transcript for this meeting.",
|
||||||
|
"transcript.will_appear": "Transcript will appear here as you record.",
|
||||||
|
"transcript.play_from_here": "Play from here",
|
||||||
|
"transcript.resize": "Resize transcript and notes",
|
||||||
|
"transcript.reprocess_placeholder": "Re-transcribe with…",
|
||||||
|
"transcript.reprocess_lang_aria": "Reprocess language",
|
||||||
|
"transcript.reprocess_keep_lang": "Keep current language",
|
||||||
|
"transcript.reprocess_go": "Go",
|
||||||
|
"transcript.retranscribing": "Re-transcribing…",
|
||||||
|
"transcript.has_note": "Has a note",
|
||||||
|
"transcript.note_placeholder": "Add a note for this moment…",
|
||||||
|
"transcript.note_aria": "Note for this transcript line",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Word document",
|
||||||
|
"transcript.filter_obsidian": "Obsidian note",
|
||||||
|
|
||||||
|
"notes.heading": "Notes",
|
||||||
|
"notes.show": "Show notes",
|
||||||
|
"notes.hide": "Hide notes",
|
||||||
|
"notes.placeholder": "Notes…",
|
||||||
|
"notes.live_placeholder": "Type notes while you talk…",
|
||||||
|
"notes.toolbar_aria": "Notes formatting",
|
||||||
|
"notes.bold": "Bold",
|
||||||
|
"notes.italic": "Italic",
|
||||||
|
"notes.h1": "Heading 1",
|
||||||
|
"notes.h2": "Heading 2",
|
||||||
|
"notes.h3": "Heading 3",
|
||||||
|
"notes.bullet": "Bullet list",
|
||||||
|
"notes.numbered": "Numbered list",
|
||||||
|
"notes.quote": "Quote",
|
||||||
|
"notes.divider": "Divider",
|
||||||
|
"notes.checkbox_title": "Checkbox",
|
||||||
|
"notes.checkbox_aria": "Checkbox list item",
|
||||||
|
"notes.enhance": "Enhance",
|
||||||
|
"notes.enhancing": "Enhancing…",
|
||||||
|
"notes.enhance_title": "Expand these notes into structured Markdown using the transcript (AI)",
|
||||||
|
"notes.enhanced_note": "Notes enhanced from the transcript.",
|
||||||
|
"notes.undo_enhance": "Undo",
|
||||||
|
"notes.edit_raw": "Edit the raw markdown",
|
||||||
|
"notes.render": "Render the markdown",
|
||||||
|
"notes.editor": "Editor",
|
||||||
|
"notes.preview": "Preview",
|
||||||
|
"notes.export_md_title": "Export notes as .md",
|
||||||
|
"notes.export_pdf_title": "Export notes as .pdf",
|
||||||
|
"notes.export_docx_title": "Export notes as .docx",
|
||||||
|
"notes.export_bundle_title": "Export audio + transcript + notes to a folder",
|
||||||
|
"notes.export_obsidian_title": "Export one Obsidian note (notes + summary + transcript, no audio)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "Recording",
|
||||||
|
"summary.loading_recording": "Loading recording…",
|
||||||
|
"summary.sync_heading": "Sync",
|
||||||
|
"summary.uploading": "Uploading…",
|
||||||
|
"summary.upload_now": "Upload now",
|
||||||
|
"summary.no_uploads": "No uploads yet for this meeting.",
|
||||||
|
"summary.retry": "retry",
|
||||||
|
"summary.tags_heading": "Tags",
|
||||||
|
"summary.tags_select": "Select a meeting to tag it.",
|
||||||
|
"summary.tag_add_placeholder": "Add tag…",
|
||||||
|
"summary.tag_first_placeholder": "project, client, topic…",
|
||||||
|
"summary.generating": "Generating…",
|
||||||
|
"summary.generate_tags": "Generate tags",
|
||||||
|
"summary.saving": "Saving…",
|
||||||
|
"summary.save_tags": "Save tags",
|
||||||
|
"summary.summary_heading": "Summary",
|
||||||
|
"summary.ai_provider": "AI provider",
|
||||||
|
"summary.provider_off": "Off",
|
||||||
|
"summary.provider_custom": "Custom",
|
||||||
|
"summary.local": "local",
|
||||||
|
"summary.leaves_device": "leaves this device",
|
||||||
|
"summary.this_hosted_provider": "this hosted provider",
|
||||||
|
"summary.select_generate": "Select a meeting to generate a summary.",
|
||||||
|
"summary.decisions_heading": "Decisions",
|
||||||
|
"summary.regenerate": "Regenerate",
|
||||||
|
"summary.generated_hint": "Generated locally after the meeting (requires a local LLM provider).",
|
||||||
|
"summary.no_provider": "No AI provider configured — enable one in Settings.",
|
||||||
|
"summary.generate_summary": "Generate summary",
|
||||||
|
"summary.briefs_heading": "Feature briefs",
|
||||||
|
"summary.briefs_select": "Select a meeting to create or view feature briefs.",
|
||||||
|
"summary.briefs_desc": "Distills this meeting into an agent-ready spec (requires a local LLM provider) — hand it to a coding agent, or serve it over the MCP server once that's on.",
|
||||||
|
"summary.brief_repo_placeholder": "Target repo (optional), e.g. acme/reporting-web",
|
||||||
|
"summary.distilling": "Distilling…",
|
||||||
|
"summary.create_brief": "Create feature brief",
|
||||||
|
"summary.brief_mcp_title": "Available when the MCP server is on",
|
||||||
|
"summary.copied": "Copied",
|
||||||
|
"summary.copy_md": "Copy as Markdown",
|
||||||
|
"summary.copy_json": "Copy as JSON",
|
||||||
|
"summary.brief_problem": "Problem",
|
||||||
|
"summary.brief_outcome": "Desired outcome",
|
||||||
|
"summary.brief_criteria": "Acceptance criteria",
|
||||||
|
"summary.brief_none": "None captured.",
|
||||||
|
"summary.brief_context": "Context",
|
||||||
|
"summary.action_items_heading": "Action items",
|
||||||
|
"summary.ai_select": "Select a meeting to see its action items.",
|
||||||
|
"summary.ai_empty": "None yet — add one below, or generate a summary to extract them automatically.",
|
||||||
|
"summary.confirmed": "Confirmed",
|
||||||
|
"summary.ai_text_placeholder": "Action item…",
|
||||||
|
"summary.ai_text_aria": "Action item text",
|
||||||
|
"summary.owner": "Owner",
|
||||||
|
"summary.due_date": "Due date",
|
||||||
|
"summary.reminder_title": "Schedule a local reminder for this due date",
|
||||||
|
"summary.ai_delete_title": "Delete this action item",
|
||||||
|
"summary.ai_delete_aria": "Delete action item",
|
||||||
|
"summary.add_action_item": "Add action item",
|
||||||
|
"summary.save_action_items": "Save action items",
|
||||||
|
"summary.calendar_heading": "Calendar event",
|
||||||
|
"summary.cal_select": "Select a meeting to link it to a calendar event.",
|
||||||
|
"summary.event_search_placeholder": "Search event title…",
|
||||||
|
"summary.linked_event": "Linked event",
|
||||||
|
"summary.change_event": "Change event…",
|
||||||
|
"summary.link_event": "Link an event…",
|
||||||
|
"summary.untitled_event": "(untitled)",
|
||||||
|
"summary.no_events_1": "No events imported yet — import a",
|
||||||
|
"summary.no_events_2": "from Settings → Calendar.",
|
||||||
|
"summary.no_events_match": "No events match this search/date — try clearing one.",
|
||||||
|
"summary.organizer_label": "Organizer: {name}",
|
||||||
|
"summary.participants_heading": "Participants",
|
||||||
|
"summary.participants_hint": "Populated from the linked calendar event.",
|
||||||
|
"summary.speakers_heading": "Speakers",
|
||||||
|
"summary.speakers_select": "Select a meeting to name its speakers.",
|
||||||
|
"summary.no_speakers": "No speakers detected yet.",
|
||||||
|
"summary.speaker_name_placeholder": "Speaker name",
|
||||||
|
"summary.save": "Save",
|
||||||
|
"summary.cancel": "Cancel",
|
||||||
|
"summary.name_speaker": "Name this speaker…",
|
||||||
|
"summary.add_new_name": "+ Add new name…",
|
||||||
|
|
||||||
|
"settings.recording.title": "Recording",
|
||||||
|
"settings.recording.record_default": "Record meetings by default",
|
||||||
|
"settings.recording.save_audio_as": "save audio as",
|
||||||
|
"settings.recording.default_hint": "Off by default. When off, audio is used only to produce the transcript and is deleted when the meeting is finalized. You can also toggle recording per meeting.",
|
||||||
|
"settings.recording.consent_label": "Consent: {status}.",
|
||||||
|
"settings.recording.consent_ack": "acknowledged",
|
||||||
|
"settings.recording.consent_not": "not yet acknowledged",
|
||||||
|
"settings.recording.auto_label": "Auto-start recording when a calendar event begins",
|
||||||
|
"settings.recording.auto_hint": "Only while WhispAssist is open. When an imported calendar event's start time arrives, a recording begins automatically (using your default retention setting above). Nothing runs in the background — the timer is armed only while the app is running. Import events under Settings → Calendar.",
|
||||||
|
"settings.recording.autostart_label": "Launch WhispAssist at login",
|
||||||
|
"settings.recording.autostart_hint": "Starts WhispAssist automatically when you sign in to Windows. Off by default; installs a per-user startup entry (no admin required) and does not begin recording on its own.",
|
||||||
|
"settings.recording.close_tray_label": "Close to system tray",
|
||||||
|
"settings.recording.close_tray_hint": "Closing the window keeps WhispAssist running in the background instead of quitting. Reopen it from the tray icon; use the tray's Quit to exit fully. On by default."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "Grabación",
|
||||||
|
"nav.hardware": "Hardware",
|
||||||
|
"nav.storage": "Almacenamiento",
|
||||||
|
"nav.calendar": "Calendario",
|
||||||
|
"nav.sync": "Sincronización",
|
||||||
|
"nav.ai": "IA",
|
||||||
|
"nav.mcp": "Servidor MCP",
|
||||||
|
"nav.privacy": "Privacidad",
|
||||||
|
"nav.about": "Acerca de",
|
||||||
|
"nav.language": "Idioma",
|
||||||
|
|
||||||
|
"settings.language.title": "Idioma",
|
||||||
|
"settings.language.display": "Idioma de la interfaz",
|
||||||
|
"settings.language.display_hint": "El idioma de la interfaz de la aplicación. Añadir un idioma es tan sencillo como colocar un único archivo de traducción.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "Idioma de transcripción",
|
||||||
|
"settings.transcription.label": "Idioma",
|
||||||
|
"settings.transcription.auto": "Detección automática",
|
||||||
|
"settings.transcription.applies_hint": "Se aplica a la próxima grabación. El idioma realmente utilizado se muestra después en cada reunión.",
|
||||||
|
"settings.transcription.english_only": "Solo inglés",
|
||||||
|
"settings.transcription.model_english_only": "«{model}» es solo en inglés.",
|
||||||
|
"settings.transcription.no_model": "Ningún modelo seleccionado.",
|
||||||
|
"settings.transcription.switch_multilingual": "Cambia a un modelo multilingüe arriba para elegir un idioma.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "Hardware",
|
||||||
|
"settings.hardware.active_backend": "Backend activo",
|
||||||
|
"settings.hardware.model_meta": "· modelo {size}",
|
||||||
|
"settings.hardware.preferred_backend": "Backend preferido",
|
||||||
|
"settings.hardware.auto_backend": "Automático (el mejor disponible)",
|
||||||
|
"settings.hardware.not_available": "no disponible",
|
||||||
|
"settings.hardware.fallback_hint": "Recurre automáticamente (NPU → NVIDIA → AMD → Intel → CPU) si el backend elegido no se carga.",
|
||||||
|
"settings.hardware.audio_devices": "Dispositivos de audio",
|
||||||
|
"settings.hardware.recording_device": "Dispositivo de grabación",
|
||||||
|
"settings.hardware.default_system_audio": "Audio del sistema predeterminado",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist graba lo que reproduce este dispositivo (loopback) — el otro lado de la llamada. Elige una salida concreta si no quieres que siga el valor predeterminado del sistema de Windows.",
|
||||||
|
"settings.hardware.microphone": "Micrófono",
|
||||||
|
"settings.hardware.mic_off": "Desactivado — no capturar mi micrófono",
|
||||||
|
"settings.hardware.default_mic": "Micrófono predeterminado",
|
||||||
|
"settings.hardware.mic_hint": "Añade tu propia voz a la transcripción en directo para que se capturen ambos lados de la reunión. Permanece en tu dispositivo — no se sube nada. Elige «Desactivado» para transcribir solo el audio del sistema de arriba.",
|
||||||
|
"settings.hardware.npu_detected": "Acelerador NPU detectado",
|
||||||
|
"settings.hardware.ready": "Listo",
|
||||||
|
"settings.hardware.downloading": "Descargando…",
|
||||||
|
"settings.hardware.package_needed": "Se necesita un paquete",
|
||||||
|
"settings.hardware.npu_hint": "Tu NPU necesita un paquete de aceleración único (modelo Whisper ONNX{extra}). Se descarga en segundo plano; también puedes iniciarlo ahora.",
|
||||||
|
"settings.hardware.plus_openvino": " + entorno de ejecución OpenVINO",
|
||||||
|
"settings.hardware.download_npu": "Descargar paquete NPU",
|
||||||
|
"settings.hardware.dml_title": "Aceleración de GPU (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "Tu GPU puede acelerarse sin Vulkan mediante DirectML — un paquete único (modelo Whisper ONNX{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + entorno de ejecución DirectML",
|
||||||
|
"settings.hardware.download_dml": "Descargar paquete DirectML",
|
||||||
|
"settings.hardware.detection_unavailable": "Detección de hardware no disponible.",
|
||||||
|
"settings.hardware.low_overhead": "Preajuste de baja carga",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + modelo más pequeño, para uso con batería/en segundo plano",
|
||||||
|
"settings.hardware.models": "Modelos",
|
||||||
|
"settings.hardware.downloading_model": "Descargando {name}",
|
||||||
|
"settings.hardware.active_badge": "Activo",
|
||||||
|
"settings.hardware.installed": "Instalado",
|
||||||
|
"settings.hardware.use": "Usar",
|
||||||
|
"settings.hardware.remove": "Eliminar",
|
||||||
|
"settings.hardware.cant_remove_active": "No se puede eliminar el modelo activo",
|
||||||
|
"settings.hardware.download": "Descargar",
|
||||||
|
"settings.hardware.diarization": "Diarización de hablantes",
|
||||||
|
"settings.hardware.diarization_hint": "Instala ambos modelos para que las grabaciones terminadas se separen por hablante (Hablante 1, Hablante 2, …) en lugar de una única transcripción continua. Se ejecuta totalmente sin conexión, una vez por reunión al detenerse. Sin ellos, cada línea se atribuye a un solo hablante.",
|
||||||
|
|
||||||
|
"settings.storage.title": "Almacenamiento",
|
||||||
|
"settings.storage.location_label": "Ubicación",
|
||||||
|
"settings.storage.location_hint": "Cambiar la ubicación de almacenamiento aún no es compatible — aquí es donde residen actualmente las reuniones, los modelos y la base de datos.",
|
||||||
|
"settings.storage.retention_title": "Retención",
|
||||||
|
"settings.storage.retention_hint": "Elimina automáticamente las reuniones más antiguas que superen un límite. Se comprueba una vez al inicio; vacío significa sin límite. Nunca afecta a una reunión que se está grabando.",
|
||||||
|
"settings.storage.max_age": "Antigüedad máxima (días)",
|
||||||
|
"settings.storage.max_size": "Tamaño máximo (GB)",
|
||||||
|
"settings.storage.no_limit": "sin límite",
|
||||||
|
"settings.storage.export_import_title": "Exportar e importar",
|
||||||
|
"settings.storage.export_hint_1": "Mueve grabaciones entre ordenadores como carpetas de paquete portátiles (audio, transcripción, notas, resumen y un",
|
||||||
|
"settings.storage.export_hint_2": "manifiesto). Exporta a cualquier carpeta — una unidad sincronizada, una memoria USB o el punto de montaje local de un destino de sincronización — y luego impórtalo en la otra máquina. Las reuniones importadas obtienen un id nuevo, por lo que reimportar nunca sobrescribe nada.",
|
||||||
|
"settings.storage.export_dialog_title": "Exportar todas las reuniones a…",
|
||||||
|
"settings.storage.import_dialog_title": "Importar un paquete (o una carpeta de paquetes)…",
|
||||||
|
"settings.storage.exporting": "Exportando…",
|
||||||
|
"settings.storage.export_all": "Exportar todas las reuniones…",
|
||||||
|
"settings.storage.importing": "Importando…",
|
||||||
|
"settings.storage.import_all": "Importar reuniones…",
|
||||||
|
"settings.storage.exported_one": "{n} reunión exportada.",
|
||||||
|
"settings.storage.exported_many": "{n} reuniones exportadas.",
|
||||||
|
"settings.storage.imported_one": "{n} reunión importada.",
|
||||||
|
"settings.storage.imported_many": "{n} reuniones importadas.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "Calendario y Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "Importa eventos y asistentes desde una copia de seguridad local de Outlook",
|
||||||
|
"settings.calendar.intro_2": "— solo lectura, no se escribe nada de vuelta en el archivo. Nada sale de este dispositivo.",
|
||||||
|
"settings.calendar.pst_file": "Archivo .pst",
|
||||||
|
"settings.calendar.no_file": "Ningún archivo seleccionado",
|
||||||
|
"settings.calendar.browse": "Examinar…",
|
||||||
|
"settings.calendar.password": "Contraseña",
|
||||||
|
"settings.calendar.password_note": "raramente necesaria",
|
||||||
|
"settings.calendar.optional": "opcional",
|
||||||
|
"settings.calendar.import_range": "Rango de importación",
|
||||||
|
"settings.calendar.range_30": "Últimos 30 días",
|
||||||
|
"settings.calendar.range_90": "Últimos 90 días",
|
||||||
|
"settings.calendar.range_180": "Últimos 6 meses",
|
||||||
|
"settings.calendar.range_365": "Último año",
|
||||||
|
"settings.calendar.range_all": "Todo el tiempo",
|
||||||
|
"settings.calendar.range_hint": "Un buzón de larga duración puede contener años de entradas periódicas/festivas — reducir el rango mantiene el calendario importado en lo que realmente es relevante. Se aplica tanto a este botón de importar como a la reimportación automática al iniciar.",
|
||||||
|
"settings.calendar.importing": "Importando…",
|
||||||
|
"settings.calendar.import": "Importar",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} eventos",
|
||||||
|
"settings.calendar.auto_reimport": "Reimportar este archivo automáticamente al iniciar",
|
||||||
|
"settings.calendar.auto_reimport_hint": "Se ejecuta una vez al inicio, no con un temporizador — la reimportación se puede repetir sin problemas (los eventos existentes se cotejan y actualizan, no se duplican).",
|
||||||
|
"settings.calendar.import_failed": "Error de importación: {error} — el archivo en sí no se toca; comprueba la ruta e inténtalo de nuevo.",
|
||||||
|
"settings.calendar.cleanup_title": "Limpiar",
|
||||||
|
"settings.calendar.cleanup_hint": "Los eventos ya vinculados a una reunión grabada se conservan siempre, sin importar la opción elegida abajo.",
|
||||||
|
"settings.calendar.cleanup_30": "Más antiguos de 30 días",
|
||||||
|
"settings.calendar.cleanup_90": "Más antiguos de 90 días",
|
||||||
|
"settings.calendar.cleanup_180": "Más antiguos de 6 meses",
|
||||||
|
"settings.calendar.cleanup_365": "Más antiguos de 1 año",
|
||||||
|
"settings.calendar.cleanup_all": "Eliminar todo",
|
||||||
|
"settings.calendar.cleaning": "Limpiando…",
|
||||||
|
"settings.calendar.cleanup_btn": "Limpiar calendario",
|
||||||
|
"settings.calendar.cleanup_result": "{deleted}{extra} eliminados.",
|
||||||
|
"settings.calendar.cleanup_kept": ", {n} conservados (vinculados a una reunión)",
|
||||||
|
"settings.calendar.cleanup_failed": "Error al limpiar: {error}",
|
||||||
|
"settings.calendar.imported_events": "Eventos importados",
|
||||||
|
"settings.calendar.no_events": "Aún no se han importado eventos.",
|
||||||
|
"settings.calendar.search_title": "Buscar título",
|
||||||
|
"settings.calendar.search_placeholder": "Nombre de la reunión…",
|
||||||
|
"settings.calendar.date_label": "Fecha",
|
||||||
|
"settings.calendar.events_count": "{shown} de {total} eventos",
|
||||||
|
"settings.calendar.untitled": "(sin título)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "Ajustes",
|
||||||
|
"settings.close": "Cerrar",
|
||||||
|
"settings.close_title": "Cerrar (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "Modo de vista previa — el backend de sincronización aún no está implementado (Fase 9). Los cambios se mantienen solo en la interfaz.",
|
||||||
|
"settings.sync.title": "Sincronización y subida",
|
||||||
|
"settings.sync.enable_label": "Habilitar la subida de artefactos de reuniones a los destinos configurados",
|
||||||
|
"settings.sync.enable_hint": "Desactivado por defecto. No se sube nada a menos que esto esté activado y haya un destino habilitado. Los destinos autoalojados mantienen los datos en tu propio servidor; las nubes de terceros están claramente etiquetadas.",
|
||||||
|
"settings.sync.targets_title": "Destinos",
|
||||||
|
"settings.sync.no_targets": "Aún no hay destinos. Añade uno abajo.",
|
||||||
|
"settings.sync.third_party": "terceros",
|
||||||
|
"settings.sync.your_server": "tu servidor",
|
||||||
|
"settings.sync.edit": "Editar",
|
||||||
|
"settings.sync.remove": "Eliminar",
|
||||||
|
"settings.sync.edit_target": "Editar destino",
|
||||||
|
"settings.sync.add_target": "Añadir un destino",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV cubre Nextcloud, ownCloud, Cloudreve, Seafile y Synology. (Seafile: habilita SeafDAV en el servidor.)",
|
||||||
|
"settings.sync.name": "Nombre",
|
||||||
|
"settings.sync.name_placeholder": "Nextcloud de casa",
|
||||||
|
"settings.sync.provider": "Proveedor",
|
||||||
|
"settings.sync.server_url": "URL del servidor",
|
||||||
|
"settings.sync.server_url_note_1": "Solo la URL de tu servidor — WhispAssist añade",
|
||||||
|
"settings.sync.server_url_note_2": "automáticamente.",
|
||||||
|
"settings.sync.remote_folder": "Carpeta remota",
|
||||||
|
"settings.sync.username": "Nombre de usuario",
|
||||||
|
"settings.sync.app_password": "Contraseña de aplicación",
|
||||||
|
"settings.sync.pw_keep": "déjalo en blanco para mantener la contraseña actual",
|
||||||
|
"settings.sync.pw_store": "almacenada en el gestor de credenciales del sistema",
|
||||||
|
"settings.sync.upload_legend": "Subir",
|
||||||
|
"settings.sync.artifact_transcript": "transcripción",
|
||||||
|
"settings.sync.artifact_notes": "notas",
|
||||||
|
"settings.sync.artifact_summary": "resumen",
|
||||||
|
"settings.sync.artifact_recording": "grabación (.wav, si se conserva)",
|
||||||
|
"settings.sync.allow_plaintext": "Permitir http en texto plano para una dirección LAN (no recomendado)",
|
||||||
|
"settings.sync.encrypt_before": "Cifrar antes de subir (el destino almacena solo texto cifrado)",
|
||||||
|
"settings.sync.requires_vault": "— requiere una bóveda desbloqueada",
|
||||||
|
"settings.sync.test_connection": "Probar conexión",
|
||||||
|
"settings.sync.save_changes": "Guardar cambios",
|
||||||
|
"settings.sync.add_target_btn": "Añadir destino",
|
||||||
|
"settings.sync.cancel": "Cancelar",
|
||||||
|
"settings.sync.third_party_banner": "{kind} es una nube de terceros — subir envía tus datos fuera de tu dispositivo a {kind}.",
|
||||||
|
"settings.sync.link_account": "Vincular cuenta de {kind}…",
|
||||||
|
"settings.sync.oauth_hint": "Abre un inicio de sesión OAuth (redirección loopback); el token se almacena en el gestor de credenciales del sistema.",
|
||||||
|
"settings.sync.footnote": "Las credenciales nunca se escriben en los ajustes ni en la base de datos — solo en el gestor de credenciales del sistema. TLS es obligatorio para destinos que no sean de la LAN.",
|
||||||
|
|
||||||
|
"settings.ai.title": "Proveedor de resúmenes de IA",
|
||||||
|
"settings.ai.intro_1": "Los resúmenes se ejecutan en un LLM local por defecto. Apunta esto a Ollama en este PC o en otra máquina de tu LAN (p. ej.",
|
||||||
|
"settings.ai.intro_2": "— ambos cuentan como local, así que nada sale de tu red. Los proveedores alojados (Anthropic) son opcionales, están desactivados por defecto y envían la transcripción a un tercero en cuanto activas uno.",
|
||||||
|
"settings.ai.provider": "Proveedor",
|
||||||
|
"settings.ai.off": "Desactivado",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (local / LAN)",
|
||||||
|
"settings.ai.provider_custom": "Personalizado (compatible con OpenAI)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — alojado, sale de este dispositivo",
|
||||||
|
"settings.ai.endpoint": "Endpoint",
|
||||||
|
"settings.ai.model": "Modelo",
|
||||||
|
"settings.ai.api_key": "Clave API",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (ya configurada — déjalo en blanco para mantenerla)",
|
||||||
|
"settings.ai.show_api_key": "Mostrar clave API",
|
||||||
|
"settings.ai.hide_api_key": "Ocultar clave API",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic es un servicio alojado de terceros — la transcripción de esta reunión sale de tu dispositivo cuando generas un resumen.",
|
||||||
|
"settings.ai.endpoint_banner": "Este endpoint no está en tu máquina ni en tu LAN — tu transcripción saldría de tu red.",
|
||||||
|
"settings.ai.saving": "Guardando…",
|
||||||
|
"settings.ai.save": "Guardar",
|
||||||
|
"settings.ai.test_connection": "Probar conexión",
|
||||||
|
"settings.ai.this_hosted_provider": "este proveedor alojado",
|
||||||
|
"settings.ai.reachable": "Accesible",
|
||||||
|
"settings.ai.unreachable": "Inaccesible",
|
||||||
|
"settings.ai.model_count_one": "· {n} modelo",
|
||||||
|
"settings.ai.model_count_many": "· {n} modelos",
|
||||||
|
"settings.ai.on_lan": "· en tu máquina / LAN",
|
||||||
|
"settings.ai.leaves_network": "· sale de tu red",
|
||||||
|
"settings.ai.advanced_title": "Configuración avanzada de Ollama",
|
||||||
|
"settings.ai.changed": "{n} modificados",
|
||||||
|
"settings.ai.reset_all": "Restablecer todo a los valores predeterminados",
|
||||||
|
"settings.ai.system_prompt": "Prompt del sistema",
|
||||||
|
"settings.ai.system_prompt_placeholder": "p. ej. Eres un resumidor de reuniones conciso.",
|
||||||
|
"settings.ai.system_prompt_hint": "Se añade antes del formato de salida requerido por WhispAssist, para que tus instrucciones no puedan romper el análisis del resumen/tareas.",
|
||||||
|
"settings.ai.think": "Razonar",
|
||||||
|
"settings.ai.think_help": "Esfuerzo de razonamiento (solo modelos de razonamiento).",
|
||||||
|
"settings.ai.keep_alive": "Mantener activo",
|
||||||
|
"settings.ai.keep_alive_help": "Cuánto tiempo permanece el modelo en la RAM. p. ej. 5m, 1h, 0 (descargar), -1 (siempre).",
|
||||||
|
"settings.ai.runtime_hardware": "Ejecución y hardware",
|
||||||
|
"settings.ai.rarely_needed": "— raramente necesario",
|
||||||
|
"settings.ai.save_advanced": "Guardar avanzado",
|
||||||
|
"settings.ai.saved": "Guardado",
|
||||||
|
"settings.ai.turn_off": "Desactivar",
|
||||||
|
"settings.ai.default_value": "Predeterminado {value}.",
|
||||||
|
"settings.ai.reset_default": "Restablecer al valor predeterminado",
|
||||||
|
"settings.ai.reset_field": "Restablecer {name}",
|
||||||
|
|
||||||
|
"settings.mcp.title": "Servidor MCP",
|
||||||
|
"settings.mcp.banner_1": "Esto permite que tu propio agente de programación (Claude Code, Codex, Copilot, OpenCode, …) obtenga el contexto de la reunión en tu máquina local. Una vez conectado,",
|
||||||
|
"settings.mcp.banner_agent": "ese agente",
|
||||||
|
"settings.mcp.banner_2": "puede reenviar lo que lee a la nube de su propio proveedor de modelos — fuera del control de WhispAssist. WhispAssist en sí nunca envía estos datos a ningún sitio; el servidor solo escucha en este dispositivo (",
|
||||||
|
"settings.mcp.banner_3": ") y cada lectura se registra abajo.",
|
||||||
|
"settings.mcp.enable": "Habilitar el servidor MCP",
|
||||||
|
"settings.mcp.enable_hint": "Desactivado por defecto. Solo loopback, protegido con token — nada es accesible desde la red.",
|
||||||
|
"settings.mcp.transport": "Transporte",
|
||||||
|
"settings.mcp.transport_http": "HTTP en streaming",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (el agente genera un proceso)",
|
||||||
|
"settings.mcp.port": "Puerto",
|
||||||
|
"settings.mcp.transport_change_hint": "Para cambiar el transporte/puerto, apaga primero el servidor y vuelve a encenderlo.",
|
||||||
|
"settings.mcp.scope_title": "Alcance",
|
||||||
|
"settings.mcp.expose": "Exponer",
|
||||||
|
"settings.mcp.scope_none": "Ninguno — no se comparte nada",
|
||||||
|
"settings.mcp.scope_selected": "Seleccionados — solo los resúmenes de funciones que hayas marcado como compartidos",
|
||||||
|
"settings.mcp.scope_all": "Todo — reuniones, transcripciones, tareas y resúmenes compartidos",
|
||||||
|
"settings.mcp.expose_recordings": "Permitir también reuniones con una grabación guardada (desactivado por defecto — la transcripción de una reunión grabada se retiene incluso en el alcance «Todo» hasta que esto esté activado)",
|
||||||
|
"settings.mcp.token_title": "Nuevo token de autenticación — se muestra una vez, cópialo ahora",
|
||||||
|
"settings.mcp.token_hint": "Esto no se volverá a mostrar. Se almacena en el gestor de credenciales del sistema; si lo pierdes, apaga el servidor y vuelve a encenderlo para generar uno nuevo.",
|
||||||
|
"settings.mcp.copied": "Copiado",
|
||||||
|
"settings.mcp.copy": "Copiar",
|
||||||
|
"settings.mcp.endpoint": "Endpoint",
|
||||||
|
"settings.mcp.endpoint_hint": "Apunta la configuración del cliente MCP de tu agente a {kind}, con el token de arriba como credencial bearer.",
|
||||||
|
"settings.mcp.kind_command": "comando",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "Registro de accesos",
|
||||||
|
"settings.mcp.access_log_hint": "Cada lectura de herramienta que hace un agente, permitida o denegada (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "Ningún agente ha leído nada aún.",
|
||||||
|
"settings.mcp.log_meeting": "reunión {id}",
|
||||||
|
"settings.mcp.refresh": "Actualizar",
|
||||||
|
|
||||||
|
"settings.privacy.title": "Privacidad",
|
||||||
|
"settings.privacy.intro": "Lo que WhispAssist puede enviar realmente fuera de este dispositivo ahora mismo. Con todo desactivado (lo predeterminado), nada sale del dispositivo.",
|
||||||
|
"settings.privacy.llm_endpoint": "Endpoint del LLM",
|
||||||
|
"settings.privacy.off": "desactivado",
|
||||||
|
"settings.privacy.local_only": "solo local",
|
||||||
|
"settings.privacy.leaves_device": "sale de este dispositivo",
|
||||||
|
"settings.privacy.sync_label": "Sincronización",
|
||||||
|
"settings.privacy.enabled": "habilitada",
|
||||||
|
"settings.privacy.mcp_label": "Servidor MCP",
|
||||||
|
"settings.privacy.mcp_on": "activado · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "Entrante solo en loopback — no añade nada a la lista de salida de arriba. Un agente conectado aún puede reenviar lo que lee a su propio proveedor de modelos; consulta la pestaña del servidor MCP.",
|
||||||
|
"settings.privacy.egress_title": "Lista de permitidos de salida",
|
||||||
|
"settings.privacy.no_egress": "No hay hosts en la lista de permitidos — WA no realiza ninguna salida de contenido.",
|
||||||
|
"settings.privacy.sync_targets_title": "Destinos de sincronización",
|
||||||
|
"settings.privacy.third_party": "terceros",
|
||||||
|
"settings.privacy.your_server": "tu servidor",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "sin TLS",
|
||||||
|
"settings.privacy.refresh": "Actualizar",
|
||||||
|
"settings.privacy.unavailable": "Autocomprobación de privacidad no disponible.",
|
||||||
|
"settings.privacy.vault_title": "Bóveda de cifrado",
|
||||||
|
"settings.privacy.vault_intro": "Cifra notas, transcripciones y resúmenes en reposo con una contraseña. (Los archivos de audio aún no se cifran.)",
|
||||||
|
"settings.privacy.vault_password": "Contraseña de la bóveda",
|
||||||
|
"settings.privacy.enable_vault": "Habilitar bóveda",
|
||||||
|
"settings.privacy.vault_pw_hint": "Usa al menos 8 caracteres. Si la olvidas, el contenido cifrado no se podrá recuperar.",
|
||||||
|
"settings.privacy.vault_locked_1": "La bóveda está ",
|
||||||
|
"settings.privacy.locked_word": "bloqueada",
|
||||||
|
"settings.privacy.vault_locked_2": ". Desbloquéala para leer las reuniones cifradas.",
|
||||||
|
"settings.privacy.password": "Contraseña",
|
||||||
|
"settings.privacy.unlock": "Desbloquear",
|
||||||
|
"settings.privacy.vault_unlocked_1": "La bóveda está ",
|
||||||
|
"settings.privacy.unlocked_word": "desbloqueada",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". Las nuevas notas, transcripciones y resúmenes se cifran en reposo.",
|
||||||
|
"settings.privacy.lock_now": "Bloquear ahora",
|
||||||
|
"settings.privacy.change_password": "Cambiar contraseña",
|
||||||
|
"settings.privacy.current_password": "Contraseña actual",
|
||||||
|
"settings.privacy.new_password": "Nueva contraseña",
|
||||||
|
|
||||||
|
"settings.about.title": "Acerca de",
|
||||||
|
"settings.about.tagline": "Un asistente de reuniones totalmente local, de código abierto y nativo de Windows.",
|
||||||
|
"settings.about.build_commit": "Commit de compilación",
|
||||||
|
|
||||||
|
"consent.heading": "Antes de grabar",
|
||||||
|
"consent.body": "Grabar conversaciones sin el consentimiento de los participantes puede ser ilegal en tu región. Consulta las leyes locales sobre grabación. Esto es una advertencia, no asesoramiento legal.",
|
||||||
|
"consent.accept": "Entiendo — habilitar la grabación",
|
||||||
|
"consent.cancel": "Cancelar",
|
||||||
|
|
||||||
|
"hosted.heading": "Antes de usar {provider}",
|
||||||
|
"hosted.body_1": "Generar con",
|
||||||
|
"hosted.body_2": "envía la transcripción de esta reunión a sus servidores — sale de este dispositivo y queda sujeta a su política de privacidad. WhispAssist no tiene control sobre el tratamiento de los datos una vez que salen de tu dispositivo. Esto está desactivado por defecto; lo estás eligiendo ahora.",
|
||||||
|
"hosted.accept": "Entiendo — continuar",
|
||||||
|
"hosted.cancel": "Cancelar",
|
||||||
|
|
||||||
|
"theme.group": "Tema",
|
||||||
|
"theme.system": "Según el sistema",
|
||||||
|
"theme.light": "Claro",
|
||||||
|
"theme.dark": "Oscuro",
|
||||||
|
|
||||||
|
"import.dialog_label": "Añadir una reunión desde un archivo o URL",
|
||||||
|
"import.title": "Añadir una reunión",
|
||||||
|
"import.close": "Cerrar",
|
||||||
|
"import.close_title": "Cerrar (Esc)",
|
||||||
|
"import.body": "Importa una grabación existente — un archivo de audio/vídeo local o un enlace (YouTube, una página de streaming o una URL de medios directa). Se transcribe y se diariza igual que una grabación en directo.",
|
||||||
|
"import.file_or_url": "Archivo o URL",
|
||||||
|
"import.source_placeholder": "Pega una URL o busca un archivo…",
|
||||||
|
"import.choose_file": "Elegir un archivo local",
|
||||||
|
"import.browse": "Examinar…",
|
||||||
|
"import.title_label": "Título",
|
||||||
|
"import.optional": "opcional",
|
||||||
|
"import.title_placeholder": "Por defecto, el nombre del archivo",
|
||||||
|
"import.filter_av": "Audio / vídeo",
|
||||||
|
"import.requires_1": "Requiere",
|
||||||
|
"import.requires_2": "instalado y en tu PATH (además de",
|
||||||
|
"import.requires_3": "para las URL). WhispAssist no los incluye.",
|
||||||
|
"import.importing": "Importando… esto puede tardar un rato",
|
||||||
|
"import.import": "Importar",
|
||||||
|
"import.cancel": "Cancelar",
|
||||||
|
|
||||||
|
"tagchip.filter": "Filtrar reuniones etiquetadas con «{tag}»",
|
||||||
|
"tagchip.remove": "Eliminar etiqueta {tag}",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "Nivel de entrada del sistema y del micrófono",
|
||||||
|
"levelmeter.system": "Nivel de entrada del sistema",
|
||||||
|
|
||||||
|
"app.tagline": "local · privado",
|
||||||
|
"app.note_template": "Plantilla de notas",
|
||||||
|
"app.no_template": "Sin plantilla",
|
||||||
|
"app.add_meeting": "Añadir reunión",
|
||||||
|
"app.add_meeting_title": "Añadir una reunión desde un archivo o URL",
|
||||||
|
"app.record": "Grabar",
|
||||||
|
"app.record_title": "Iniciar grabación (Ctrl+Mayús+R)",
|
||||||
|
"app.stop": "Detener",
|
||||||
|
"app.stop_title": "Detener grabación (Ctrl+Mayús+R)",
|
||||||
|
"app.cancel": "Cancelar",
|
||||||
|
"app.cancel_title": "Descartar esta grabación y eliminarla",
|
||||||
|
"app.recording": "Grabando…",
|
||||||
|
"app.backend_title": "Backend de transcripción activo",
|
||||||
|
"app.retention_title": "Guardar el audio como .wav para esta reunión",
|
||||||
|
"app.saving": "guardando",
|
||||||
|
"app.not_saved": "no guardado",
|
||||||
|
"app.reconnecting": "reconectando el dispositivo de audio…",
|
||||||
|
"app.settings": "Ajustes",
|
||||||
|
"app.settings_title": "Ajustes (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "¿Descartar esta grabación? Se eliminarán su audio y su transcripción.",
|
||||||
|
"app.announce_started": "Grabación iniciada",
|
||||||
|
"app.announce_paused": "Grabación en pausa",
|
||||||
|
"app.announce_stopped": "Grabación detenida",
|
||||||
|
"app.consent_dialog": "Consentimiento de grabación",
|
||||||
|
"app.vault_dialog": "Desbloquear la bóveda de cifrado",
|
||||||
|
"app.vault_title": "Desbloquear la bóveda de cifrado",
|
||||||
|
"app.vault_desc": "Tus reuniones están cifradas en reposo. Introduce tu contraseña para leerlas.",
|
||||||
|
"app.vault_password": "Contraseña de la bóveda",
|
||||||
|
"app.vault_incorrect": "Contraseña incorrecta",
|
||||||
|
"app.unlock": "Desbloquear",
|
||||||
|
"app.continue_locked": "Continuar bloqueado",
|
||||||
|
"app.show_meetings": "Mostrar lista de reuniones",
|
||||||
|
"app.hide_meetings": "Ocultar lista de reuniones",
|
||||||
|
"app.show_summary": "Mostrar panel de resumen",
|
||||||
|
"app.hide_summary": "Ocultar panel de resumen",
|
||||||
|
"app.resize_meetings": "Redimensionar lista de reuniones",
|
||||||
|
"app.resize_summary": "Redimensionar panel de resumen",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "Buscar reuniones…",
|
||||||
|
"meetings.search_aria": "Buscar reuniones",
|
||||||
|
"meetings.filter_tag_aria": "Filtrar por etiqueta",
|
||||||
|
"meetings.all_tags": "Todas las etiquetas",
|
||||||
|
"meetings.from_date_aria": "Desde la fecha",
|
||||||
|
"meetings.to_date_aria": "Hasta la fecha",
|
||||||
|
"meetings.bulk_format_aria": "Formato de exportación masiva",
|
||||||
|
"meetings.bulk_export": "Exportación masiva",
|
||||||
|
"meetings.bulk_export_title": "Exportar todas las reuniones que coincidan con los filtros de etiqueta/fecha de arriba",
|
||||||
|
"meetings.exporting": "Exportando…",
|
||||||
|
"meetings.exported_one": "{n} reunión exportada.",
|
||||||
|
"meetings.exported_many": "{n} reuniones exportadas.",
|
||||||
|
"meetings.searching": "Buscando…",
|
||||||
|
"meetings.loading": "Cargando reuniones…",
|
||||||
|
"meetings.no_matches": "Sin coincidencias.",
|
||||||
|
"meetings.empty": "Aún no hay reuniones — haz clic en Grabar arriba para empezar.",
|
||||||
|
"meetings.status.recording": "grabando",
|
||||||
|
"meetings.status.transcribing": "transcribiendo",
|
||||||
|
"meetings.status.recovering": "recuperando",
|
||||||
|
"meetings.status.error": "error",
|
||||||
|
"meetings.resume": "Reanudar transcripción",
|
||||||
|
"meetings.delete_aria": "Eliminar reunión",
|
||||||
|
"meetings.delete_confirm": "¿Eliminar esta reunión? Se eliminarán su grabación, transcripción y notas.",
|
||||||
|
|
||||||
|
"transcript.heading": "Transcripción",
|
||||||
|
"transcript.title_aria": "Título de la reunión",
|
||||||
|
"transcript.lang_title": "Idioma de transcripción",
|
||||||
|
"transcript.lang_auto": "detectando…",
|
||||||
|
"transcript.show": "Mostrar transcripción",
|
||||||
|
"transcript.hide": "Ocultar transcripción",
|
||||||
|
"transcript.empty": "No hay transcripción para esta reunión.",
|
||||||
|
"transcript.will_appear": "La transcripción aparecerá aquí mientras grabas.",
|
||||||
|
"transcript.play_from_here": "Reproducir desde aquí",
|
||||||
|
"transcript.resize": "Redimensionar transcripción y notas",
|
||||||
|
"transcript.reprocess_placeholder": "Volver a transcribir con…",
|
||||||
|
"transcript.reprocess_lang_aria": "Idioma de reprocesamiento",
|
||||||
|
"transcript.reprocess_keep_lang": "Mantener el idioma actual",
|
||||||
|
"transcript.reprocess_go": "Ir",
|
||||||
|
"transcript.retranscribing": "Volviendo a transcribir…",
|
||||||
|
"transcript.has_note": "Tiene una nota",
|
||||||
|
"transcript.note_placeholder": "Añade una nota para este momento…",
|
||||||
|
"transcript.note_aria": "Nota para esta línea de la transcripción",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Documento de Word",
|
||||||
|
"transcript.filter_obsidian": "Nota de Obsidian",
|
||||||
|
|
||||||
|
"notes.heading": "Notas",
|
||||||
|
"notes.show": "Mostrar notas",
|
||||||
|
"notes.hide": "Ocultar notas",
|
||||||
|
"notes.placeholder": "Notas…",
|
||||||
|
"notes.live_placeholder": "Escribe notas mientras hablas…",
|
||||||
|
"notes.toolbar_aria": "Formato de notas",
|
||||||
|
"notes.bold": "Negrita",
|
||||||
|
"notes.italic": "Cursiva",
|
||||||
|
"notes.h1": "Encabezado 1",
|
||||||
|
"notes.h2": "Encabezado 2",
|
||||||
|
"notes.bullet": "Lista con viñetas",
|
||||||
|
"notes.checkbox_title": "Casilla de verificación",
|
||||||
|
"notes.checkbox_aria": "Elemento de lista con casilla",
|
||||||
|
"notes.edit_raw": "Editar el markdown en bruto",
|
||||||
|
"notes.render": "Renderizar el markdown",
|
||||||
|
"notes.editor": "Editor",
|
||||||
|
"notes.preview": "Vista previa",
|
||||||
|
"notes.export_md_title": "Exportar notas como .md",
|
||||||
|
"notes.export_pdf_title": "Exportar notas como .pdf",
|
||||||
|
"notes.export_docx_title": "Exportar notas como .docx",
|
||||||
|
"notes.export_bundle_title": "Exportar audio + transcripción + notas a una carpeta",
|
||||||
|
"notes.export_obsidian_title": "Exportar una nota de Obsidian (notas + resumen + transcripción, sin audio)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "Grabación",
|
||||||
|
"summary.loading_recording": "Cargando grabación…",
|
||||||
|
"summary.sync_heading": "Sincronización",
|
||||||
|
"summary.uploading": "Subiendo…",
|
||||||
|
"summary.upload_now": "Subir ahora",
|
||||||
|
"summary.no_uploads": "Aún no hay subidas para esta reunión.",
|
||||||
|
"summary.retry": "reintentar",
|
||||||
|
"summary.tags_heading": "Etiquetas",
|
||||||
|
"summary.tags_select": "Selecciona una reunión para etiquetarla.",
|
||||||
|
"summary.tag_add_placeholder": "Añadir etiqueta…",
|
||||||
|
"summary.tag_first_placeholder": "proyecto, cliente, tema…",
|
||||||
|
"summary.generating": "Generando…",
|
||||||
|
"summary.generate_tags": "Generar etiquetas",
|
||||||
|
"summary.saving": "Guardando…",
|
||||||
|
"summary.save_tags": "Guardar etiquetas",
|
||||||
|
"summary.summary_heading": "Resumen",
|
||||||
|
"summary.ai_provider": "Proveedor de IA",
|
||||||
|
"summary.provider_off": "Desactivado",
|
||||||
|
"summary.provider_custom": "Personalizado",
|
||||||
|
"summary.local": "local",
|
||||||
|
"summary.leaves_device": "sale de este dispositivo",
|
||||||
|
"summary.this_hosted_provider": "este proveedor alojado",
|
||||||
|
"summary.select_generate": "Selecciona una reunión para generar un resumen.",
|
||||||
|
"summary.decisions_heading": "Decisiones",
|
||||||
|
"summary.regenerate": "Regenerar",
|
||||||
|
"summary.generated_hint": "Generado localmente tras la reunión (requiere un proveedor de LLM local).",
|
||||||
|
"summary.no_provider": "No hay proveedor de IA configurado — habilita uno en Ajustes.",
|
||||||
|
"summary.generate_summary": "Generar resumen",
|
||||||
|
"summary.briefs_heading": "Resúmenes de funciones",
|
||||||
|
"summary.briefs_select": "Selecciona una reunión para crear o ver resúmenes de funciones.",
|
||||||
|
"summary.briefs_desc": "Destila esta reunión en una especificación lista para agentes (requiere un proveedor de LLM local) — entrégala a un agente de programación o sírvela a través del servidor MCP una vez que esté activado.",
|
||||||
|
"summary.brief_repo_placeholder": "Repo de destino (opcional), p. ej. acme/reporting-web",
|
||||||
|
"summary.distilling": "Destilando…",
|
||||||
|
"summary.create_brief": "Crear resumen de función",
|
||||||
|
"summary.brief_mcp_title": "Disponible cuando el servidor MCP está activado",
|
||||||
|
"summary.copied": "Copiado",
|
||||||
|
"summary.copy_md": "Copiar como Markdown",
|
||||||
|
"summary.copy_json": "Copiar como JSON",
|
||||||
|
"summary.brief_problem": "Problema",
|
||||||
|
"summary.brief_outcome": "Resultado deseado",
|
||||||
|
"summary.brief_criteria": "Criterios de aceptación",
|
||||||
|
"summary.brief_none": "Nada capturado.",
|
||||||
|
"summary.brief_context": "Contexto",
|
||||||
|
"summary.action_items_heading": "Tareas",
|
||||||
|
"summary.ai_select": "Selecciona una reunión para ver sus tareas.",
|
||||||
|
"summary.ai_empty": "Aún no hay ninguna — añade una abajo o genera un resumen para extraerlas automáticamente.",
|
||||||
|
"summary.confirmed": "Confirmada",
|
||||||
|
"summary.ai_text_placeholder": "Tarea…",
|
||||||
|
"summary.ai_text_aria": "Texto de la tarea",
|
||||||
|
"summary.owner": "Responsable",
|
||||||
|
"summary.due_date": "Fecha límite",
|
||||||
|
"summary.reminder_title": "Programar un recordatorio local para esta fecha límite",
|
||||||
|
"summary.ai_delete_title": "Eliminar esta tarea",
|
||||||
|
"summary.ai_delete_aria": "Eliminar tarea",
|
||||||
|
"summary.add_action_item": "Añadir tarea",
|
||||||
|
"summary.save_action_items": "Guardar tareas",
|
||||||
|
"summary.calendar_heading": "Evento del calendario",
|
||||||
|
"summary.cal_select": "Selecciona una reunión para vincularla a un evento del calendario.",
|
||||||
|
"summary.event_search_placeholder": "Buscar título del evento…",
|
||||||
|
"summary.linked_event": "Evento vinculado",
|
||||||
|
"summary.change_event": "Cambiar evento…",
|
||||||
|
"summary.link_event": "Vincular un evento…",
|
||||||
|
"summary.untitled_event": "(sin título)",
|
||||||
|
"summary.no_events_1": "Aún no se han importado eventos — importa uno",
|
||||||
|
"summary.no_events_2": "desde Ajustes → Calendario.",
|
||||||
|
"summary.no_events_match": "Ningún evento coincide con esta búsqueda/fecha — prueba a borrar una.",
|
||||||
|
"summary.organizer_label": "Organizador: {name}",
|
||||||
|
"summary.participants_heading": "Participantes",
|
||||||
|
"summary.participants_hint": "Se rellena a partir del evento del calendario vinculado.",
|
||||||
|
"summary.speakers_heading": "Hablantes",
|
||||||
|
"summary.speakers_select": "Selecciona una reunión para nombrar a sus hablantes.",
|
||||||
|
"summary.no_speakers": "Aún no se han detectado hablantes.",
|
||||||
|
"summary.speaker_name_placeholder": "Nombre del hablante",
|
||||||
|
"summary.save": "Guardar",
|
||||||
|
"summary.cancel": "Cancelar",
|
||||||
|
"summary.name_speaker": "Nombrar a este hablante…",
|
||||||
|
"summary.add_new_name": "+ Añadir nuevo nombre…",
|
||||||
|
|
||||||
|
"settings.recording.title": "Grabación",
|
||||||
|
"settings.recording.record_default": "Grabar las reuniones por defecto",
|
||||||
|
"settings.recording.save_audio_as": "guardar el audio como",
|
||||||
|
"settings.recording.default_hint": "Desactivado por defecto. Cuando está desactivado, el audio se usa solo para producir la transcripción y se elimina al finalizar la reunión. También puedes activar la grabación por reunión.",
|
||||||
|
"settings.recording.consent_label": "Consentimiento: {status}.",
|
||||||
|
"settings.recording.consent_ack": "reconocido",
|
||||||
|
"settings.recording.consent_not": "aún no reconocido",
|
||||||
|
"settings.recording.auto_label": "Iniciar la grabación automáticamente cuando comience un evento del calendario",
|
||||||
|
"settings.recording.auto_hint": "Solo mientras WhispAssist esté abierto. Cuando llega la hora de inicio de un evento del calendario importado, comienza una grabación automáticamente (usando tu ajuste de retención predeterminado de arriba). Nada se ejecuta en segundo plano — el temporizador solo se arma mientras la aplicación está en ejecución. Importa eventos en Ajustes → Calendario."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "Grabación",
|
||||||
|
"nav.hardware": "Hardware",
|
||||||
|
"nav.storage": "Almacenamiento",
|
||||||
|
"nav.calendar": "Calendario",
|
||||||
|
"nav.sync": "Sincronización",
|
||||||
|
"nav.ai": "IA",
|
||||||
|
"nav.mcp": "Servidor MCP",
|
||||||
|
"nav.privacy": "Privacidad",
|
||||||
|
"nav.about": "Acerca de",
|
||||||
|
"nav.language": "Idioma",
|
||||||
|
|
||||||
|
"settings.language.title": "Idioma",
|
||||||
|
"settings.language.display": "Idioma de la interfaz",
|
||||||
|
"settings.language.display_hint": "El idioma de la interfaz de la aplicación. Agregar un idioma es tan sencillo como colocar un solo archivo de traducción.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "Idioma de transcripción",
|
||||||
|
"settings.transcription.label": "Idioma",
|
||||||
|
"settings.transcription.auto": "Detección automática",
|
||||||
|
"settings.transcription.applies_hint": "Se aplica a la próxima grabación. El idioma realmente utilizado se muestra después en cada reunión.",
|
||||||
|
"settings.transcription.english_only": "Solo inglés",
|
||||||
|
"settings.transcription.model_english_only": "«{model}» es solo en inglés.",
|
||||||
|
"settings.transcription.no_model": "Ningún modelo seleccionado.",
|
||||||
|
"settings.transcription.switch_multilingual": "Cambia a un modelo multilingüe arriba para elegir un idioma.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "Hardware",
|
||||||
|
"settings.hardware.active_backend": "Backend activo",
|
||||||
|
"settings.hardware.model_meta": "· modelo {size}",
|
||||||
|
"settings.hardware.preferred_backend": "Backend preferido",
|
||||||
|
"settings.hardware.auto_backend": "Automático (el mejor disponible)",
|
||||||
|
"settings.hardware.not_available": "no disponible",
|
||||||
|
"settings.hardware.fallback_hint": "Recurre automáticamente (NPU → NVIDIA → AMD → Intel → CPU) si el backend elegido no se carga.",
|
||||||
|
"settings.hardware.audio_devices": "Dispositivos de audio",
|
||||||
|
"settings.hardware.recording_device": "Dispositivo de grabación",
|
||||||
|
"settings.hardware.default_system_audio": "Audio del sistema predeterminado",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist graba lo que reproduce este dispositivo (loopback) — el otro lado de la llamada. Elige una salida específica si no quieres que siga el valor predeterminado del sistema de Windows.",
|
||||||
|
"settings.hardware.microphone": "Micrófono",
|
||||||
|
"settings.hardware.mic_off": "Desactivado — no capturar mi micrófono",
|
||||||
|
"settings.hardware.default_mic": "Micrófono predeterminado",
|
||||||
|
"settings.hardware.mic_hint": "Agrega tu propia voz a la transcripción en vivo para que se capturen ambos lados de la reunión. Permanece en tu dispositivo — no se sube nada. Elige «Desactivado» para transcribir solo el audio del sistema de arriba.",
|
||||||
|
"settings.hardware.npu_detected": "Acelerador NPU detectado",
|
||||||
|
"settings.hardware.ready": "Listo",
|
||||||
|
"settings.hardware.downloading": "Descargando…",
|
||||||
|
"settings.hardware.package_needed": "Se necesita un paquete",
|
||||||
|
"settings.hardware.npu_hint": "Tu NPU necesita un paquete de aceleración único (modelo Whisper ONNX{extra}). Se descarga en segundo plano; también puedes iniciarlo ahora.",
|
||||||
|
"settings.hardware.plus_openvino": " + entorno de ejecución OpenVINO",
|
||||||
|
"settings.hardware.download_npu": "Descargar paquete NPU",
|
||||||
|
"settings.hardware.dml_title": "Aceleración de GPU (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "Tu GPU puede acelerarse sin Vulkan mediante DirectML — un paquete único (modelo Whisper ONNX{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + entorno de ejecución DirectML",
|
||||||
|
"settings.hardware.download_dml": "Descargar paquete DirectML",
|
||||||
|
"settings.hardware.detection_unavailable": "Detección de hardware no disponible.",
|
||||||
|
"settings.hardware.low_overhead": "Preajuste de baja carga",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + modelo más pequeño, para uso con batería/en segundo plano",
|
||||||
|
"settings.hardware.models": "Modelos",
|
||||||
|
"settings.hardware.downloading_model": "Descargando {name}",
|
||||||
|
"settings.hardware.active_badge": "Activo",
|
||||||
|
"settings.hardware.installed": "Instalado",
|
||||||
|
"settings.hardware.use": "Usar",
|
||||||
|
"settings.hardware.remove": "Quitar",
|
||||||
|
"settings.hardware.cant_remove_active": "No se puede quitar el modelo activo",
|
||||||
|
"settings.hardware.download": "Descargar",
|
||||||
|
"settings.hardware.diarization": "Diarización de hablantes",
|
||||||
|
"settings.hardware.diarization_hint": "Instala ambos modelos para que las grabaciones terminadas se separen por hablante (Hablante 1, Hablante 2, …) en lugar de una sola transcripción continua. Se ejecuta totalmente sin conexión, una vez por reunión al detenerse. Sin ellos, cada línea se atribuye a un solo hablante.",
|
||||||
|
|
||||||
|
"settings.storage.title": "Almacenamiento",
|
||||||
|
"settings.storage.location_label": "Ubicación",
|
||||||
|
"settings.storage.location_hint": "Cambiar la ubicación de almacenamiento aún no es compatible — aquí es donde residen actualmente las reuniones, los modelos y la base de datos.",
|
||||||
|
"settings.storage.retention_title": "Retención",
|
||||||
|
"settings.storage.retention_hint": "Elimina automáticamente las reuniones más antiguas que superen un límite. Se comprueba una vez al inicio; vacío significa sin límite. Nunca afecta a una reunión que se está grabando.",
|
||||||
|
"settings.storage.max_age": "Antigüedad máxima (días)",
|
||||||
|
"settings.storage.max_size": "Tamaño máximo (GB)",
|
||||||
|
"settings.storage.no_limit": "sin límite",
|
||||||
|
"settings.storage.export_import_title": "Exportar e importar",
|
||||||
|
"settings.storage.export_hint_1": "Mueve grabaciones entre computadoras como carpetas de paquete portátiles (audio, transcripción, notas, resumen y un",
|
||||||
|
"settings.storage.export_hint_2": "manifiesto). Exporta a cualquier carpeta — una unidad sincronizada, una memoria USB o el punto de montaje local de un destino de sincronización — y luego impórtalo en la otra computadora. Las reuniones importadas obtienen un id nuevo, por lo que reimportar nunca sobrescribe nada.",
|
||||||
|
"settings.storage.export_dialog_title": "Exportar todas las reuniones a…",
|
||||||
|
"settings.storage.import_dialog_title": "Importar un paquete (o una carpeta de paquetes)…",
|
||||||
|
"settings.storage.exporting": "Exportando…",
|
||||||
|
"settings.storage.export_all": "Exportar todas las reuniones…",
|
||||||
|
"settings.storage.importing": "Importando…",
|
||||||
|
"settings.storage.import_all": "Importar reuniones…",
|
||||||
|
"settings.storage.exported_one": "{n} reunión exportada.",
|
||||||
|
"settings.storage.exported_many": "{n} reuniones exportadas.",
|
||||||
|
"settings.storage.imported_one": "{n} reunión importada.",
|
||||||
|
"settings.storage.imported_many": "{n} reuniones importadas.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "Calendario y Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "Importa eventos y asistentes desde un respaldo local de Outlook",
|
||||||
|
"settings.calendar.intro_2": "— solo lectura, no se escribe nada de vuelta en el archivo. Nada sale de este dispositivo.",
|
||||||
|
"settings.calendar.pst_file": "Archivo .pst",
|
||||||
|
"settings.calendar.no_file": "Ningún archivo seleccionado",
|
||||||
|
"settings.calendar.browse": "Examinar…",
|
||||||
|
"settings.calendar.password": "Contraseña",
|
||||||
|
"settings.calendar.password_note": "raramente necesaria",
|
||||||
|
"settings.calendar.optional": "opcional",
|
||||||
|
"settings.calendar.import_range": "Rango de importación",
|
||||||
|
"settings.calendar.range_30": "Últimos 30 días",
|
||||||
|
"settings.calendar.range_90": "Últimos 90 días",
|
||||||
|
"settings.calendar.range_180": "Últimos 6 meses",
|
||||||
|
"settings.calendar.range_365": "Último año",
|
||||||
|
"settings.calendar.range_all": "Todo el tiempo",
|
||||||
|
"settings.calendar.range_hint": "Un buzón de larga duración puede contener años de entradas periódicas/festivas — reducir el rango mantiene el calendario importado en lo que realmente es relevante. Se aplica tanto a este botón de importar como a la reimportación automática al iniciar.",
|
||||||
|
"settings.calendar.importing": "Importando…",
|
||||||
|
"settings.calendar.import": "Importar",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} eventos",
|
||||||
|
"settings.calendar.auto_reimport": "Reimportar este archivo automáticamente al iniciar",
|
||||||
|
"settings.calendar.auto_reimport_hint": "Se ejecuta una vez al inicio, no con un temporizador — la reimportación se puede repetir sin problemas (los eventos existentes se cotejan y actualizan, no se duplican).",
|
||||||
|
"settings.calendar.import_failed": "Error de importación: {error} — el archivo en sí no se toca; comprueba la ruta e inténtalo de nuevo.",
|
||||||
|
"settings.calendar.cleanup_title": "Limpiar",
|
||||||
|
"settings.calendar.cleanup_hint": "Los eventos ya vinculados a una reunión grabada se conservan siempre, sin importar la opción elegida abajo.",
|
||||||
|
"settings.calendar.cleanup_30": "Más antiguos de 30 días",
|
||||||
|
"settings.calendar.cleanup_90": "Más antiguos de 90 días",
|
||||||
|
"settings.calendar.cleanup_180": "Más antiguos de 6 meses",
|
||||||
|
"settings.calendar.cleanup_365": "Más antiguos de 1 año",
|
||||||
|
"settings.calendar.cleanup_all": "Eliminar todo",
|
||||||
|
"settings.calendar.cleaning": "Limpiando…",
|
||||||
|
"settings.calendar.cleanup_btn": "Limpiar calendario",
|
||||||
|
"settings.calendar.cleanup_result": "{deleted}{extra} eliminados.",
|
||||||
|
"settings.calendar.cleanup_kept": ", {n} conservados (vinculados a una reunión)",
|
||||||
|
"settings.calendar.cleanup_failed": "Error al limpiar: {error}",
|
||||||
|
"settings.calendar.imported_events": "Eventos importados",
|
||||||
|
"settings.calendar.no_events": "Aún no se han importado eventos.",
|
||||||
|
"settings.calendar.search_title": "Buscar título",
|
||||||
|
"settings.calendar.search_placeholder": "Nombre de la reunión…",
|
||||||
|
"settings.calendar.date_label": "Fecha",
|
||||||
|
"settings.calendar.events_count": "{shown} de {total} eventos",
|
||||||
|
"settings.calendar.untitled": "(sin título)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "Configuración",
|
||||||
|
"settings.close": "Cerrar",
|
||||||
|
"settings.close_title": "Cerrar (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "Modo de vista previa — el backend de sincronización aún no está implementado (Fase 9). Los cambios se mantienen solo en la interfaz.",
|
||||||
|
"settings.sync.title": "Sincronización y subida",
|
||||||
|
"settings.sync.enable_label": "Habilitar la subida de artefactos de reuniones a los destinos configurados",
|
||||||
|
"settings.sync.enable_hint": "Desactivado por defecto. No se sube nada a menos que esto esté activado y haya un destino habilitado. Los destinos autoalojados mantienen los datos en tu propio servidor; las nubes de terceros están claramente etiquetadas.",
|
||||||
|
"settings.sync.targets_title": "Destinos",
|
||||||
|
"settings.sync.no_targets": "Aún no hay destinos. Agrega uno abajo.",
|
||||||
|
"settings.sync.third_party": "terceros",
|
||||||
|
"settings.sync.your_server": "tu servidor",
|
||||||
|
"settings.sync.edit": "Editar",
|
||||||
|
"settings.sync.remove": "Quitar",
|
||||||
|
"settings.sync.edit_target": "Editar destino",
|
||||||
|
"settings.sync.add_target": "Agregar un destino",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV cubre Nextcloud, ownCloud, Cloudreve, Seafile y Synology. (Seafile: habilita SeafDAV en el servidor.)",
|
||||||
|
"settings.sync.name": "Nombre",
|
||||||
|
"settings.sync.name_placeholder": "Nextcloud de casa",
|
||||||
|
"settings.sync.provider": "Proveedor",
|
||||||
|
"settings.sync.server_url": "URL del servidor",
|
||||||
|
"settings.sync.server_url_note_1": "Solo la URL de tu servidor — WhispAssist agrega",
|
||||||
|
"settings.sync.server_url_note_2": "automáticamente.",
|
||||||
|
"settings.sync.remote_folder": "Carpeta remota",
|
||||||
|
"settings.sync.username": "Nombre de usuario",
|
||||||
|
"settings.sync.app_password": "Contraseña de aplicación",
|
||||||
|
"settings.sync.pw_keep": "déjalo en blanco para mantener la contraseña actual",
|
||||||
|
"settings.sync.pw_store": "almacenada en el administrador de credenciales del sistema",
|
||||||
|
"settings.sync.upload_legend": "Subir",
|
||||||
|
"settings.sync.artifact_transcript": "transcripción",
|
||||||
|
"settings.sync.artifact_notes": "notas",
|
||||||
|
"settings.sync.artifact_summary": "resumen",
|
||||||
|
"settings.sync.artifact_recording": "grabación (.wav, si se conserva)",
|
||||||
|
"settings.sync.allow_plaintext": "Permitir http en texto plano para una dirección LAN (no recomendado)",
|
||||||
|
"settings.sync.encrypt_before": "Cifrar antes de subir (el destino almacena solo texto cifrado)",
|
||||||
|
"settings.sync.requires_vault": "— requiere una bóveda desbloqueada",
|
||||||
|
"settings.sync.test_connection": "Probar conexión",
|
||||||
|
"settings.sync.save_changes": "Guardar cambios",
|
||||||
|
"settings.sync.add_target_btn": "Agregar destino",
|
||||||
|
"settings.sync.cancel": "Cancelar",
|
||||||
|
"settings.sync.third_party_banner": "{kind} es una nube de terceros — subir envía tus datos fuera de tu dispositivo a {kind}.",
|
||||||
|
"settings.sync.link_account": "Vincular cuenta de {kind}…",
|
||||||
|
"settings.sync.oauth_hint": "Abre un inicio de sesión OAuth (redirección loopback); el token se almacena en el administrador de credenciales del sistema.",
|
||||||
|
"settings.sync.footnote": "Las credenciales nunca se escriben en la configuración ni en la base de datos — solo en el administrador de credenciales del sistema. TLS es obligatorio para destinos que no sean de la LAN.",
|
||||||
|
|
||||||
|
"settings.ai.title": "Proveedor de resúmenes de IA",
|
||||||
|
"settings.ai.intro_1": "Los resúmenes se ejecutan en un LLM local por defecto. Apunta esto a Ollama en esta computadora o en otra máquina de tu LAN (p. ej.",
|
||||||
|
"settings.ai.intro_2": "— ambos cuentan como local, así que nada sale de tu red. Los proveedores alojados (Anthropic) son opcionales, están desactivados por defecto y envían la transcripción a un tercero en cuanto activas uno.",
|
||||||
|
"settings.ai.provider": "Proveedor",
|
||||||
|
"settings.ai.off": "Desactivado",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (local / LAN)",
|
||||||
|
"settings.ai.provider_custom": "Personalizado (compatible con OpenAI)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — alojado, sale de este dispositivo",
|
||||||
|
"settings.ai.endpoint": "Endpoint",
|
||||||
|
"settings.ai.model": "Modelo",
|
||||||
|
"settings.ai.api_key": "Clave API",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (ya configurada — déjalo en blanco para mantenerla)",
|
||||||
|
"settings.ai.show_api_key": "Mostrar clave API",
|
||||||
|
"settings.ai.hide_api_key": "Ocultar clave API",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic es un servicio alojado de terceros — la transcripción de esta reunión sale de tu dispositivo cuando generas un resumen.",
|
||||||
|
"settings.ai.endpoint_banner": "Este endpoint no está en tu máquina ni en tu LAN — tu transcripción saldría de tu red.",
|
||||||
|
"settings.ai.saving": "Guardando…",
|
||||||
|
"settings.ai.save": "Guardar",
|
||||||
|
"settings.ai.test_connection": "Probar conexión",
|
||||||
|
"settings.ai.this_hosted_provider": "este proveedor alojado",
|
||||||
|
"settings.ai.reachable": "Accesible",
|
||||||
|
"settings.ai.unreachable": "Inaccesible",
|
||||||
|
"settings.ai.model_count_one": "· {n} modelo",
|
||||||
|
"settings.ai.model_count_many": "· {n} modelos",
|
||||||
|
"settings.ai.on_lan": "· en tu máquina / LAN",
|
||||||
|
"settings.ai.leaves_network": "· sale de tu red",
|
||||||
|
"settings.ai.advanced_title": "Configuración avanzada de Ollama",
|
||||||
|
"settings.ai.changed": "{n} modificados",
|
||||||
|
"settings.ai.reset_all": "Restablecer todo a los valores predeterminados",
|
||||||
|
"settings.ai.system_prompt": "Prompt del sistema",
|
||||||
|
"settings.ai.system_prompt_placeholder": "p. ej. Eres un resumidor de reuniones conciso.",
|
||||||
|
"settings.ai.system_prompt_hint": "Se agrega antes del formato de salida requerido por WhispAssist, para que tus instrucciones no puedan romper el análisis del resumen/tareas.",
|
||||||
|
"settings.ai.think": "Razonar",
|
||||||
|
"settings.ai.think_help": "Esfuerzo de razonamiento (solo modelos de razonamiento).",
|
||||||
|
"settings.ai.keep_alive": "Mantener activo",
|
||||||
|
"settings.ai.keep_alive_help": "Cuánto tiempo permanece el modelo en la RAM. p. ej. 5m, 1h, 0 (descargar), -1 (siempre).",
|
||||||
|
"settings.ai.runtime_hardware": "Ejecución y hardware",
|
||||||
|
"settings.ai.rarely_needed": "— raramente necesario",
|
||||||
|
"settings.ai.save_advanced": "Guardar avanzado",
|
||||||
|
"settings.ai.saved": "Guardado",
|
||||||
|
"settings.ai.turn_off": "Desactivar",
|
||||||
|
"settings.ai.default_value": "Predeterminado {value}.",
|
||||||
|
"settings.ai.reset_default": "Restablecer al valor predeterminado",
|
||||||
|
"settings.ai.reset_field": "Restablecer {name}",
|
||||||
|
|
||||||
|
"settings.mcp.title": "Servidor MCP",
|
||||||
|
"settings.mcp.banner_1": "Esto permite que tu propio agente de programación (Claude Code, Codex, Copilot, OpenCode, …) obtenga el contexto de la reunión en tu máquina local. Una vez conectado,",
|
||||||
|
"settings.mcp.banner_agent": "ese agente",
|
||||||
|
"settings.mcp.banner_2": "puede reenviar lo que lee a la nube de su propio proveedor de modelos — fuera del control de WhispAssist. WhispAssist en sí nunca envía estos datos a ningún lado; el servidor solo escucha en este dispositivo (",
|
||||||
|
"settings.mcp.banner_3": ") y cada lectura se registra abajo.",
|
||||||
|
"settings.mcp.enable": "Habilitar el servidor MCP",
|
||||||
|
"settings.mcp.enable_hint": "Desactivado por defecto. Solo loopback, protegido con token — nada es accesible desde la red.",
|
||||||
|
"settings.mcp.transport": "Transporte",
|
||||||
|
"settings.mcp.transport_http": "HTTP en streaming",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (el agente genera un proceso)",
|
||||||
|
"settings.mcp.port": "Puerto",
|
||||||
|
"settings.mcp.transport_change_hint": "Para cambiar el transporte/puerto, apaga primero el servidor y vuelve a encenderlo.",
|
||||||
|
"settings.mcp.scope_title": "Alcance",
|
||||||
|
"settings.mcp.expose": "Exponer",
|
||||||
|
"settings.mcp.scope_none": "Ninguno — no se comparte nada",
|
||||||
|
"settings.mcp.scope_selected": "Seleccionados — solo los resúmenes de funciones que hayas marcado como compartidos",
|
||||||
|
"settings.mcp.scope_all": "Todo — reuniones, transcripciones, tareas y resúmenes compartidos",
|
||||||
|
"settings.mcp.expose_recordings": "Permitir también reuniones con una grabación guardada (desactivado por defecto — la transcripción de una reunión grabada se retiene incluso en el alcance «Todo» hasta que esto esté activado)",
|
||||||
|
"settings.mcp.token_title": "Nuevo token de autenticación — se muestra una vez, cópialo ahora",
|
||||||
|
"settings.mcp.token_hint": "Esto no se volverá a mostrar. Se almacena en el administrador de credenciales del sistema; si lo pierdes, apaga el servidor y vuelve a encenderlo para generar uno nuevo.",
|
||||||
|
"settings.mcp.copied": "Copiado",
|
||||||
|
"settings.mcp.copy": "Copiar",
|
||||||
|
"settings.mcp.endpoint": "Endpoint",
|
||||||
|
"settings.mcp.endpoint_hint": "Apunta la configuración del cliente MCP de tu agente a {kind}, con el token de arriba como credencial bearer.",
|
||||||
|
"settings.mcp.kind_command": "comando",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "Registro de accesos",
|
||||||
|
"settings.mcp.access_log_hint": "Cada lectura de herramienta que hace un agente, permitida o denegada (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "Ningún agente ha leído nada aún.",
|
||||||
|
"settings.mcp.log_meeting": "reunión {id}",
|
||||||
|
"settings.mcp.refresh": "Actualizar",
|
||||||
|
|
||||||
|
"settings.privacy.title": "Privacidad",
|
||||||
|
"settings.privacy.intro": "Lo que WhispAssist puede enviar realmente fuera de este dispositivo ahora mismo. Con todo desactivado (lo predeterminado), nada sale del dispositivo.",
|
||||||
|
"settings.privacy.llm_endpoint": "Endpoint del LLM",
|
||||||
|
"settings.privacy.off": "desactivado",
|
||||||
|
"settings.privacy.local_only": "solo local",
|
||||||
|
"settings.privacy.leaves_device": "sale de este dispositivo",
|
||||||
|
"settings.privacy.sync_label": "Sincronización",
|
||||||
|
"settings.privacy.enabled": "habilitada",
|
||||||
|
"settings.privacy.mcp_label": "Servidor MCP",
|
||||||
|
"settings.privacy.mcp_on": "activado · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "Entrante solo en loopback — no agrega nada a la lista de salida de arriba. Un agente conectado aún puede reenviar lo que lee a su propio proveedor de modelos; consulta la pestaña del servidor MCP.",
|
||||||
|
"settings.privacy.egress_title": "Lista de permitidos de salida",
|
||||||
|
"settings.privacy.no_egress": "No hay hosts en la lista de permitidos — WA no realiza ninguna salida de contenido.",
|
||||||
|
"settings.privacy.sync_targets_title": "Destinos de sincronización",
|
||||||
|
"settings.privacy.third_party": "terceros",
|
||||||
|
"settings.privacy.your_server": "tu servidor",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "sin TLS",
|
||||||
|
"settings.privacy.refresh": "Actualizar",
|
||||||
|
"settings.privacy.unavailable": "Autocomprobación de privacidad no disponible.",
|
||||||
|
"settings.privacy.vault_title": "Bóveda de cifrado",
|
||||||
|
"settings.privacy.vault_intro": "Cifra notas, transcripciones y resúmenes en reposo con una contraseña. (Los archivos de audio aún no se cifran.)",
|
||||||
|
"settings.privacy.vault_password": "Contraseña de la bóveda",
|
||||||
|
"settings.privacy.enable_vault": "Habilitar bóveda",
|
||||||
|
"settings.privacy.vault_pw_hint": "Usa al menos 8 caracteres. Si la olvidas, el contenido cifrado no se podrá recuperar.",
|
||||||
|
"settings.privacy.vault_locked_1": "La bóveda está ",
|
||||||
|
"settings.privacy.locked_word": "bloqueada",
|
||||||
|
"settings.privacy.vault_locked_2": ". Desbloquéala para leer las reuniones cifradas.",
|
||||||
|
"settings.privacy.password": "Contraseña",
|
||||||
|
"settings.privacy.unlock": "Desbloquear",
|
||||||
|
"settings.privacy.vault_unlocked_1": "La bóveda está ",
|
||||||
|
"settings.privacy.unlocked_word": "desbloqueada",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". Las nuevas notas, transcripciones y resúmenes se cifran en reposo.",
|
||||||
|
"settings.privacy.lock_now": "Bloquear ahora",
|
||||||
|
"settings.privacy.change_password": "Cambiar contraseña",
|
||||||
|
"settings.privacy.current_password": "Contraseña actual",
|
||||||
|
"settings.privacy.new_password": "Nueva contraseña",
|
||||||
|
|
||||||
|
"settings.about.title": "Acerca de",
|
||||||
|
"settings.about.tagline": "Un asistente de reuniones totalmente local, de código abierto y nativo de Windows.",
|
||||||
|
"settings.about.build_commit": "Commit de compilación",
|
||||||
|
|
||||||
|
"consent.heading": "Antes de grabar",
|
||||||
|
"consent.body": "Grabar conversaciones sin el consentimiento de los participantes puede ser ilegal en tu región. Consulta las leyes locales sobre grabación. Esto es una advertencia, no asesoría legal.",
|
||||||
|
"consent.accept": "Entiendo — habilitar la grabación",
|
||||||
|
"consent.cancel": "Cancelar",
|
||||||
|
|
||||||
|
"hosted.heading": "Antes de usar {provider}",
|
||||||
|
"hosted.body_1": "Generar con",
|
||||||
|
"hosted.body_2": "envía la transcripción de esta reunión a sus servidores — sale de este dispositivo y queda sujeta a su política de privacidad. WhispAssist no tiene control sobre el tratamiento de los datos una vez que salen de tu dispositivo. Esto está desactivado por defecto; lo estás eligiendo ahora.",
|
||||||
|
"hosted.accept": "Entiendo — continuar",
|
||||||
|
"hosted.cancel": "Cancelar",
|
||||||
|
|
||||||
|
"theme.group": "Tema",
|
||||||
|
"theme.system": "Según el sistema",
|
||||||
|
"theme.light": "Claro",
|
||||||
|
"theme.dark": "Oscuro",
|
||||||
|
|
||||||
|
"import.dialog_label": "Agregar una reunión desde un archivo o URL",
|
||||||
|
"import.title": "Agregar una reunión",
|
||||||
|
"import.close": "Cerrar",
|
||||||
|
"import.close_title": "Cerrar (Esc)",
|
||||||
|
"import.body": "Importa una grabación existente — un archivo de audio/video local o un enlace (YouTube, una página de streaming o una URL de medios directa). Se transcribe y se diariza igual que una grabación en vivo.",
|
||||||
|
"import.file_or_url": "Archivo o URL",
|
||||||
|
"import.source_placeholder": "Pega una URL o busca un archivo…",
|
||||||
|
"import.choose_file": "Elegir un archivo local",
|
||||||
|
"import.browse": "Examinar…",
|
||||||
|
"import.title_label": "Título",
|
||||||
|
"import.optional": "opcional",
|
||||||
|
"import.title_placeholder": "Por defecto, el nombre del archivo",
|
||||||
|
"import.filter_av": "Audio / video",
|
||||||
|
"import.requires_1": "Requiere",
|
||||||
|
"import.requires_2": "instalado y en tu PATH (además de",
|
||||||
|
"import.requires_3": "para las URL). WhispAssist no los incluye.",
|
||||||
|
"import.importing": "Importando… esto puede tardar un rato",
|
||||||
|
"import.import": "Importar",
|
||||||
|
"import.cancel": "Cancelar",
|
||||||
|
|
||||||
|
"tagchip.filter": "Filtrar reuniones etiquetadas con «{tag}»",
|
||||||
|
"tagchip.remove": "Quitar etiqueta {tag}",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "Nivel de entrada del sistema y del micrófono",
|
||||||
|
"levelmeter.system": "Nivel de entrada del sistema",
|
||||||
|
|
||||||
|
"app.tagline": "local · privado",
|
||||||
|
"app.note_template": "Plantilla de notas",
|
||||||
|
"app.no_template": "Sin plantilla",
|
||||||
|
"app.add_meeting": "Agregar reunión",
|
||||||
|
"app.add_meeting_title": "Agregar una reunión desde un archivo o URL",
|
||||||
|
"app.record": "Grabar",
|
||||||
|
"app.record_title": "Iniciar grabación (Ctrl+Mayús+R)",
|
||||||
|
"app.stop": "Detener",
|
||||||
|
"app.stop_title": "Detener grabación (Ctrl+Mayús+R)",
|
||||||
|
"app.cancel": "Cancelar",
|
||||||
|
"app.cancel_title": "Descartar esta grabación y eliminarla",
|
||||||
|
"app.recording": "Grabando…",
|
||||||
|
"app.backend_title": "Backend de transcripción activo",
|
||||||
|
"app.retention_title": "Guardar el audio como .wav para esta reunión",
|
||||||
|
"app.saving": "guardando",
|
||||||
|
"app.not_saved": "no guardado",
|
||||||
|
"app.reconnecting": "reconectando el dispositivo de audio…",
|
||||||
|
"app.settings": "Configuración",
|
||||||
|
"app.settings_title": "Configuración (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "¿Descartar esta grabación? Se eliminarán su audio y su transcripción.",
|
||||||
|
"app.announce_started": "Grabación iniciada",
|
||||||
|
"app.announce_paused": "Grabación en pausa",
|
||||||
|
"app.announce_stopped": "Grabación detenida",
|
||||||
|
"app.consent_dialog": "Consentimiento de grabación",
|
||||||
|
"app.vault_dialog": "Desbloquear la bóveda de cifrado",
|
||||||
|
"app.vault_title": "Desbloquear la bóveda de cifrado",
|
||||||
|
"app.vault_desc": "Tus reuniones están cifradas en reposo. Ingresa tu contraseña para leerlas.",
|
||||||
|
"app.vault_password": "Contraseña de la bóveda",
|
||||||
|
"app.vault_incorrect": "Contraseña incorrecta",
|
||||||
|
"app.unlock": "Desbloquear",
|
||||||
|
"app.continue_locked": "Continuar bloqueado",
|
||||||
|
"app.show_meetings": "Mostrar lista de reuniones",
|
||||||
|
"app.hide_meetings": "Ocultar lista de reuniones",
|
||||||
|
"app.show_summary": "Mostrar panel de resumen",
|
||||||
|
"app.hide_summary": "Ocultar panel de resumen",
|
||||||
|
"app.resize_meetings": "Redimensionar lista de reuniones",
|
||||||
|
"app.resize_summary": "Redimensionar panel de resumen",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "Buscar reuniones…",
|
||||||
|
"meetings.search_aria": "Buscar reuniones",
|
||||||
|
"meetings.filter_tag_aria": "Filtrar por etiqueta",
|
||||||
|
"meetings.all_tags": "Todas las etiquetas",
|
||||||
|
"meetings.from_date_aria": "Desde la fecha",
|
||||||
|
"meetings.to_date_aria": "Hasta la fecha",
|
||||||
|
"meetings.bulk_format_aria": "Formato de exportación masiva",
|
||||||
|
"meetings.bulk_export": "Exportación masiva",
|
||||||
|
"meetings.bulk_export_title": "Exportar todas las reuniones que coincidan con los filtros de etiqueta/fecha de arriba",
|
||||||
|
"meetings.exporting": "Exportando…",
|
||||||
|
"meetings.exported_one": "{n} reunión exportada.",
|
||||||
|
"meetings.exported_many": "{n} reuniones exportadas.",
|
||||||
|
"meetings.searching": "Buscando…",
|
||||||
|
"meetings.loading": "Cargando reuniones…",
|
||||||
|
"meetings.no_matches": "Sin coincidencias.",
|
||||||
|
"meetings.empty": "Aún no hay reuniones — haz clic en Grabar arriba para empezar.",
|
||||||
|
"meetings.status.recording": "grabando",
|
||||||
|
"meetings.status.transcribing": "transcribiendo",
|
||||||
|
"meetings.status.recovering": "recuperando",
|
||||||
|
"meetings.status.error": "error",
|
||||||
|
"meetings.resume": "Reanudar transcripción",
|
||||||
|
"meetings.delete_aria": "Eliminar reunión",
|
||||||
|
"meetings.delete_confirm": "¿Eliminar esta reunión? Se eliminarán su grabación, transcripción y notas.",
|
||||||
|
|
||||||
|
"transcript.heading": "Transcripción",
|
||||||
|
"transcript.title_aria": "Título de la reunión",
|
||||||
|
"transcript.lang_title": "Idioma de transcripción",
|
||||||
|
"transcript.lang_auto": "detectando…",
|
||||||
|
"transcript.show": "Mostrar transcripción",
|
||||||
|
"transcript.hide": "Ocultar transcripción",
|
||||||
|
"transcript.empty": "No hay transcripción para esta reunión.",
|
||||||
|
"transcript.will_appear": "La transcripción aparecerá aquí mientras grabas.",
|
||||||
|
"transcript.play_from_here": "Reproducir desde aquí",
|
||||||
|
"transcript.resize": "Redimensionar transcripción y notas",
|
||||||
|
"transcript.reprocess_placeholder": "Volver a transcribir con…",
|
||||||
|
"transcript.reprocess_lang_aria": "Idioma de reprocesamiento",
|
||||||
|
"transcript.reprocess_keep_lang": "Mantener el idioma actual",
|
||||||
|
"transcript.reprocess_go": "Ir",
|
||||||
|
"transcript.retranscribing": "Volviendo a transcribir…",
|
||||||
|
"transcript.has_note": "Tiene una nota",
|
||||||
|
"transcript.note_placeholder": "Agrega una nota para este momento…",
|
||||||
|
"transcript.note_aria": "Nota para esta línea de la transcripción",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Documento de Word",
|
||||||
|
"transcript.filter_obsidian": "Nota de Obsidian",
|
||||||
|
|
||||||
|
"notes.heading": "Notas",
|
||||||
|
"notes.show": "Mostrar notas",
|
||||||
|
"notes.hide": "Ocultar notas",
|
||||||
|
"notes.placeholder": "Notas…",
|
||||||
|
"notes.live_placeholder": "Escribe notas mientras hablas…",
|
||||||
|
"notes.toolbar_aria": "Formato de notas",
|
||||||
|
"notes.bold": "Negrita",
|
||||||
|
"notes.italic": "Cursiva",
|
||||||
|
"notes.h1": "Encabezado 1",
|
||||||
|
"notes.h2": "Encabezado 2",
|
||||||
|
"notes.bullet": "Lista con viñetas",
|
||||||
|
"notes.checkbox_title": "Casilla de verificación",
|
||||||
|
"notes.checkbox_aria": "Elemento de lista con casilla",
|
||||||
|
"notes.edit_raw": "Editar el markdown en bruto",
|
||||||
|
"notes.render": "Renderizar el markdown",
|
||||||
|
"notes.editor": "Editor",
|
||||||
|
"notes.preview": "Vista previa",
|
||||||
|
"notes.export_md_title": "Exportar notas como .md",
|
||||||
|
"notes.export_pdf_title": "Exportar notas como .pdf",
|
||||||
|
"notes.export_docx_title": "Exportar notas como .docx",
|
||||||
|
"notes.export_bundle_title": "Exportar audio + transcripción + notas a una carpeta",
|
||||||
|
"notes.export_obsidian_title": "Exportar una nota de Obsidian (notas + resumen + transcripción, sin audio)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "Grabación",
|
||||||
|
"summary.loading_recording": "Cargando grabación…",
|
||||||
|
"summary.sync_heading": "Sincronización",
|
||||||
|
"summary.uploading": "Subiendo…",
|
||||||
|
"summary.upload_now": "Subir ahora",
|
||||||
|
"summary.no_uploads": "Aún no hay subidas para esta reunión.",
|
||||||
|
"summary.retry": "reintentar",
|
||||||
|
"summary.tags_heading": "Etiquetas",
|
||||||
|
"summary.tags_select": "Selecciona una reunión para etiquetarla.",
|
||||||
|
"summary.tag_add_placeholder": "Agregar etiqueta…",
|
||||||
|
"summary.tag_first_placeholder": "proyecto, cliente, tema…",
|
||||||
|
"summary.generating": "Generando…",
|
||||||
|
"summary.generate_tags": "Generar etiquetas",
|
||||||
|
"summary.saving": "Guardando…",
|
||||||
|
"summary.save_tags": "Guardar etiquetas",
|
||||||
|
"summary.summary_heading": "Resumen",
|
||||||
|
"summary.ai_provider": "Proveedor de IA",
|
||||||
|
"summary.provider_off": "Desactivado",
|
||||||
|
"summary.provider_custom": "Personalizado",
|
||||||
|
"summary.local": "local",
|
||||||
|
"summary.leaves_device": "sale de este dispositivo",
|
||||||
|
"summary.this_hosted_provider": "este proveedor alojado",
|
||||||
|
"summary.select_generate": "Selecciona una reunión para generar un resumen.",
|
||||||
|
"summary.decisions_heading": "Decisiones",
|
||||||
|
"summary.regenerate": "Regenerar",
|
||||||
|
"summary.generated_hint": "Generado localmente tras la reunión (requiere un proveedor de LLM local).",
|
||||||
|
"summary.no_provider": "No hay proveedor de IA configurado — habilita uno en Configuración.",
|
||||||
|
"summary.generate_summary": "Generar resumen",
|
||||||
|
"summary.briefs_heading": "Resúmenes de funciones",
|
||||||
|
"summary.briefs_select": "Selecciona una reunión para crear o ver resúmenes de funciones.",
|
||||||
|
"summary.briefs_desc": "Destila esta reunión en una especificación lista para agentes (requiere un proveedor de LLM local) — entrégala a un agente de programación o sírvela a través del servidor MCP una vez que esté activado.",
|
||||||
|
"summary.brief_repo_placeholder": "Repo de destino (opcional), p. ej. acme/reporting-web",
|
||||||
|
"summary.distilling": "Destilando…",
|
||||||
|
"summary.create_brief": "Crear resumen de función",
|
||||||
|
"summary.brief_mcp_title": "Disponible cuando el servidor MCP está activado",
|
||||||
|
"summary.copied": "Copiado",
|
||||||
|
"summary.copy_md": "Copiar como Markdown",
|
||||||
|
"summary.copy_json": "Copiar como JSON",
|
||||||
|
"summary.brief_problem": "Problema",
|
||||||
|
"summary.brief_outcome": "Resultado deseado",
|
||||||
|
"summary.brief_criteria": "Criterios de aceptación",
|
||||||
|
"summary.brief_none": "Nada capturado.",
|
||||||
|
"summary.brief_context": "Contexto",
|
||||||
|
"summary.action_items_heading": "Tareas",
|
||||||
|
"summary.ai_select": "Selecciona una reunión para ver sus tareas.",
|
||||||
|
"summary.ai_empty": "Aún no hay ninguna — agrega una abajo o genera un resumen para extraerlas automáticamente.",
|
||||||
|
"summary.confirmed": "Confirmada",
|
||||||
|
"summary.ai_text_placeholder": "Tarea…",
|
||||||
|
"summary.ai_text_aria": "Texto de la tarea",
|
||||||
|
"summary.owner": "Responsable",
|
||||||
|
"summary.due_date": "Fecha límite",
|
||||||
|
"summary.reminder_title": "Programar un recordatorio local para esta fecha límite",
|
||||||
|
"summary.ai_delete_title": "Eliminar esta tarea",
|
||||||
|
"summary.ai_delete_aria": "Eliminar tarea",
|
||||||
|
"summary.add_action_item": "Agregar tarea",
|
||||||
|
"summary.save_action_items": "Guardar tareas",
|
||||||
|
"summary.calendar_heading": "Evento del calendario",
|
||||||
|
"summary.cal_select": "Selecciona una reunión para vincularla a un evento del calendario.",
|
||||||
|
"summary.event_search_placeholder": "Buscar título del evento…",
|
||||||
|
"summary.linked_event": "Evento vinculado",
|
||||||
|
"summary.change_event": "Cambiar evento…",
|
||||||
|
"summary.link_event": "Vincular un evento…",
|
||||||
|
"summary.untitled_event": "(sin título)",
|
||||||
|
"summary.no_events_1": "Aún no se han importado eventos — importa uno",
|
||||||
|
"summary.no_events_2": "desde Configuración → Calendario.",
|
||||||
|
"summary.no_events_match": "Ningún evento coincide con esta búsqueda/fecha — prueba a borrar una.",
|
||||||
|
"summary.organizer_label": "Organizador: {name}",
|
||||||
|
"summary.participants_heading": "Participantes",
|
||||||
|
"summary.participants_hint": "Se rellena a partir del evento del calendario vinculado.",
|
||||||
|
"summary.speakers_heading": "Hablantes",
|
||||||
|
"summary.speakers_select": "Selecciona una reunión para nombrar a sus hablantes.",
|
||||||
|
"summary.no_speakers": "Aún no se han detectado hablantes.",
|
||||||
|
"summary.speaker_name_placeholder": "Nombre del hablante",
|
||||||
|
"summary.save": "Guardar",
|
||||||
|
"summary.cancel": "Cancelar",
|
||||||
|
"summary.name_speaker": "Nombrar a este hablante…",
|
||||||
|
"summary.add_new_name": "+ Agregar nuevo nombre…",
|
||||||
|
|
||||||
|
"settings.recording.title": "Grabación",
|
||||||
|
"settings.recording.record_default": "Grabar las reuniones por defecto",
|
||||||
|
"settings.recording.save_audio_as": "guardar el audio como",
|
||||||
|
"settings.recording.default_hint": "Desactivado por defecto. Cuando está desactivado, el audio se usa solo para producir la transcripción y se elimina al finalizar la reunión. También puedes activar la grabación por reunión.",
|
||||||
|
"settings.recording.consent_label": "Consentimiento: {status}.",
|
||||||
|
"settings.recording.consent_ack": "reconocido",
|
||||||
|
"settings.recording.consent_not": "aún no reconocido",
|
||||||
|
"settings.recording.auto_label": "Iniciar la grabación automáticamente cuando comience un evento del calendario",
|
||||||
|
"settings.recording.auto_hint": "Solo mientras WhispAssist esté abierto. Cuando llega la hora de inicio de un evento del calendario importado, comienza una grabación automáticamente (usando tu configuración de retención predeterminada de arriba). Nada se ejecuta en segundo plano — el temporizador solo se arma mientras la aplicación está en ejecución. Importa eventos en Configuración → Calendario."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "Tallennus",
|
||||||
|
"nav.hardware": "Laitteisto",
|
||||||
|
"nav.storage": "Tallennustila",
|
||||||
|
"nav.calendar": "Kalenteri",
|
||||||
|
"nav.sync": "Synkronointi",
|
||||||
|
"nav.ai": "Tekoäly",
|
||||||
|
"nav.mcp": "MCP-palvelin",
|
||||||
|
"nav.privacy": "Yksityisyys",
|
||||||
|
"nav.about": "Tietoja",
|
||||||
|
"nav.language": "Kieli",
|
||||||
|
|
||||||
|
"settings.language.title": "Kieli",
|
||||||
|
"settings.language.display": "Näyttökieli",
|
||||||
|
"settings.language.display_hint": "Sovelluksen käyttöliittymän kieli. Kielen lisääminen on yhtä helppoa kuin yhden käännöstiedoston pudottaminen paikalleen.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "Litterointikieli",
|
||||||
|
"settings.transcription.label": "Kieli",
|
||||||
|
"settings.transcription.auto": "Tunnista automaattisesti",
|
||||||
|
"settings.transcription.applies_hint": "Koskee seuraavaa tallennusta. Todella käytetty kieli näytetään jälkeenpäin jokaisessa kokouksessa.",
|
||||||
|
"settings.transcription.english_only": "Vain englanti",
|
||||||
|
"settings.transcription.model_english_only": "”{model}” on vain englanniksi.",
|
||||||
|
"settings.transcription.no_model": "Mallia ei ole valittu.",
|
||||||
|
"settings.transcription.switch_multilingual": "Vaihda yllä monikieliseen malliin valitaksesi kielen.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "Laitteisto",
|
||||||
|
"settings.hardware.active_backend": "Aktiivinen taustajärjestelmä",
|
||||||
|
"settings.hardware.model_meta": "· malli {size}",
|
||||||
|
"settings.hardware.preferred_backend": "Ensisijainen taustajärjestelmä",
|
||||||
|
"settings.hardware.auto_backend": "Automaattinen (paras saatavilla)",
|
||||||
|
"settings.hardware.not_available": "ei saatavilla",
|
||||||
|
"settings.hardware.fallback_hint": "Vaihtaa automaattisesti (NPU → NVIDIA → AMD → Intel → CPU), jos valittu taustajärjestelmä ei lataudu.",
|
||||||
|
"settings.hardware.audio_devices": "Äänilaitteet",
|
||||||
|
"settings.hardware.recording_device": "Tallennuslaite",
|
||||||
|
"settings.hardware.default_system_audio": "Järjestelmän oletusääni",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist tallentaa sen, mitä tämä laite toistaa (loopback) — puhelun toisen puolen. Valitse tietty lähtö, jos et halua sen seuraavan Windowsin järjestelmän oletusta.",
|
||||||
|
"settings.hardware.microphone": "Mikrofoni",
|
||||||
|
"settings.hardware.mic_off": "Pois — älä kaappaa mikrofoniani",
|
||||||
|
"settings.hardware.default_mic": "Oletusmikrofoni",
|
||||||
|
"settings.hardware.mic_hint": "Lisää oman äänesi suoraan litterointiin, jotta kokouksen molemmat puolet tallentuvat. Pysyy laitteellasi — mitään ei ladata verkkoon. Valitse ”Pois” litteroidaksesi vain yllä olevan järjestelmän äänen.",
|
||||||
|
"settings.hardware.npu_detected": "NPU-kiihdytin havaittu",
|
||||||
|
"settings.hardware.ready": "Valmis",
|
||||||
|
"settings.hardware.downloading": "Ladataan…",
|
||||||
|
"settings.hardware.package_needed": "Paketti tarvitaan",
|
||||||
|
"settings.hardware.npu_hint": "NPU:si tarvitsee kertaluonteisen kiihdytyspaketin (Whisper ONNX -malli{extra}). Se ladataan taustalla; voit myös käynnistää sen nyt.",
|
||||||
|
"settings.hardware.plus_openvino": " + OpenVINO-suoritusympäristö",
|
||||||
|
"settings.hardware.download_npu": "Lataa NPU-paketti",
|
||||||
|
"settings.hardware.dml_title": "GPU-kiihdytys (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "GPU:tasi voidaan kiihdyttää ilman Vulkania DirectML:n kautta — kertaluonteinen paketti (Whisper ONNX -malli{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + DirectML-suoritusympäristö",
|
||||||
|
"settings.hardware.download_dml": "Lataa DirectML-paketti",
|
||||||
|
"settings.hardware.detection_unavailable": "Laitteiston tunnistus ei saatavilla.",
|
||||||
|
"settings.hardware.low_overhead": "Kevyt esiasetus",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + pienin malli, akku-/taustakäyttöön",
|
||||||
|
"settings.hardware.models": "Mallit",
|
||||||
|
"settings.hardware.downloading_model": "Ladataan {name}",
|
||||||
|
"settings.hardware.active_badge": "Aktiivinen",
|
||||||
|
"settings.hardware.installed": "Asennettu",
|
||||||
|
"settings.hardware.use": "Käytä",
|
||||||
|
"settings.hardware.remove": "Poista",
|
||||||
|
"settings.hardware.cant_remove_active": "Aktiivista mallia ei voi poistaa",
|
||||||
|
"settings.hardware.download": "Lataa",
|
||||||
|
"settings.hardware.diarization": "Puhujien erottelu",
|
||||||
|
"settings.hardware.diarization_hint": "Asenna molemmat mallit, jotta valmiit tallennukset erotellaan puhujittain (Puhuja 1, Puhuja 2, …) yhden juoksevan litteroinnin sijaan. Toimii täysin offline-tilassa, kerran kokousta kohden sen pysähdyttyä. Ilman niitä jokainen rivi liitetään yhteen puhujaan.",
|
||||||
|
|
||||||
|
"settings.storage.title": "Tallennustila",
|
||||||
|
"settings.storage.location_label": "Sijainti",
|
||||||
|
"settings.storage.location_hint": "Tallennuspaikan vaihtamista ei vielä tueta — täällä kokoukset, mallit ja tietokanta tällä hetkellä sijaitsevat.",
|
||||||
|
"settings.storage.retention_title": "Säilytys",
|
||||||
|
"settings.storage.retention_hint": "Poistaa automaattisesti vanhimmat kokoukset rajan yli. Tarkistetaan kerran käynnistyksessä; tyhjä tarkoittaa ei rajaa. Ei koskaan koske kokoukseen, jota parhaillaan tallennetaan.",
|
||||||
|
"settings.storage.max_age": "Enimmäisikä (päivää)",
|
||||||
|
"settings.storage.max_size": "Enimmäiskoko (Gt)",
|
||||||
|
"settings.storage.no_limit": "ei rajaa",
|
||||||
|
"settings.storage.export_import_title": "Vienti ja tuonti",
|
||||||
|
"settings.storage.export_hint_1": "Siirrä tallennuksia tietokoneiden välillä siirrettävinä pakettikansioina (ääni, litterointi, muistiinpanot, yhteenveto ja",
|
||||||
|
"settings.storage.export_hint_2": "luettelotiedosto). Vie mihin tahansa kansioon — synkronoituun asemaan, USB-tikkuun tai synkronointikohteen paikalliseen liitospisteeseen — ja tuo se sitten toisella koneella. Tuodut kokoukset saavat uuden id:n, joten uudelleentuonti ei koskaan korvaa mitään.",
|
||||||
|
"settings.storage.export_dialog_title": "Vie kaikki kokoukset kohteeseen…",
|
||||||
|
"settings.storage.import_dialog_title": "Tuo paketti (tai pakettien kansio)…",
|
||||||
|
"settings.storage.exporting": "Viedään…",
|
||||||
|
"settings.storage.export_all": "Vie kaikki kokoukset…",
|
||||||
|
"settings.storage.importing": "Tuodaan…",
|
||||||
|
"settings.storage.import_all": "Tuo kokouksia…",
|
||||||
|
"settings.storage.exported_one": "Vietiin {n} kokous.",
|
||||||
|
"settings.storage.exported_many": "Vietiin {n} kokousta.",
|
||||||
|
"settings.storage.imported_one": "Tuotiin {n} kokous.",
|
||||||
|
"settings.storage.imported_many": "Tuotiin {n} kokousta.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "Kalenteri ja Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "Tuo tapahtumia ja osallistujia paikallisesta Outlook-",
|
||||||
|
"settings.calendar.intro_2": "varmuuskopiosta — vain luku, mitään ei kirjoiteta takaisin tiedostoon. Mikään ei poistu tältä laitteelta.",
|
||||||
|
"settings.calendar.pst_file": ".pst-tiedosto",
|
||||||
|
"settings.calendar.no_file": "Ei valittua tiedostoa",
|
||||||
|
"settings.calendar.browse": "Selaa…",
|
||||||
|
"settings.calendar.password": "Salasana",
|
||||||
|
"settings.calendar.password_note": "harvoin tarpeen",
|
||||||
|
"settings.calendar.optional": "valinnainen",
|
||||||
|
"settings.calendar.import_range": "Tuontialue",
|
||||||
|
"settings.calendar.range_30": "Viimeiset 30 päivää",
|
||||||
|
"settings.calendar.range_90": "Viimeiset 90 päivää",
|
||||||
|
"settings.calendar.range_180": "Viimeiset 6 kuukautta",
|
||||||
|
"settings.calendar.range_365": "Viimeinen vuosi",
|
||||||
|
"settings.calendar.range_all": "Koko ajalta",
|
||||||
|
"settings.calendar.range_hint": "Pitkäikäinen postilaatikko voi sisältää vuosien toistuvia/pyhäpäivämerkintöjä — alueen kaventaminen pitää tuodun kalenterin siinä, mikä on todella olennaista. Koskee sekä tätä tuontipainiketta että automaattista uudelleentuontia käynnistyksessä.",
|
||||||
|
"settings.calendar.importing": "Tuodaan…",
|
||||||
|
"settings.calendar.import": "Tuo",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} tapahtumaa",
|
||||||
|
"settings.calendar.auto_reimport": "Tuo tämä tiedosto automaattisesti uudelleen käynnistyksessä",
|
||||||
|
"settings.calendar.auto_reimport_hint": "Suoritetaan kerran käynnistyksessä, ei ajastimella — uudelleentuonti on turvallista toistaa (olemassa olevat tapahtumat sovitetaan ja päivitetään, ei kahdenneta).",
|
||||||
|
"settings.calendar.import_failed": "Tuonti epäonnistui: {error} — itse tiedostoon ei kosketa; tarkista polku ja yritä uudelleen.",
|
||||||
|
"settings.calendar.cleanup_title": "Siivoa",
|
||||||
|
"settings.calendar.cleanup_hint": "Tapahtumat, jotka on jo liitetty tallennettuun kokoukseen, säilytetään aina, riippumatta alla valitusta vaihtoehdosta.",
|
||||||
|
"settings.calendar.cleanup_30": "Vanhemmat kuin 30 päivää",
|
||||||
|
"settings.calendar.cleanup_90": "Vanhemmat kuin 90 päivää",
|
||||||
|
"settings.calendar.cleanup_180": "Vanhemmat kuin 6 kuukautta",
|
||||||
|
"settings.calendar.cleanup_365": "Vanhemmat kuin 1 vuosi",
|
||||||
|
"settings.calendar.cleanup_all": "Poista kaikki",
|
||||||
|
"settings.calendar.cleaning": "Siivotaan…",
|
||||||
|
"settings.calendar.cleanup_btn": "Siivoa kalenteri",
|
||||||
|
"settings.calendar.cleanup_result": "Poistettiin {deleted}{extra}.",
|
||||||
|
"settings.calendar.cleanup_kept": ", säilytettiin {n} (liitetty kokoukseen)",
|
||||||
|
"settings.calendar.cleanup_failed": "Siivous epäonnistui: {error}",
|
||||||
|
"settings.calendar.imported_events": "Tuodut tapahtumat",
|
||||||
|
"settings.calendar.no_events": "Tapahtumia ei ole vielä tuotu.",
|
||||||
|
"settings.calendar.search_title": "Hae otsikkoa",
|
||||||
|
"settings.calendar.search_placeholder": "Kokouksen nimi…",
|
||||||
|
"settings.calendar.date_label": "Päivämäärä",
|
||||||
|
"settings.calendar.events_count": "{shown}/{total} tapahtumaa",
|
||||||
|
"settings.calendar.untitled": "(nimetön)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "Asetukset",
|
||||||
|
"settings.close": "Sulje",
|
||||||
|
"settings.close_title": "Sulje (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "Esikatselutila — synkronoinnin taustajärjestelmää ei ole vielä toteutettu (Vaihe 9). Muutokset säilytetään vain käyttöliittymässä.",
|
||||||
|
"settings.sync.title": "Synkronointi ja lataus",
|
||||||
|
"settings.sync.enable_label": "Ota käyttöön kokousartefaktien lataaminen määritettyihin kohteisiin",
|
||||||
|
"settings.sync.enable_hint": "Oletuksena pois. Mitään ei ladata, ellei tämä ole päällä ja kohde otettu käyttöön. Itse isännöidyt kohteet pitävät tiedot omalla palvelimellasi; kolmannen osapuolen pilvet on selkeästi merkitty.",
|
||||||
|
"settings.sync.targets_title": "Kohteet",
|
||||||
|
"settings.sync.no_targets": "Ei vielä kohteita. Lisää yksi alta.",
|
||||||
|
"settings.sync.third_party": "kolmas osapuoli",
|
||||||
|
"settings.sync.your_server": "palvelimesi",
|
||||||
|
"settings.sync.edit": "Muokkaa",
|
||||||
|
"settings.sync.remove": "Poista",
|
||||||
|
"settings.sync.edit_target": "Muokkaa kohdetta",
|
||||||
|
"settings.sync.add_target": "Lisää kohde",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV kattaa Nextcloudin, ownCloudin, Cloudreven, Seafilen ja Synologyn. (Seafile: ota SeafDAV käyttöön palvelimen puolella.)",
|
||||||
|
"settings.sync.name": "Nimi",
|
||||||
|
"settings.sync.name_placeholder": "Koti-Nextcloud",
|
||||||
|
"settings.sync.provider": "Palveluntarjoaja",
|
||||||
|
"settings.sync.server_url": "Palvelimen URL",
|
||||||
|
"settings.sync.server_url_note_1": "Vain palvelimesi URL — WhispAssist lisää",
|
||||||
|
"settings.sync.server_url_note_2": "automaattisesti.",
|
||||||
|
"settings.sync.remote_folder": "Etäkansio",
|
||||||
|
"settings.sync.username": "Käyttäjänimi",
|
||||||
|
"settings.sync.app_password": "Sovellussalasana",
|
||||||
|
"settings.sync.pw_keep": "jätä tyhjäksi säilyttääksesi nykyisen salasanan",
|
||||||
|
"settings.sync.pw_store": "tallennettu käyttöjärjestelmän tunnistetietovarastoon",
|
||||||
|
"settings.sync.upload_legend": "Lataa",
|
||||||
|
"settings.sync.artifact_transcript": "litterointi",
|
||||||
|
"settings.sync.artifact_notes": "muistiinpanot",
|
||||||
|
"settings.sync.artifact_summary": "yhteenveto",
|
||||||
|
"settings.sync.artifact_recording": "tallennus (.wav, jos säilytetty)",
|
||||||
|
"settings.sync.allow_plaintext": "Salli selkotekstinen http LAN-osoitteelle (ei suositella)",
|
||||||
|
"settings.sync.encrypt_before": "Salaa ennen lataamista (kohde tallentaa vain salatekstin)",
|
||||||
|
"settings.sync.requires_vault": "— vaatii avatun holvin",
|
||||||
|
"settings.sync.test_connection": "Testaa yhteys",
|
||||||
|
"settings.sync.save_changes": "Tallenna muutokset",
|
||||||
|
"settings.sync.add_target_btn": "Lisää kohde",
|
||||||
|
"settings.sync.cancel": "Peruuta",
|
||||||
|
"settings.sync.third_party_banner": "{kind} on kolmannen osapuolen pilvi — lataaminen lähettää tietosi pois laitteeltasi kohteeseen {kind}.",
|
||||||
|
"settings.sync.link_account": "Yhdistä {kind}-tili…",
|
||||||
|
"settings.sync.oauth_hint": "Avaa OAuth-kirjautumisen (loopback-uudelleenohjaus); tunnus tallennetaan käyttöjärjestelmän tunnistetietovarastoon.",
|
||||||
|
"settings.sync.footnote": "Tunnistetietoja ei koskaan kirjoiteta asetuksiin tai tietokantaan — vain käyttöjärjestelmän tunnistetietovarastoon. TLS vaaditaan LAN:in ulkopuolisille kohteille.",
|
||||||
|
|
||||||
|
"settings.ai.title": "Tekoälyn yhteenvetopalveluntarjoaja",
|
||||||
|
"settings.ai.intro_1": "Yhteenvedot suoritetaan oletuksena paikallisella LLM:llä. Osoita tämä Ollamaan tällä tietokoneella tai toisella koneella LAN:issasi (esim.",
|
||||||
|
"settings.ai.intro_2": "— molemmat lasketaan paikallisiksi, joten mikään ei poistu verkostasi. Isännöidyt palveluntarjoajat (Anthropic) ovat valinnaisia, oletuksena pois, ja lähettävät litteroinnin kolmannelle osapuolelle heti kun otat yhden käyttöön.",
|
||||||
|
"settings.ai.provider": "Palveluntarjoaja",
|
||||||
|
"settings.ai.off": "Pois",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (paikallinen / LAN)",
|
||||||
|
"settings.ai.provider_custom": "Mukautettu (OpenAI-yhteensopiva)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — isännöity, poistuu tältä laitteelta",
|
||||||
|
"settings.ai.endpoint": "Päätepiste",
|
||||||
|
"settings.ai.model": "Malli",
|
||||||
|
"settings.ai.api_key": "API-avain",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (jo asetettu — jätä tyhjäksi säilyttääksesi sen)",
|
||||||
|
"settings.ai.show_api_key": "Näytä API-avain",
|
||||||
|
"settings.ai.hide_api_key": "Piilota API-avain",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic on isännöity kolmannen osapuolen palvelu — tämän kokouksen litterointi poistuu laitteeltasi, kun luot yhteenvedon.",
|
||||||
|
"settings.ai.endpoint_banner": "Tämä päätepiste ei ole koneellasi tai LAN:issasi — litterointisi poistuisi verkostasi.",
|
||||||
|
"settings.ai.saving": "Tallennetaan…",
|
||||||
|
"settings.ai.save": "Tallenna",
|
||||||
|
"settings.ai.test_connection": "Testaa yhteys",
|
||||||
|
"settings.ai.this_hosted_provider": "tämä isännöity palveluntarjoaja",
|
||||||
|
"settings.ai.reachable": "Tavoitettavissa",
|
||||||
|
"settings.ai.unreachable": "Ei tavoitettavissa",
|
||||||
|
"settings.ai.model_count_one": "· {n} malli",
|
||||||
|
"settings.ai.model_count_many": "· {n} mallia",
|
||||||
|
"settings.ai.on_lan": "· koneellasi / LAN",
|
||||||
|
"settings.ai.leaves_network": "· poistuu verkostasi",
|
||||||
|
"settings.ai.advanced_title": "Ollaman lisäasetukset",
|
||||||
|
"settings.ai.changed": "{n} muutettu",
|
||||||
|
"settings.ai.reset_all": "Palauta kaikki oletuksiin",
|
||||||
|
"settings.ai.system_prompt": "Järjestelmäkehote",
|
||||||
|
"settings.ai.system_prompt_placeholder": "esim. Olet ytimekäs kokousten yhteenvetäjä.",
|
||||||
|
"settings.ai.system_prompt_hint": "Lisätään ennen WhispAssistin vaadittua tulostemuotoa, jotta ohjeesi eivät voi rikkoa yhteenvedon/tehtävien jäsentämistä.",
|
||||||
|
"settings.ai.think": "Päättele",
|
||||||
|
"settings.ai.think_help": "Päättelypanostus (vain päättelymallit).",
|
||||||
|
"settings.ai.keep_alive": "Pidä käynnissä",
|
||||||
|
"settings.ai.keep_alive_help": "Kuinka kauan malli pysyy RAM:issa. esim. 5m, 1h, 0 (pura), -1 (ikuisesti).",
|
||||||
|
"settings.ai.runtime_hardware": "Suoritus ja laitteisto",
|
||||||
|
"settings.ai.rarely_needed": "— harvoin tarpeen",
|
||||||
|
"settings.ai.save_advanced": "Tallenna lisäasetukset",
|
||||||
|
"settings.ai.saved": "Tallennettu",
|
||||||
|
"settings.ai.turn_off": "Kytke pois",
|
||||||
|
"settings.ai.default_value": "Oletus {value}.",
|
||||||
|
"settings.ai.reset_default": "Palauta oletukseen",
|
||||||
|
"settings.ai.reset_field": "Palauta {name}",
|
||||||
|
|
||||||
|
"settings.mcp.title": "MCP-palvelin",
|
||||||
|
"settings.mcp.banner_1": "Tämä antaa oman koodausagenttisi (Claude Code, Codex, Copilot, OpenCode, …) hakea kokouskontekstia paikallisella koneellasi. Kun yhteys on muodostettu,",
|
||||||
|
"settings.mcp.banner_agent": "kyseinen agentti",
|
||||||
|
"settings.mcp.banner_2": "voi välittää lukemansa oman mallipalveluntarjoajansa pilveen — WhispAssistin hallinnan ulkopuolelle. WhispAssist itse ei koskaan lähetä näitä tietoja minnekään; palvelin kuuntelee vain tällä laitteella (",
|
||||||
|
"settings.mcp.banner_3": ") ja jokainen lukukerta kirjataan alle.",
|
||||||
|
"settings.mcp.enable": "Ota MCP-palvelin käyttöön",
|
||||||
|
"settings.mcp.enable_hint": "Oletuksena pois. Vain loopback, tunnussuojattu — mitään ei voi tavoittaa verkosta.",
|
||||||
|
"settings.mcp.transport": "Kuljetus",
|
||||||
|
"settings.mcp.transport_http": "Suoratoistava HTTP",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (agentti käynnistää prosessin)",
|
||||||
|
"settings.mcp.port": "Portti",
|
||||||
|
"settings.mcp.transport_change_hint": "Vaihtaaksesi kuljetuksen/portin, kytke palvelin ensin pois ja sitten takaisin päälle.",
|
||||||
|
"settings.mcp.scope_title": "Laajuus",
|
||||||
|
"settings.mcp.expose": "Julkaise",
|
||||||
|
"settings.mcp.scope_none": "Ei mitään — mitään ei jaeta",
|
||||||
|
"settings.mcp.scope_selected": "Valitut — vain ominaisuustiivistelmät, jotka olet merkinnyt jaetuiksi",
|
||||||
|
"settings.mcp.scope_all": "Kaikki — kokoukset, litteroinnit, tehtävät ja jaetut tiivistelmät",
|
||||||
|
"settings.mcp.expose_recordings": "Salli myös kokoukset, joilla on tallennettu tallennus (oletuksena pois — tallennetun kokouksen litterointi pidätetään jopa ”Kaikki”-laajuudessa, kunnes tämä on päällä)",
|
||||||
|
"settings.mcp.token_title": "Uusi todennustunnus — näytetään kerran, kopioi se nyt",
|
||||||
|
"settings.mcp.token_hint": "Tätä ei näytetä uudelleen. Se tallennetaan käyttöjärjestelmän tunnistetietovarastoon; jos hukkaat sen, kytke palvelin pois ja takaisin päälle luodaksesi uuden.",
|
||||||
|
"settings.mcp.copied": "Kopioitu",
|
||||||
|
"settings.mcp.copy": "Kopioi",
|
||||||
|
"settings.mcp.endpoint": "Päätepiste",
|
||||||
|
"settings.mcp.endpoint_hint": "Osoita agenttisi MCP-asiakasmääritys kohteeseen {kind}, käyttäen yllä olevaa tunnusta bearer-tunnistetietona.",
|
||||||
|
"settings.mcp.kind_command": "komento",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "Käyttöloki",
|
||||||
|
"settings.mcp.access_log_hint": "Jokainen työkaluluku, jonka agentti tekee, sallittu tai evätty (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "Yksikään agentti ei ole vielä lukenut mitään.",
|
||||||
|
"settings.mcp.log_meeting": "kokous {id}",
|
||||||
|
"settings.mcp.refresh": "Päivitä",
|
||||||
|
|
||||||
|
"settings.privacy.title": "Yksityisyys",
|
||||||
|
"settings.privacy.intro": "Mitä WhispAssist saa tällä hetkellä todella lähettää pois tältä laitteelta. Kun kaikki on pois (oletus), mikään ei poistu laitteelta lainkaan.",
|
||||||
|
"settings.privacy.llm_endpoint": "LLM-päätepiste",
|
||||||
|
"settings.privacy.off": "pois",
|
||||||
|
"settings.privacy.local_only": "vain paikallinen",
|
||||||
|
"settings.privacy.leaves_device": "poistuu tältä laitteelta",
|
||||||
|
"settings.privacy.sync_label": "Synkronointi",
|
||||||
|
"settings.privacy.enabled": "käytössä",
|
||||||
|
"settings.privacy.mcp_label": "MCP-palvelin",
|
||||||
|
"settings.privacy.mcp_on": "päällä · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "Saapuva vain loopbackissa — ei lisää mitään yllä olevaan lähtevän luetteloon. Yhdistetty agentti voi silti välittää lukemansa omalle mallipalveluntarjoajalleen; katso MCP-palvelin-välilehti.",
|
||||||
|
"settings.privacy.egress_title": "Lähtevän liikenteen sallittujen luettelo",
|
||||||
|
"settings.privacy.no_egress": "Yhtään isäntää ei ole sallittu — WA ei tee mitään sisällön lähtevää siirtoa.",
|
||||||
|
"settings.privacy.sync_targets_title": "Synkronointikohteet",
|
||||||
|
"settings.privacy.third_party": "kolmas osapuoli",
|
||||||
|
"settings.privacy.your_server": "palvelimesi",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "ei TLS:ää",
|
||||||
|
"settings.privacy.refresh": "Päivitä",
|
||||||
|
"settings.privacy.unavailable": "Yksityisyyden itsetarkistus ei saatavilla.",
|
||||||
|
"settings.privacy.vault_title": "Salausholvi",
|
||||||
|
"settings.privacy.vault_intro": "Salaa muistiinpanot, litteroinnit ja yhteenvedot levossa salasanalla. (Äänitiedostoja ei vielä salata.)",
|
||||||
|
"settings.privacy.vault_password": "Holvin salasana",
|
||||||
|
"settings.privacy.enable_vault": "Ota holvi käyttöön",
|
||||||
|
"settings.privacy.vault_pw_hint": "Käytä vähintään 8 merkkiä. Jos unohdat sen, salattua sisältöä ei voi palauttaa.",
|
||||||
|
"settings.privacy.vault_locked_1": "Holvi on ",
|
||||||
|
"settings.privacy.locked_word": "lukittu",
|
||||||
|
"settings.privacy.vault_locked_2": ". Avaa lukitus lukeaksesi salatut kokoukset.",
|
||||||
|
"settings.privacy.password": "Salasana",
|
||||||
|
"settings.privacy.unlock": "Avaa lukitus",
|
||||||
|
"settings.privacy.vault_unlocked_1": "Holvi on ",
|
||||||
|
"settings.privacy.unlocked_word": "avattu",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". Uudet muistiinpanot, litteroinnit ja yhteenvedot salataan levossa.",
|
||||||
|
"settings.privacy.lock_now": "Lukitse nyt",
|
||||||
|
"settings.privacy.change_password": "Vaihda salasana",
|
||||||
|
"settings.privacy.current_password": "Nykyinen salasana",
|
||||||
|
"settings.privacy.new_password": "Uusi salasana",
|
||||||
|
|
||||||
|
"settings.about.title": "Tietoja",
|
||||||
|
"settings.about.tagline": "Täysin paikallinen, avoimen lähdekoodin, Windows-natiivi kokousavustaja.",
|
||||||
|
"settings.about.build_commit": "Build-commit",
|
||||||
|
|
||||||
|
"consent.heading": "Ennen kuin tallennat",
|
||||||
|
"consent.body": "Keskustelujen tallentaminen ilman osallistujien suostumusta voi olla laitonta alueellasi. Tarkista paikalliset tallennuslait. Tämä on varoitus, ei oikeudellinen neuvo.",
|
||||||
|
"consent.accept": "Ymmärrän — ota tallennus käyttöön",
|
||||||
|
"consent.cancel": "Peruuta",
|
||||||
|
|
||||||
|
"hosted.heading": "Ennen kuin käytät palvelua {provider}",
|
||||||
|
"hosted.body_1": "Luominen palvelulla",
|
||||||
|
"hosted.body_2": "lähettää tämän kokouksen litteroinnin heidän palvelimilleen — se poistuu tältä laitteelta ja kuuluu heidän tietosuojakäytäntönsä piiriin. WhispAssistilla ei ole hallintaa tietojen käsittelyyn, kun ne poistuvat laitteeltasi. Tämä on oletuksena pois; valitset sen nyt.",
|
||||||
|
"hosted.accept": "Ymmärrän — jatka",
|
||||||
|
"hosted.cancel": "Peruuta",
|
||||||
|
|
||||||
|
"theme.group": "Teema",
|
||||||
|
"theme.system": "Seuraa järjestelmää",
|
||||||
|
"theme.light": "Vaalea",
|
||||||
|
"theme.dark": "Tumma",
|
||||||
|
|
||||||
|
"import.dialog_label": "Lisää kokous tiedostosta tai URL-osoitteesta",
|
||||||
|
"import.title": "Lisää kokous",
|
||||||
|
"import.close": "Sulje",
|
||||||
|
"import.close_title": "Sulje (Esc)",
|
||||||
|
"import.body": "Tuo olemassa oleva tallennus — paikallinen ääni-/videotiedosto tai linkki (YouTube, suoratoistosivu tai suora media-URL). Se litteroidaan ja eritellään puhujittain aivan kuten suora tallennus.",
|
||||||
|
"import.file_or_url": "Tiedosto tai URL",
|
||||||
|
"import.source_placeholder": "Liitä URL tai selaa tiedostoa…",
|
||||||
|
"import.choose_file": "Valitse paikallinen tiedosto",
|
||||||
|
"import.browse": "Selaa…",
|
||||||
|
"import.title_label": "Otsikko",
|
||||||
|
"import.optional": "valinnainen",
|
||||||
|
"import.title_placeholder": "Oletuksena tiedoston nimi",
|
||||||
|
"import.filter_av": "Ääni / video",
|
||||||
|
"import.requires_1": "Vaatii",
|
||||||
|
"import.requires_2": "asennettuna ja PATH:issasi (sekä",
|
||||||
|
"import.requires_3": "URL-osoitteille). WhispAssist ei sisällä niitä.",
|
||||||
|
"import.importing": "Tuodaan… tämä voi kestää hetken",
|
||||||
|
"import.import": "Tuo",
|
||||||
|
"import.cancel": "Peruuta",
|
||||||
|
|
||||||
|
"tagchip.filter": "Suodata kokoukset, joilla on tunniste ”{tag}”",
|
||||||
|
"tagchip.remove": "Poista tunniste {tag}",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "Järjestelmän ja mikrofonin tulotaso",
|
||||||
|
"levelmeter.system": "Järjestelmän tulotaso",
|
||||||
|
|
||||||
|
"app.tagline": "paikallinen · yksityinen",
|
||||||
|
"app.note_template": "Muistiinpanomalli",
|
||||||
|
"app.no_template": "Ei mallia",
|
||||||
|
"app.add_meeting": "Lisää kokous",
|
||||||
|
"app.add_meeting_title": "Lisää kokous tiedostosta tai URL-osoitteesta",
|
||||||
|
"app.record": "Tallenna",
|
||||||
|
"app.record_title": "Aloita tallennus (Ctrl+Shift+R)",
|
||||||
|
"app.stop": "Pysäytä",
|
||||||
|
"app.stop_title": "Pysäytä tallennus (Ctrl+Shift+R)",
|
||||||
|
"app.cancel": "Peruuta",
|
||||||
|
"app.cancel_title": "Hylkää tämä tallennus ja poista se",
|
||||||
|
"app.recording": "Tallennetaan…",
|
||||||
|
"app.backend_title": "Aktiivinen litteroinnin taustajärjestelmä",
|
||||||
|
"app.retention_title": "Tallenna ääni .wav-muodossa tälle kokoukselle",
|
||||||
|
"app.saving": "tallennetaan",
|
||||||
|
"app.not_saved": "ei tallennettu",
|
||||||
|
"app.reconnecting": "yhdistetään äänilaitetta uudelleen…",
|
||||||
|
"app.settings": "Asetukset",
|
||||||
|
"app.settings_title": "Asetukset (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "Hylätäänkö tämä tallennus? Sen ääni ja litterointi poistetaan.",
|
||||||
|
"app.announce_started": "Tallennus aloitettu",
|
||||||
|
"app.announce_paused": "Tallennus keskeytetty",
|
||||||
|
"app.announce_stopped": "Tallennus pysäytetty",
|
||||||
|
"app.consent_dialog": "Tallennussuostumus",
|
||||||
|
"app.vault_dialog": "Avaa salausholvin lukitus",
|
||||||
|
"app.vault_title": "Avaa salausholvin lukitus",
|
||||||
|
"app.vault_desc": "Kokouksesi on salattu levossa. Syötä salasanasi lukeaksesi ne.",
|
||||||
|
"app.vault_password": "Holvin salasana",
|
||||||
|
"app.vault_incorrect": "Väärä salasana",
|
||||||
|
"app.unlock": "Avaa lukitus",
|
||||||
|
"app.continue_locked": "Jatka lukittuna",
|
||||||
|
"app.show_meetings": "Näytä kokousluettelo",
|
||||||
|
"app.hide_meetings": "Piilota kokousluettelo",
|
||||||
|
"app.show_summary": "Näytä yhteenvetopaneeli",
|
||||||
|
"app.hide_summary": "Piilota yhteenvetopaneeli",
|
||||||
|
"app.resize_meetings": "Muuta kokousluettelon kokoa",
|
||||||
|
"app.resize_summary": "Muuta yhteenvetopaneelin kokoa",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "Hae kokouksia…",
|
||||||
|
"meetings.search_aria": "Hae kokouksia",
|
||||||
|
"meetings.filter_tag_aria": "Suodata tunnisteen mukaan",
|
||||||
|
"meetings.all_tags": "Kaikki tunnisteet",
|
||||||
|
"meetings.from_date_aria": "Alkupäivä",
|
||||||
|
"meetings.to_date_aria": "Loppupäivä",
|
||||||
|
"meetings.bulk_format_aria": "Joukkoviennin muoto",
|
||||||
|
"meetings.bulk_export": "Joukkovienti",
|
||||||
|
"meetings.bulk_export_title": "Vie kaikki kokoukset, jotka vastaavat yllä olevia tunniste-/päivämääräsuodattimia",
|
||||||
|
"meetings.exporting": "Viedään…",
|
||||||
|
"meetings.exported_one": "Vietiin {n} kokous.",
|
||||||
|
"meetings.exported_many": "Vietiin {n} kokousta.",
|
||||||
|
"meetings.searching": "Haetaan…",
|
||||||
|
"meetings.loading": "Ladataan kokouksia…",
|
||||||
|
"meetings.no_matches": "Ei osumia.",
|
||||||
|
"meetings.empty": "Ei vielä kokouksia — napsauta yllä Tallenna aloittaaksesi.",
|
||||||
|
"meetings.status.recording": "tallennetaan",
|
||||||
|
"meetings.status.transcribing": "litteroidaan",
|
||||||
|
"meetings.status.recovering": "palautetaan",
|
||||||
|
"meetings.status.error": "virhe",
|
||||||
|
"meetings.resume": "Jatka litterointia",
|
||||||
|
"meetings.delete_aria": "Poista kokous",
|
||||||
|
"meetings.delete_confirm": "Poistetaanko tämä kokous? Tämä poistaa sen tallennuksen, litteroinnin ja muistiinpanot.",
|
||||||
|
|
||||||
|
"transcript.heading": "Litterointi",
|
||||||
|
"transcript.title_aria": "Kokouksen otsikko",
|
||||||
|
"transcript.lang_title": "Litterointikieli",
|
||||||
|
"transcript.lang_auto": "tunnistetaan…",
|
||||||
|
"transcript.show": "Näytä litterointi",
|
||||||
|
"transcript.hide": "Piilota litterointi",
|
||||||
|
"transcript.empty": "Tälle kokoukselle ei ole litterointia.",
|
||||||
|
"transcript.will_appear": "Litterointi ilmestyy tähän tallennuksen aikana.",
|
||||||
|
"transcript.play_from_here": "Toista tästä",
|
||||||
|
"transcript.resize": "Muuta litteroinnin ja muistiinpanojen kokoa",
|
||||||
|
"transcript.reprocess_placeholder": "Litteroi uudelleen mallilla…",
|
||||||
|
"transcript.reprocess_lang_aria": "Uudelleenkäsittelyn kieli",
|
||||||
|
"transcript.reprocess_keep_lang": "Säilytä nykyinen kieli",
|
||||||
|
"transcript.reprocess_go": "Suorita",
|
||||||
|
"transcript.retranscribing": "Litteroidaan uudelleen…",
|
||||||
|
"transcript.has_note": "On muistiinpano",
|
||||||
|
"transcript.note_placeholder": "Lisää muistiinpano tälle hetkelle…",
|
||||||
|
"transcript.note_aria": "Muistiinpano tälle litterointiriville",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Word-asiakirja",
|
||||||
|
"transcript.filter_obsidian": "Obsidian-muistiinpano",
|
||||||
|
|
||||||
|
"notes.heading": "Muistiinpanot",
|
||||||
|
"notes.show": "Näytä muistiinpanot",
|
||||||
|
"notes.hide": "Piilota muistiinpanot",
|
||||||
|
"notes.placeholder": "Muistiinpanot…",
|
||||||
|
"notes.live_placeholder": "Kirjoita muistiinpanoja puhuessasi…",
|
||||||
|
"notes.toolbar_aria": "Muistiinpanojen muotoilu",
|
||||||
|
"notes.bold": "Lihavointi",
|
||||||
|
"notes.italic": "Kursivointi",
|
||||||
|
"notes.h1": "Otsikko 1",
|
||||||
|
"notes.h2": "Otsikko 2",
|
||||||
|
"notes.bullet": "Luettelomerkitty lista",
|
||||||
|
"notes.checkbox_title": "Valintaruutu",
|
||||||
|
"notes.checkbox_aria": "Valintaruutulistan kohde",
|
||||||
|
"notes.edit_raw": "Muokkaa raakaa markdownia",
|
||||||
|
"notes.render": "Renderöi markdown",
|
||||||
|
"notes.editor": "Editori",
|
||||||
|
"notes.preview": "Esikatselu",
|
||||||
|
"notes.export_md_title": "Vie muistiinpanot muodossa .md",
|
||||||
|
"notes.export_pdf_title": "Vie muistiinpanot muodossa .pdf",
|
||||||
|
"notes.export_docx_title": "Vie muistiinpanot muodossa .docx",
|
||||||
|
"notes.export_bundle_title": "Vie ääni + litterointi + muistiinpanot kansioon",
|
||||||
|
"notes.export_obsidian_title": "Vie yksi Obsidian-muistiinpano (muistiinpanot + yhteenveto + litterointi, ei ääntä)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "Tallennus",
|
||||||
|
"summary.loading_recording": "Ladataan tallennusta…",
|
||||||
|
"summary.sync_heading": "Synkronointi",
|
||||||
|
"summary.uploading": "Ladataan…",
|
||||||
|
"summary.upload_now": "Lataa nyt",
|
||||||
|
"summary.no_uploads": "Tälle kokoukselle ei ole vielä latauksia.",
|
||||||
|
"summary.retry": "yritä uudelleen",
|
||||||
|
"summary.tags_heading": "Tunnisteet",
|
||||||
|
"summary.tags_select": "Valitse kokous merkitäksesi sen tunnisteilla.",
|
||||||
|
"summary.tag_add_placeholder": "Lisää tunniste…",
|
||||||
|
"summary.tag_first_placeholder": "projekti, asiakas, aihe…",
|
||||||
|
"summary.generating": "Luodaan…",
|
||||||
|
"summary.generate_tags": "Luo tunnisteet",
|
||||||
|
"summary.saving": "Tallennetaan…",
|
||||||
|
"summary.save_tags": "Tallenna tunnisteet",
|
||||||
|
"summary.summary_heading": "Yhteenveto",
|
||||||
|
"summary.ai_provider": "Tekoälyn palveluntarjoaja",
|
||||||
|
"summary.provider_off": "Pois",
|
||||||
|
"summary.provider_custom": "Mukautettu",
|
||||||
|
"summary.local": "paikallinen",
|
||||||
|
"summary.leaves_device": "poistuu tältä laitteelta",
|
||||||
|
"summary.this_hosted_provider": "tämä isännöity palveluntarjoaja",
|
||||||
|
"summary.select_generate": "Valitse kokous luodaksesi yhteenvedon.",
|
||||||
|
"summary.decisions_heading": "Päätökset",
|
||||||
|
"summary.regenerate": "Luo uudelleen",
|
||||||
|
"summary.generated_hint": "Luotu paikallisesti kokouksen jälkeen (vaatii paikallisen LLM-palveluntarjoajan).",
|
||||||
|
"summary.no_provider": "Tekoälyn palveluntarjoajaa ei ole määritetty — ota yksi käyttöön Asetuksissa.",
|
||||||
|
"summary.generate_summary": "Luo yhteenveto",
|
||||||
|
"summary.briefs_heading": "Ominaisuustiivistelmät",
|
||||||
|
"summary.briefs_select": "Valitse kokous luodaksesi tai nähdäksesi ominaisuustiivistelmiä.",
|
||||||
|
"summary.briefs_desc": "Tiivistää tämän kokouksen agentille valmiiksi määrittelyksi (vaatii paikallisen LLM-palveluntarjoajan) — anna se koodausagentille tai tarjoa se MCP-palvelimen kautta, kun se on päällä.",
|
||||||
|
"summary.brief_repo_placeholder": "Kohderepo (valinnainen), esim. acme/reporting-web",
|
||||||
|
"summary.distilling": "Tiivistetään…",
|
||||||
|
"summary.create_brief": "Luo ominaisuustiivistelmä",
|
||||||
|
"summary.brief_mcp_title": "Saatavilla, kun MCP-palvelin on päällä",
|
||||||
|
"summary.copied": "Kopioitu",
|
||||||
|
"summary.copy_md": "Kopioi Markdownina",
|
||||||
|
"summary.copy_json": "Kopioi JSONina",
|
||||||
|
"summary.brief_problem": "Ongelma",
|
||||||
|
"summary.brief_outcome": "Haluttu lopputulos",
|
||||||
|
"summary.brief_criteria": "Hyväksymiskriteerit",
|
||||||
|
"summary.brief_none": "Ei mitään tallennettu.",
|
||||||
|
"summary.brief_context": "Konteksti",
|
||||||
|
"summary.action_items_heading": "Tehtävät",
|
||||||
|
"summary.ai_select": "Valitse kokous nähdäksesi sen tehtävät.",
|
||||||
|
"summary.ai_empty": "Ei vielä yhtään — lisää yksi alle tai luo yhteenveto poimiaksesi ne automaattisesti.",
|
||||||
|
"summary.confirmed": "Vahvistettu",
|
||||||
|
"summary.ai_text_placeholder": "Tehtävä…",
|
||||||
|
"summary.ai_text_aria": "Tehtävän teksti",
|
||||||
|
"summary.owner": "Vastuuhenkilö",
|
||||||
|
"summary.due_date": "Määräpäivä",
|
||||||
|
"summary.reminder_title": "Ajasta paikallinen muistutus tälle määräpäivälle",
|
||||||
|
"summary.ai_delete_title": "Poista tämä tehtävä",
|
||||||
|
"summary.ai_delete_aria": "Poista tehtävä",
|
||||||
|
"summary.add_action_item": "Lisää tehtävä",
|
||||||
|
"summary.save_action_items": "Tallenna tehtävät",
|
||||||
|
"summary.calendar_heading": "Kalenteritapahtuma",
|
||||||
|
"summary.cal_select": "Valitse kokous liittääksesi sen kalenteritapahtumaan.",
|
||||||
|
"summary.event_search_placeholder": "Hae tapahtuman otsikkoa…",
|
||||||
|
"summary.linked_event": "Linkitetty tapahtuma",
|
||||||
|
"summary.change_event": "Vaihda tapahtuma…",
|
||||||
|
"summary.link_event": "Linkitä tapahtuma…",
|
||||||
|
"summary.untitled_event": "(nimetön)",
|
||||||
|
"summary.no_events_1": "Tapahtumia ei ole vielä tuotu — tuo",
|
||||||
|
"summary.no_events_2": "kohdasta Asetukset → Kalenteri.",
|
||||||
|
"summary.no_events_match": "Yksikään tapahtuma ei vastaa tätä hakua/päivämäärää — yritä tyhjentää yksi.",
|
||||||
|
"summary.organizer_label": "Järjestäjä: {name}",
|
||||||
|
"summary.participants_heading": "Osallistujat",
|
||||||
|
"summary.participants_hint": "Täytetään linkitetystä kalenteritapahtumasta.",
|
||||||
|
"summary.speakers_heading": "Puhujat",
|
||||||
|
"summary.speakers_select": "Valitse kokous nimetäksesi sen puhujat.",
|
||||||
|
"summary.no_speakers": "Puhujia ei ole vielä havaittu.",
|
||||||
|
"summary.speaker_name_placeholder": "Puhujan nimi",
|
||||||
|
"summary.save": "Tallenna",
|
||||||
|
"summary.cancel": "Peruuta",
|
||||||
|
"summary.name_speaker": "Nimeä tämä puhuja…",
|
||||||
|
"summary.add_new_name": "+ Lisää uusi nimi…",
|
||||||
|
|
||||||
|
"settings.recording.title": "Tallennus",
|
||||||
|
"settings.recording.record_default": "Tallenna kokoukset oletuksena",
|
||||||
|
"settings.recording.save_audio_as": "tallenna ääni muodossa",
|
||||||
|
"settings.recording.default_hint": "Oletuksena pois. Kun pois, ääntä käytetään vain litteroinnin tuottamiseen ja se poistetaan, kun kokous viimeistellään. Voit myös vaihtaa tallennusta kokouskohtaisesti.",
|
||||||
|
"settings.recording.consent_label": "Suostumus: {status}.",
|
||||||
|
"settings.recording.consent_ack": "vahvistettu",
|
||||||
|
"settings.recording.consent_not": "ei vielä vahvistettu",
|
||||||
|
"settings.recording.auto_label": "Aloita tallennus automaattisesti, kun kalenteritapahtuma alkaa",
|
||||||
|
"settings.recording.auto_hint": "Vain kun WhispAssist on auki. Kun tuodun kalenteritapahtuman alkamisaika koittaa, tallennus alkaa automaattisesti (käyttäen yllä olevaa oletussäilytysasetustasi). Mikään ei suoritu taustalla — ajastin on viritetty vain sovelluksen ollessa käynnissä. Tuo tapahtumia kohdasta Asetukset → Kalenteri."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "Enregistrement",
|
||||||
|
"nav.hardware": "Matériel",
|
||||||
|
"nav.storage": "Stockage",
|
||||||
|
"nav.calendar": "Calendrier",
|
||||||
|
"nav.sync": "Synchronisation",
|
||||||
|
"nav.ai": "IA",
|
||||||
|
"nav.mcp": "Serveur MCP",
|
||||||
|
"nav.privacy": "Confidentialité",
|
||||||
|
"nav.about": "À propos",
|
||||||
|
"nav.language": "Langue",
|
||||||
|
|
||||||
|
"settings.language.title": "Langue",
|
||||||
|
"settings.language.display": "Langue d'affichage",
|
||||||
|
"settings.language.display_hint": "La langue de l'interface de l'application. Ajouter une langue est aussi simple que de déposer un seul fichier de traduction.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "Langue de transcription",
|
||||||
|
"settings.transcription.label": "Langue",
|
||||||
|
"settings.transcription.auto": "Détection automatique",
|
||||||
|
"settings.transcription.applies_hint": "S'applique au prochain enregistrement. La langue réellement utilisée est ensuite affichée sur chaque réunion.",
|
||||||
|
"settings.transcription.english_only": "Anglais seulement",
|
||||||
|
"settings.transcription.model_english_only": "« {model} » est en anglais seulement.",
|
||||||
|
"settings.transcription.no_model": "Aucun modèle sélectionné.",
|
||||||
|
"settings.transcription.switch_multilingual": "Passez à un modèle multilingue ci-dessus pour choisir une langue.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "Matériel",
|
||||||
|
"settings.hardware.active_backend": "Moteur actif",
|
||||||
|
"settings.hardware.model_meta": "· modèle {size}",
|
||||||
|
"settings.hardware.preferred_backend": "Moteur préféré",
|
||||||
|
"settings.hardware.auto_backend": "Automatique (le meilleur disponible)",
|
||||||
|
"settings.hardware.not_available": "non disponible",
|
||||||
|
"settings.hardware.fallback_hint": "Bascule automatiquement (NPU → NVIDIA → AMD → Intel → CPU) si le moteur choisi ne se charge pas.",
|
||||||
|
"settings.hardware.audio_devices": "Périphériques audio",
|
||||||
|
"settings.hardware.recording_device": "Périphérique d'enregistrement",
|
||||||
|
"settings.hardware.default_system_audio": "Audio système par défaut",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist enregistre ce que ce périphérique restitue (loopback) — l'autre côté de l'appel. Choisissez une sortie précise si vous ne voulez pas qu'il suive le périphérique par défaut de Windows.",
|
||||||
|
"settings.hardware.microphone": "Microphone",
|
||||||
|
"settings.hardware.mic_off": "Désactivé — ne pas capturer mon microphone",
|
||||||
|
"settings.hardware.default_mic": "Microphone par défaut",
|
||||||
|
"settings.hardware.mic_hint": "Ajoute votre propre voix à la transcription en direct afin que les deux côtés de la réunion soient captés. Reste sur votre appareil — rien n'est téléversé. Choisissez « Désactivé » pour ne transcrire que l'audio système ci-dessus.",
|
||||||
|
"settings.hardware.npu_detected": "Accélérateur NPU détecté",
|
||||||
|
"settings.hardware.ready": "Prêt",
|
||||||
|
"settings.hardware.downloading": "Téléchargement…",
|
||||||
|
"settings.hardware.package_needed": "Paquet requis",
|
||||||
|
"settings.hardware.npu_hint": "Votre NPU nécessite un paquet d'accélération unique (modèle Whisper ONNX{extra}). Il se télécharge en arrière-plan ; vous pouvez aussi le démarrer maintenant.",
|
||||||
|
"settings.hardware.plus_openvino": " + environnement d'exécution OpenVINO",
|
||||||
|
"settings.hardware.download_npu": "Télécharger le paquet NPU",
|
||||||
|
"settings.hardware.dml_title": "Accélération GPU (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "Votre GPU peut être accéléré sans Vulkan via DirectML — un paquet unique (modèle Whisper ONNX{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + environnement d'exécution DirectML",
|
||||||
|
"settings.hardware.download_dml": "Télécharger le paquet DirectML",
|
||||||
|
"settings.hardware.detection_unavailable": "Détection du matériel indisponible.",
|
||||||
|
"settings.hardware.low_overhead": "Préréglage à faible charge",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + plus petit modèle, pour une utilisation sur batterie/en arrière-plan",
|
||||||
|
"settings.hardware.models": "Modèles",
|
||||||
|
"settings.hardware.downloading_model": "Téléchargement de {name}",
|
||||||
|
"settings.hardware.active_badge": "Actif",
|
||||||
|
"settings.hardware.installed": "Installé",
|
||||||
|
"settings.hardware.use": "Utiliser",
|
||||||
|
"settings.hardware.remove": "Supprimer",
|
||||||
|
"settings.hardware.cant_remove_active": "Impossible de supprimer le modèle actif",
|
||||||
|
"settings.hardware.download": "Télécharger",
|
||||||
|
"settings.hardware.diarization": "Diarisation des locuteurs",
|
||||||
|
"settings.hardware.diarization_hint": "Installez les deux modèles pour que les enregistrements terminés soient séparés par locuteur (Locuteur 1, Locuteur 2, …) au lieu d'une seule transcription continue. Fonctionne entièrement hors ligne, une fois par réunion après son arrêt. Sans eux, chaque ligne est attribuée à un seul locuteur.",
|
||||||
|
|
||||||
|
"settings.storage.title": "Stockage",
|
||||||
|
"settings.storage.location_label": "Emplacement",
|
||||||
|
"settings.storage.location_hint": "Changer l'emplacement de stockage n'est pas encore pris en charge — c'est ici que résident actuellement les réunions, les modèles et la base de données.",
|
||||||
|
"settings.storage.retention_title": "Conservation",
|
||||||
|
"settings.storage.retention_hint": "Supprime automatiquement les réunions les plus anciennes au-delà d'une limite. Vérifié une fois au démarrage ; vide signifie aucune limite. Ne touche jamais à une réunion en cours d'enregistrement.",
|
||||||
|
"settings.storage.max_age": "Âge maximal (jours)",
|
||||||
|
"settings.storage.max_size": "Taille maximale (Go)",
|
||||||
|
"settings.storage.no_limit": "aucune limite",
|
||||||
|
"settings.storage.export_import_title": "Exportation et importation",
|
||||||
|
"settings.storage.export_hint_1": "Déplacez les enregistrements entre ordinateurs sous forme de dossiers de paquets portables (audio, transcription, notes, résumé et un",
|
||||||
|
"settings.storage.export_hint_2": "manifeste). Exportez vers n'importe quel dossier — un lecteur synchronisé, une clé USB ou le point de montage local d'une cible de synchronisation — puis importez-le sur l'autre machine. Les réunions importées obtiennent un nouvel identifiant, de sorte qu'une réimportation n'écrase jamais rien.",
|
||||||
|
"settings.storage.export_dialog_title": "Exporter toutes les réunions vers…",
|
||||||
|
"settings.storage.import_dialog_title": "Importer un paquet (ou un dossier de paquets)…",
|
||||||
|
"settings.storage.exporting": "Exportation…",
|
||||||
|
"settings.storage.export_all": "Exporter toutes les réunions…",
|
||||||
|
"settings.storage.importing": "Importation…",
|
||||||
|
"settings.storage.import_all": "Importer des réunions…",
|
||||||
|
"settings.storage.exported_one": "{n} réunion exportée.",
|
||||||
|
"settings.storage.exported_many": "{n} réunions exportées.",
|
||||||
|
"settings.storage.imported_one": "{n} réunion importée.",
|
||||||
|
"settings.storage.imported_many": "{n} réunions importées.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "Calendrier et Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "Importez des événements et des participants depuis une sauvegarde Outlook locale",
|
||||||
|
"settings.calendar.intro_2": "— en lecture seule, rien n'est réécrit dans le fichier. Rien ne quitte cet appareil.",
|
||||||
|
"settings.calendar.pst_file": "Fichier .pst",
|
||||||
|
"settings.calendar.no_file": "Aucun fichier sélectionné",
|
||||||
|
"settings.calendar.browse": "Parcourir…",
|
||||||
|
"settings.calendar.password": "Mot de passe",
|
||||||
|
"settings.calendar.password_note": "rarement nécessaire",
|
||||||
|
"settings.calendar.optional": "facultatif",
|
||||||
|
"settings.calendar.import_range": "Plage d'importation",
|
||||||
|
"settings.calendar.range_30": "30 derniers jours",
|
||||||
|
"settings.calendar.range_90": "90 derniers jours",
|
||||||
|
"settings.calendar.range_180": "6 derniers mois",
|
||||||
|
"settings.calendar.range_365": "Dernière année",
|
||||||
|
"settings.calendar.range_all": "Toute la période",
|
||||||
|
"settings.calendar.range_hint": "Une boîte de courriel ancienne peut contenir des années d'entrées récurrentes/de congés — réduire la plage limite le calendrier importé à ce qui est réellement pertinent. S'applique à ce bouton d'importation ainsi qu'à la réimportation automatique au lancement.",
|
||||||
|
"settings.calendar.importing": "Importation…",
|
||||||
|
"settings.calendar.import": "Importer",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} événements",
|
||||||
|
"settings.calendar.auto_reimport": "Réimporter ce fichier automatiquement au lancement",
|
||||||
|
"settings.calendar.auto_reimport_hint": "S'exécute une fois au démarrage, pas sur une minuterie — la réimportation peut être répétée sans risque (les événements existants sont mis en correspondance et mis à jour, pas dupliqués).",
|
||||||
|
"settings.calendar.import_failed": "Échec de l'importation : {error} — le fichier lui-même n'est pas modifié ; vérifiez le chemin et réessayez.",
|
||||||
|
"settings.calendar.cleanup_title": "Nettoyer",
|
||||||
|
"settings.calendar.cleanup_hint": "Les événements déjà rattachés à une réunion enregistrée sont toujours conservés, quelle que soit l'option choisie ci-dessous.",
|
||||||
|
"settings.calendar.cleanup_30": "Plus anciens que 30 jours",
|
||||||
|
"settings.calendar.cleanup_90": "Plus anciens que 90 jours",
|
||||||
|
"settings.calendar.cleanup_180": "Plus anciens que 6 mois",
|
||||||
|
"settings.calendar.cleanup_365": "Plus anciens que 1 an",
|
||||||
|
"settings.calendar.cleanup_all": "Tout supprimer",
|
||||||
|
"settings.calendar.cleaning": "Nettoyage…",
|
||||||
|
"settings.calendar.cleanup_btn": "Nettoyer le calendrier",
|
||||||
|
"settings.calendar.cleanup_result": "{deleted}{extra} supprimés.",
|
||||||
|
"settings.calendar.cleanup_kept": ", {n} conservés (liés à une réunion)",
|
||||||
|
"settings.calendar.cleanup_failed": "Échec du nettoyage : {error}",
|
||||||
|
"settings.calendar.imported_events": "Événements importés",
|
||||||
|
"settings.calendar.no_events": "Aucun événement importé pour l'instant.",
|
||||||
|
"settings.calendar.search_title": "Rechercher un titre",
|
||||||
|
"settings.calendar.search_placeholder": "Nom de la réunion…",
|
||||||
|
"settings.calendar.date_label": "Date",
|
||||||
|
"settings.calendar.events_count": "{shown} sur {total} événements",
|
||||||
|
"settings.calendar.untitled": "(sans titre)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "Paramètres",
|
||||||
|
"settings.close": "Fermer",
|
||||||
|
"settings.close_title": "Fermer (Échap)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "Mode aperçu — le moteur de synchronisation n'est pas encore implémenté (Phase 9). Les modifications ne sont conservées que dans l'interface.",
|
||||||
|
"settings.sync.title": "Synchronisation et téléversement",
|
||||||
|
"settings.sync.enable_label": "Activer le téléversement des artefacts de réunion vers les cibles configurées",
|
||||||
|
"settings.sync.enable_hint": "Désactivé par défaut. Rien n'est téléversé sauf si ceci est activé et qu'une cible est activée. Les cibles auto-hébergées conservent les données sur votre propre serveur ; les nuages tiers sont clairement étiquetés.",
|
||||||
|
"settings.sync.targets_title": "Cibles",
|
||||||
|
"settings.sync.no_targets": "Aucune cible pour l'instant. Ajoutez-en une ci-dessous.",
|
||||||
|
"settings.sync.third_party": "tiers",
|
||||||
|
"settings.sync.your_server": "votre serveur",
|
||||||
|
"settings.sync.edit": "Modifier",
|
||||||
|
"settings.sync.remove": "Supprimer",
|
||||||
|
"settings.sync.edit_target": "Modifier la cible",
|
||||||
|
"settings.sync.add_target": "Ajouter une cible",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV couvre Nextcloud, ownCloud, Cloudreve, Seafile et Synology. (Seafile : activez SeafDAV côté serveur.)",
|
||||||
|
"settings.sync.name": "Nom",
|
||||||
|
"settings.sync.name_placeholder": "Nextcloud maison",
|
||||||
|
"settings.sync.provider": "Fournisseur",
|
||||||
|
"settings.sync.server_url": "URL du serveur",
|
||||||
|
"settings.sync.server_url_note_1": "Juste l'URL de votre serveur — WhispAssist ajoute",
|
||||||
|
"settings.sync.server_url_note_2": "automatiquement.",
|
||||||
|
"settings.sync.remote_folder": "Dossier distant",
|
||||||
|
"settings.sync.username": "Nom d'utilisateur",
|
||||||
|
"settings.sync.app_password": "Mot de passe d'application",
|
||||||
|
"settings.sync.pw_keep": "laissez vide pour conserver le mot de passe actuel",
|
||||||
|
"settings.sync.pw_store": "stocké dans le gestionnaire d'identifiants du système d'exploitation",
|
||||||
|
"settings.sync.upload_legend": "Téléverser",
|
||||||
|
"settings.sync.artifact_transcript": "transcription",
|
||||||
|
"settings.sync.artifact_notes": "notes",
|
||||||
|
"settings.sync.artifact_summary": "résumé",
|
||||||
|
"settings.sync.artifact_recording": "enregistrement (.wav, si conservé)",
|
||||||
|
"settings.sync.allow_plaintext": "Autoriser le http en clair pour une adresse LAN (non recommandé)",
|
||||||
|
"settings.sync.encrypt_before": "Chiffrer avant le téléversement (la destination ne stocke que le texte chiffré)",
|
||||||
|
"settings.sync.requires_vault": "— nécessite un coffre déverrouillé",
|
||||||
|
"settings.sync.test_connection": "Tester la connexion",
|
||||||
|
"settings.sync.save_changes": "Enregistrer les modifications",
|
||||||
|
"settings.sync.add_target_btn": "Ajouter la cible",
|
||||||
|
"settings.sync.cancel": "Annuler",
|
||||||
|
"settings.sync.third_party_banner": "{kind} est un nuage tiers — le téléversement envoie vos données hors de votre appareil vers {kind}.",
|
||||||
|
"settings.sync.link_account": "Lier le compte {kind}…",
|
||||||
|
"settings.sync.oauth_hint": "Ouvre une connexion OAuth (redirection loopback) ; le jeton est stocké dans le gestionnaire d'identifiants du système d'exploitation.",
|
||||||
|
"settings.sync.footnote": "Les identifiants ne sont jamais écrits dans les paramètres ni dans la base de données — uniquement dans le gestionnaire d'identifiants du système d'exploitation. TLS est requis pour les cibles hors LAN.",
|
||||||
|
|
||||||
|
"settings.ai.title": "Fournisseur de résumés IA",
|
||||||
|
"settings.ai.intro_1": "Les résumés s'exécutent par défaut sur un LLM local. Pointez ceci vers Ollama sur ce PC ou une autre machine de votre LAN (par ex.",
|
||||||
|
"settings.ai.intro_2": "— les deux comptent comme local, donc rien ne quitte votre réseau. Les fournisseurs hébergés (Anthropic) sont facultatifs, désactivés par défaut, et envoient la transcription à un tiers dès que vous en activez un.",
|
||||||
|
"settings.ai.provider": "Fournisseur",
|
||||||
|
"settings.ai.off": "Désactivé",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (local / LAN)",
|
||||||
|
"settings.ai.provider_custom": "Personnalisé (compatible OpenAI)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — hébergé, quitte cet appareil",
|
||||||
|
"settings.ai.endpoint": "Point de terminaison",
|
||||||
|
"settings.ai.model": "Modèle",
|
||||||
|
"settings.ai.api_key": "Clé API",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (déjà définie — laissez vide pour la conserver)",
|
||||||
|
"settings.ai.show_api_key": "Afficher la clé API",
|
||||||
|
"settings.ai.hide_api_key": "Masquer la clé API",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic est un service hébergé tiers — la transcription de cette réunion quitte votre appareil lorsque vous générez un résumé.",
|
||||||
|
"settings.ai.endpoint_banner": "Ce point de terminaison n'est pas sur votre machine ni sur votre LAN — votre transcription quitterait votre réseau.",
|
||||||
|
"settings.ai.saving": "Enregistrement…",
|
||||||
|
"settings.ai.save": "Enregistrer",
|
||||||
|
"settings.ai.test_connection": "Tester la connexion",
|
||||||
|
"settings.ai.this_hosted_provider": "ce fournisseur hébergé",
|
||||||
|
"settings.ai.reachable": "Joignable",
|
||||||
|
"settings.ai.unreachable": "Injoignable",
|
||||||
|
"settings.ai.model_count_one": "· {n} modèle",
|
||||||
|
"settings.ai.model_count_many": "· {n} modèles",
|
||||||
|
"settings.ai.on_lan": "· sur votre machine / LAN",
|
||||||
|
"settings.ai.leaves_network": "· quitte votre réseau",
|
||||||
|
"settings.ai.advanced_title": "Configuration avancée d'Ollama",
|
||||||
|
"settings.ai.changed": "{n} modifiés",
|
||||||
|
"settings.ai.reset_all": "Tout réinitialiser aux valeurs par défaut",
|
||||||
|
"settings.ai.system_prompt": "Invite système",
|
||||||
|
"settings.ai.system_prompt_placeholder": "par ex. Vous êtes un rédacteur de résumés de réunion concis.",
|
||||||
|
"settings.ai.system_prompt_hint": "Ajoutée avant le format de sortie requis par WhispAssist, afin que vos instructions ne puissent pas casser l'analyse du résumé/des actions à mener.",
|
||||||
|
"settings.ai.think": "Réfléchir",
|
||||||
|
"settings.ai.think_help": "Effort de raisonnement (modèles de raisonnement uniquement).",
|
||||||
|
"settings.ai.keep_alive": "Maintenir en vie",
|
||||||
|
"settings.ai.keep_alive_help": "Durée pendant laquelle le modèle reste en RAM. par ex. 5m, 1h, 0 (décharger), -1 (indéfiniment).",
|
||||||
|
"settings.ai.runtime_hardware": "Exécution et matériel",
|
||||||
|
"settings.ai.rarely_needed": "— rarement nécessaire",
|
||||||
|
"settings.ai.save_advanced": "Enregistrer les options avancées",
|
||||||
|
"settings.ai.saved": "Enregistré",
|
||||||
|
"settings.ai.turn_off": "Désactiver",
|
||||||
|
"settings.ai.default_value": "Par défaut {value}.",
|
||||||
|
"settings.ai.reset_default": "Réinitialiser à la valeur par défaut",
|
||||||
|
"settings.ai.reset_field": "Réinitialiser {name}",
|
||||||
|
|
||||||
|
"settings.mcp.title": "Serveur MCP",
|
||||||
|
"settings.mcp.banner_1": "Cela permet à votre propre agent de codage (Claude Code, Codex, Copilot, OpenCode, …) de récupérer le contexte de la réunion sur votre machine locale. Une fois connecté,",
|
||||||
|
"settings.mcp.banner_agent": "cet agent",
|
||||||
|
"settings.mcp.banner_2": "peut transmettre ce qu'il lit au nuage de son propre fournisseur de modèle — hors du contrôle de WhispAssist. WhispAssist lui-même n'envoie jamais ces données où que ce soit ; le serveur n'écoute que sur cet appareil (",
|
||||||
|
"settings.mcp.banner_3": ") et chaque lecture est journalisée ci-dessous.",
|
||||||
|
"settings.mcp.enable": "Activer le serveur MCP",
|
||||||
|
"settings.mcp.enable_hint": "Désactivé par défaut. Loopback uniquement, protégé par jeton — rien n'est accessible depuis le réseau.",
|
||||||
|
"settings.mcp.transport": "Transport",
|
||||||
|
"settings.mcp.transport_http": "HTTP en flux continu",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (l'agent lance un processus)",
|
||||||
|
"settings.mcp.port": "Port",
|
||||||
|
"settings.mcp.transport_change_hint": "Pour changer de transport/port, désactivez d'abord le serveur, puis réactivez-le.",
|
||||||
|
"settings.mcp.scope_title": "Portée",
|
||||||
|
"settings.mcp.expose": "Exposer",
|
||||||
|
"settings.mcp.scope_none": "Aucune — rien n'est partagé",
|
||||||
|
"settings.mcp.scope_selected": "Sélectionnés — uniquement les briefs de fonctionnalité que vous avez marqués comme partagés",
|
||||||
|
"settings.mcp.scope_all": "Tout — réunions, transcriptions, actions à mener et briefs partagés",
|
||||||
|
"settings.mcp.expose_recordings": "Autoriser aussi les réunions avec un enregistrement sauvegardé (désactivé par défaut — la transcription d'une réunion enregistrée est retenue même en portée « Tout » jusqu'à ce que ceci soit activé)",
|
||||||
|
"settings.mcp.token_title": "Nouveau jeton d'authentification — affiché une seule fois, copiez-le maintenant",
|
||||||
|
"settings.mcp.token_hint": "Ceci ne sera plus affiché. Il est stocké dans le gestionnaire d'identifiants du système d'exploitation ; si vous le perdez, désactivez puis réactivez le serveur pour en générer un nouveau.",
|
||||||
|
"settings.mcp.copied": "Copié",
|
||||||
|
"settings.mcp.copy": "Copier",
|
||||||
|
"settings.mcp.endpoint": "Point de terminaison",
|
||||||
|
"settings.mcp.endpoint_hint": "Pointez la configuration du client MCP de votre agent vers {kind}, avec le jeton ci-dessus comme identifiant bearer.",
|
||||||
|
"settings.mcp.kind_command": "commande",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "Journal d'accès",
|
||||||
|
"settings.mcp.access_log_hint": "Chaque lecture d'outil effectuée par un agent, autorisée ou refusée (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "Aucun agent n'a encore rien lu.",
|
||||||
|
"settings.mcp.log_meeting": "réunion {id}",
|
||||||
|
"settings.mcp.refresh": "Actualiser",
|
||||||
|
|
||||||
|
"settings.privacy.title": "Confidentialité",
|
||||||
|
"settings.privacy.intro": "Ce que WhispAssist est réellement autorisé à envoyer hors de cet appareil actuellement. Avec tout désactivé (le réglage par défaut), rien ne quitte l'appareil.",
|
||||||
|
"settings.privacy.llm_endpoint": "Point de terminaison LLM",
|
||||||
|
"settings.privacy.off": "désactivé",
|
||||||
|
"settings.privacy.local_only": "local uniquement",
|
||||||
|
"settings.privacy.leaves_device": "quitte cet appareil",
|
||||||
|
"settings.privacy.sync_label": "Synchronisation",
|
||||||
|
"settings.privacy.enabled": "activée",
|
||||||
|
"settings.privacy.mcp_label": "Serveur MCP",
|
||||||
|
"settings.privacy.mcp_on": "activé · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "Entrant sur loopback uniquement — n'ajoute rien à la liste de sortie ci-dessus. Un agent connecté peut néanmoins transmettre ce qu'il lit à son propre fournisseur de modèle ; voir l'onglet du serveur MCP.",
|
||||||
|
"settings.privacy.egress_title": "Liste d'autorisation de sortie",
|
||||||
|
"settings.privacy.no_egress": "Aucun hôte n'est autorisé — WA n'effectue aucune sortie de contenu.",
|
||||||
|
"settings.privacy.sync_targets_title": "Cibles de synchronisation",
|
||||||
|
"settings.privacy.third_party": "tiers",
|
||||||
|
"settings.privacy.your_server": "votre serveur",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "sans TLS",
|
||||||
|
"settings.privacy.refresh": "Actualiser",
|
||||||
|
"settings.privacy.unavailable": "Autovérification de confidentialité indisponible.",
|
||||||
|
"settings.privacy.vault_title": "Coffre de chiffrement",
|
||||||
|
"settings.privacy.vault_intro": "Chiffrez les notes, transcriptions et résumés au repos avec un mot de passe. (Les fichiers audio ne sont pas encore chiffrés.)",
|
||||||
|
"settings.privacy.vault_password": "Mot de passe du coffre",
|
||||||
|
"settings.privacy.enable_vault": "Activer le coffre",
|
||||||
|
"settings.privacy.vault_pw_hint": "Utilisez au moins 8 caractères. Si vous l'oubliez, le contenu chiffré ne pourra pas être récupéré.",
|
||||||
|
"settings.privacy.vault_locked_1": "Le coffre est ",
|
||||||
|
"settings.privacy.locked_word": "verrouillé",
|
||||||
|
"settings.privacy.vault_locked_2": ". Déverrouillez pour lire les réunions chiffrées.",
|
||||||
|
"settings.privacy.password": "Mot de passe",
|
||||||
|
"settings.privacy.unlock": "Déverrouiller",
|
||||||
|
"settings.privacy.vault_unlocked_1": "Le coffre est ",
|
||||||
|
"settings.privacy.unlocked_word": "déverrouillé",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". Les nouvelles notes, transcriptions et résumés sont chiffrés au repos.",
|
||||||
|
"settings.privacy.lock_now": "Verrouiller maintenant",
|
||||||
|
"settings.privacy.change_password": "Changer le mot de passe",
|
||||||
|
"settings.privacy.current_password": "Mot de passe actuel",
|
||||||
|
"settings.privacy.new_password": "Nouveau mot de passe",
|
||||||
|
|
||||||
|
"settings.about.title": "À propos",
|
||||||
|
"settings.about.tagline": "Un assistant de réunion entièrement local, à code source ouvert et natif Windows.",
|
||||||
|
"settings.about.build_commit": "Commit de compilation",
|
||||||
|
|
||||||
|
"consent.heading": "Avant d'enregistrer",
|
||||||
|
"consent.body": "Enregistrer des conversations sans le consentement des participants peut être illégal dans votre région. Vérifiez les lois locales sur l'enregistrement. Ceci est une mise en garde, pas un avis juridique.",
|
||||||
|
"consent.accept": "Je comprends — activer l'enregistrement",
|
||||||
|
"consent.cancel": "Annuler",
|
||||||
|
|
||||||
|
"hosted.heading": "Avant d'utiliser {provider}",
|
||||||
|
"hosted.body_1": "Générer avec",
|
||||||
|
"hosted.body_2": "envoie la transcription de cette réunion sur leurs serveurs — elle quitte cet appareil et est soumise à leur politique de confidentialité. WhispAssist n'a aucun contrôle sur le traitement des données une fois qu'elles quittent votre appareil. Ceci est désactivé par défaut ; vous le choisissez maintenant.",
|
||||||
|
"hosted.accept": "Je comprends — continuer",
|
||||||
|
"hosted.cancel": "Annuler",
|
||||||
|
|
||||||
|
"theme.group": "Thème",
|
||||||
|
"theme.system": "Suivre le système",
|
||||||
|
"theme.light": "Clair",
|
||||||
|
"theme.dark": "Sombre",
|
||||||
|
|
||||||
|
"import.dialog_label": "Ajouter une réunion depuis un fichier ou une URL",
|
||||||
|
"import.title": "Ajouter une réunion",
|
||||||
|
"import.close": "Fermer",
|
||||||
|
"import.close_title": "Fermer (Échap)",
|
||||||
|
"import.body": "Importez un enregistrement existant — un fichier audio/vidéo local ou un lien (YouTube, une page de diffusion en continu ou une URL média directe). Il est transcrit et diarisé comme un enregistrement en direct.",
|
||||||
|
"import.file_or_url": "Fichier ou URL",
|
||||||
|
"import.source_placeholder": "Collez une URL ou parcourez pour trouver un fichier…",
|
||||||
|
"import.choose_file": "Choisir un fichier local",
|
||||||
|
"import.browse": "Parcourir…",
|
||||||
|
"import.title_label": "Titre",
|
||||||
|
"import.optional": "facultatif",
|
||||||
|
"import.title_placeholder": "Par défaut, le nom du fichier",
|
||||||
|
"import.filter_av": "Audio / vidéo",
|
||||||
|
"import.requires_1": "Nécessite",
|
||||||
|
"import.requires_2": "installé et dans votre PATH (plus",
|
||||||
|
"import.requires_3": "pour les URL). WhispAssist ne les inclut pas.",
|
||||||
|
"import.importing": "Importation… cela peut prendre un moment",
|
||||||
|
"import.import": "Importer",
|
||||||
|
"import.cancel": "Annuler",
|
||||||
|
|
||||||
|
"tagchip.filter": "Filtrer les réunions marquées « {tag} »",
|
||||||
|
"tagchip.remove": "Supprimer l'étiquette {tag}",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "Niveau d'entrée du système et du microphone",
|
||||||
|
"levelmeter.system": "Niveau d'entrée du système",
|
||||||
|
|
||||||
|
"app.tagline": "local · privé",
|
||||||
|
"app.note_template": "Modèle de notes",
|
||||||
|
"app.no_template": "Aucun modèle",
|
||||||
|
"app.add_meeting": "Ajouter une réunion",
|
||||||
|
"app.add_meeting_title": "Ajouter une réunion depuis un fichier ou une URL",
|
||||||
|
"app.record": "Enregistrer",
|
||||||
|
"app.record_title": "Démarrer l'enregistrement (Ctrl+Maj+R)",
|
||||||
|
"app.stop": "Arrêter",
|
||||||
|
"app.stop_title": "Arrêter l'enregistrement (Ctrl+Maj+R)",
|
||||||
|
"app.cancel": "Annuler",
|
||||||
|
"app.cancel_title": "Abandonner cet enregistrement et le supprimer",
|
||||||
|
"app.recording": "Enregistrement…",
|
||||||
|
"app.backend_title": "Moteur de transcription actif",
|
||||||
|
"app.retention_title": "Enregistrer l'audio en .wav pour cette réunion",
|
||||||
|
"app.saving": "enregistrement",
|
||||||
|
"app.not_saved": "non enregistré",
|
||||||
|
"app.reconnecting": "reconnexion du périphérique audio…",
|
||||||
|
"app.settings": "Paramètres",
|
||||||
|
"app.settings_title": "Paramètres (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "Abandonner cet enregistrement ? Son audio et sa transcription seront supprimés.",
|
||||||
|
"app.announce_started": "Enregistrement démarré",
|
||||||
|
"app.announce_paused": "Enregistrement en pause",
|
||||||
|
"app.announce_stopped": "Enregistrement arrêté",
|
||||||
|
"app.consent_dialog": "Consentement à l'enregistrement",
|
||||||
|
"app.vault_dialog": "Déverrouiller le coffre de chiffrement",
|
||||||
|
"app.vault_title": "Déverrouiller le coffre de chiffrement",
|
||||||
|
"app.vault_desc": "Vos réunions sont chiffrées au repos. Saisissez votre mot de passe pour les lire.",
|
||||||
|
"app.vault_password": "Mot de passe du coffre",
|
||||||
|
"app.vault_incorrect": "Mot de passe incorrect",
|
||||||
|
"app.unlock": "Déverrouiller",
|
||||||
|
"app.continue_locked": "Continuer verrouillé",
|
||||||
|
"app.show_meetings": "Afficher la liste des réunions",
|
||||||
|
"app.hide_meetings": "Masquer la liste des réunions",
|
||||||
|
"app.show_summary": "Afficher le panneau de résumé",
|
||||||
|
"app.hide_summary": "Masquer le panneau de résumé",
|
||||||
|
"app.resize_meetings": "Redimensionner la liste des réunions",
|
||||||
|
"app.resize_summary": "Redimensionner le panneau de résumé",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "Rechercher des réunions…",
|
||||||
|
"meetings.search_aria": "Rechercher des réunions",
|
||||||
|
"meetings.filter_tag_aria": "Filtrer par étiquette",
|
||||||
|
"meetings.all_tags": "Toutes les étiquettes",
|
||||||
|
"meetings.from_date_aria": "À partir de la date",
|
||||||
|
"meetings.to_date_aria": "Jusqu'à la date",
|
||||||
|
"meetings.bulk_format_aria": "Format d'exportation groupée",
|
||||||
|
"meetings.bulk_export": "Exportation groupée",
|
||||||
|
"meetings.bulk_export_title": "Exporter toutes les réunions correspondant aux filtres d'étiquette/date ci-dessus",
|
||||||
|
"meetings.exporting": "Exportation…",
|
||||||
|
"meetings.exported_one": "{n} réunion exportée.",
|
||||||
|
"meetings.exported_many": "{n} réunions exportées.",
|
||||||
|
"meetings.searching": "Recherche…",
|
||||||
|
"meetings.loading": "Chargement des réunions…",
|
||||||
|
"meetings.no_matches": "Aucun résultat.",
|
||||||
|
"meetings.empty": "Aucune réunion pour l'instant — cliquez sur Enregistrer ci-dessus pour commencer.",
|
||||||
|
"meetings.status.recording": "enregistrement",
|
||||||
|
"meetings.status.transcribing": "transcription",
|
||||||
|
"meetings.status.recovering": "récupération",
|
||||||
|
"meetings.status.error": "erreur",
|
||||||
|
"meetings.resume": "Reprendre la transcription",
|
||||||
|
"meetings.delete_aria": "Supprimer la réunion",
|
||||||
|
"meetings.delete_confirm": "Supprimer cette réunion ? Cela supprime son enregistrement, sa transcription et ses notes.",
|
||||||
|
|
||||||
|
"transcript.heading": "Transcription",
|
||||||
|
"transcript.title_aria": "Titre de la réunion",
|
||||||
|
"transcript.lang_title": "Langue de transcription",
|
||||||
|
"transcript.lang_auto": "détection en cours…",
|
||||||
|
"transcript.show": "Afficher la transcription",
|
||||||
|
"transcript.hide": "Masquer la transcription",
|
||||||
|
"transcript.empty": "Aucune transcription pour cette réunion.",
|
||||||
|
"transcript.will_appear": "La transcription apparaîtra ici au fur et à mesure de l'enregistrement.",
|
||||||
|
"transcript.play_from_here": "Lire à partir d'ici",
|
||||||
|
"transcript.resize": "Redimensionner la transcription et les notes",
|
||||||
|
"transcript.reprocess_placeholder": "Re-transcrire avec…",
|
||||||
|
"transcript.reprocess_lang_aria": "Langue de retraitement",
|
||||||
|
"transcript.reprocess_keep_lang": "Conserver la langue actuelle",
|
||||||
|
"transcript.reprocess_go": "Lancer",
|
||||||
|
"transcript.retranscribing": "Re-transcription…",
|
||||||
|
"transcript.has_note": "Comporte une note",
|
||||||
|
"transcript.note_placeholder": "Ajouter une note pour ce moment…",
|
||||||
|
"transcript.note_aria": "Note pour cette ligne de transcription",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Document Word",
|
||||||
|
"transcript.filter_obsidian": "Note Obsidian",
|
||||||
|
|
||||||
|
"notes.heading": "Notes",
|
||||||
|
"notes.show": "Afficher les notes",
|
||||||
|
"notes.hide": "Masquer les notes",
|
||||||
|
"notes.placeholder": "Notes…",
|
||||||
|
"notes.live_placeholder": "Tapez des notes pendant que vous parlez…",
|
||||||
|
"notes.toolbar_aria": "Mise en forme des notes",
|
||||||
|
"notes.bold": "Gras",
|
||||||
|
"notes.italic": "Italique",
|
||||||
|
"notes.h1": "Titre 1",
|
||||||
|
"notes.h2": "Titre 2",
|
||||||
|
"notes.bullet": "Liste à puces",
|
||||||
|
"notes.checkbox_title": "Case à cocher",
|
||||||
|
"notes.checkbox_aria": "Élément de liste à cocher",
|
||||||
|
"notes.edit_raw": "Modifier le markdown brut",
|
||||||
|
"notes.render": "Afficher le markdown",
|
||||||
|
"notes.editor": "Éditeur",
|
||||||
|
"notes.preview": "Aperçu",
|
||||||
|
"notes.export_md_title": "Exporter les notes en .md",
|
||||||
|
"notes.export_pdf_title": "Exporter les notes en .pdf",
|
||||||
|
"notes.export_docx_title": "Exporter les notes en .docx",
|
||||||
|
"notes.export_bundle_title": "Exporter audio + transcription + notes vers un dossier",
|
||||||
|
"notes.export_obsidian_title": "Exporter une note Obsidian (notes + résumé + transcription, sans audio)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "Enregistrement",
|
||||||
|
"summary.loading_recording": "Chargement de l'enregistrement…",
|
||||||
|
"summary.sync_heading": "Synchronisation",
|
||||||
|
"summary.uploading": "Téléversement…",
|
||||||
|
"summary.upload_now": "Téléverser maintenant",
|
||||||
|
"summary.no_uploads": "Aucun téléversement pour cette réunion pour l'instant.",
|
||||||
|
"summary.retry": "réessayer",
|
||||||
|
"summary.tags_heading": "Étiquettes",
|
||||||
|
"summary.tags_select": "Sélectionnez une réunion pour l'étiqueter.",
|
||||||
|
"summary.tag_add_placeholder": "Ajouter une étiquette…",
|
||||||
|
"summary.tag_first_placeholder": "projet, client, sujet…",
|
||||||
|
"summary.generating": "Génération…",
|
||||||
|
"summary.generate_tags": "Générer des étiquettes",
|
||||||
|
"summary.saving": "Enregistrement…",
|
||||||
|
"summary.save_tags": "Enregistrer les étiquettes",
|
||||||
|
"summary.summary_heading": "Résumé",
|
||||||
|
"summary.ai_provider": "Fournisseur d'IA",
|
||||||
|
"summary.provider_off": "Désactivé",
|
||||||
|
"summary.provider_custom": "Personnalisé",
|
||||||
|
"summary.local": "local",
|
||||||
|
"summary.leaves_device": "quitte cet appareil",
|
||||||
|
"summary.this_hosted_provider": "ce fournisseur hébergé",
|
||||||
|
"summary.select_generate": "Sélectionnez une réunion pour générer un résumé.",
|
||||||
|
"summary.decisions_heading": "Décisions",
|
||||||
|
"summary.regenerate": "Régénérer",
|
||||||
|
"summary.generated_hint": "Généré localement après la réunion (nécessite un fournisseur LLM local).",
|
||||||
|
"summary.no_provider": "Aucun fournisseur d'IA configuré — activez-en un dans les Paramètres.",
|
||||||
|
"summary.generate_summary": "Générer un résumé",
|
||||||
|
"summary.briefs_heading": "Briefs de fonctionnalité",
|
||||||
|
"summary.briefs_select": "Sélectionnez une réunion pour créer ou consulter des briefs de fonctionnalité.",
|
||||||
|
"summary.briefs_desc": "Distille cette réunion en une spécification prête pour un agent (nécessite un fournisseur LLM local) — remettez-la à un agent de codage, ou servez-la via le serveur MCP une fois celui-ci activé.",
|
||||||
|
"summary.brief_repo_placeholder": "Dépôt cible (facultatif), par ex. acme/reporting-web",
|
||||||
|
"summary.distilling": "Distillation…",
|
||||||
|
"summary.create_brief": "Créer un brief de fonctionnalité",
|
||||||
|
"summary.brief_mcp_title": "Disponible lorsque le serveur MCP est activé",
|
||||||
|
"summary.copied": "Copié",
|
||||||
|
"summary.copy_md": "Copier en Markdown",
|
||||||
|
"summary.copy_json": "Copier en JSON",
|
||||||
|
"summary.brief_problem": "Problème",
|
||||||
|
"summary.brief_outcome": "Résultat souhaité",
|
||||||
|
"summary.brief_criteria": "Critères d'acceptation",
|
||||||
|
"summary.brief_none": "Rien de capturé.",
|
||||||
|
"summary.brief_context": "Contexte",
|
||||||
|
"summary.action_items_heading": "Actions à mener",
|
||||||
|
"summary.ai_select": "Sélectionnez une réunion pour voir ses actions à mener.",
|
||||||
|
"summary.ai_empty": "Aucune pour l'instant — ajoutez-en une ci-dessous, ou générez un résumé pour les extraire automatiquement.",
|
||||||
|
"summary.confirmed": "Confirmée",
|
||||||
|
"summary.ai_text_placeholder": "Action à mener…",
|
||||||
|
"summary.ai_text_aria": "Texte de l'action à mener",
|
||||||
|
"summary.owner": "Responsable",
|
||||||
|
"summary.due_date": "Date d'échéance",
|
||||||
|
"summary.reminder_title": "Planifier un rappel local pour cette date d'échéance",
|
||||||
|
"summary.ai_delete_title": "Supprimer cette action à mener",
|
||||||
|
"summary.ai_delete_aria": "Supprimer l'action à mener",
|
||||||
|
"summary.add_action_item": "Ajouter une action à mener",
|
||||||
|
"summary.save_action_items": "Enregistrer les actions à mener",
|
||||||
|
"summary.calendar_heading": "Événement du calendrier",
|
||||||
|
"summary.cal_select": "Sélectionnez une réunion pour la lier à un événement du calendrier.",
|
||||||
|
"summary.event_search_placeholder": "Rechercher un titre d'événement…",
|
||||||
|
"summary.linked_event": "Événement lié",
|
||||||
|
"summary.change_event": "Changer d'événement…",
|
||||||
|
"summary.link_event": "Lier un événement…",
|
||||||
|
"summary.untitled_event": "(sans titre)",
|
||||||
|
"summary.no_events_1": "Aucun événement importé pour l'instant — importez-en un",
|
||||||
|
"summary.no_events_2": "depuis Paramètres → Calendrier.",
|
||||||
|
"summary.no_events_match": "Aucun événement ne correspond à cette recherche/date — essayez d'en effacer une.",
|
||||||
|
"summary.organizer_label": "Organisateur : {name}",
|
||||||
|
"summary.participants_heading": "Participants",
|
||||||
|
"summary.participants_hint": "Renseigné à partir de l'événement du calendrier lié.",
|
||||||
|
"summary.speakers_heading": "Locuteurs",
|
||||||
|
"summary.speakers_select": "Sélectionnez une réunion pour nommer ses locuteurs.",
|
||||||
|
"summary.no_speakers": "Aucun locuteur détecté pour l'instant.",
|
||||||
|
"summary.speaker_name_placeholder": "Nom du locuteur",
|
||||||
|
"summary.save": "Enregistrer",
|
||||||
|
"summary.cancel": "Annuler",
|
||||||
|
"summary.name_speaker": "Nommer ce locuteur…",
|
||||||
|
"summary.add_new_name": "+ Ajouter un nouveau nom…",
|
||||||
|
|
||||||
|
"settings.recording.title": "Enregistrement",
|
||||||
|
"settings.recording.record_default": "Enregistrer les réunions par défaut",
|
||||||
|
"settings.recording.save_audio_as": "enregistrer l'audio en",
|
||||||
|
"settings.recording.default_hint": "Désactivé par défaut. Lorsqu'il est désactivé, l'audio n'est utilisé que pour produire la transcription et est supprimé lorsque la réunion est finalisée. Vous pouvez aussi activer l'enregistrement par réunion.",
|
||||||
|
"settings.recording.consent_label": "Consentement : {status}.",
|
||||||
|
"settings.recording.consent_ack": "accepté",
|
||||||
|
"settings.recording.consent_not": "pas encore accepté",
|
||||||
|
"settings.recording.auto_label": "Démarrer automatiquement l'enregistrement au début d'un événement du calendrier",
|
||||||
|
"settings.recording.auto_hint": "Uniquement lorsque WhispAssist est ouvert. Lorsque l'heure de début d'un événement de calendrier importé arrive, un enregistrement commence automatiquement (en utilisant votre réglage de conservation par défaut ci-dessus). Rien ne s'exécute en arrière-plan — la minuterie n'est armée que pendant que l'application est en cours d'exécution. Importez des événements sous Paramètres → Calendrier."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "Enregistrement",
|
||||||
|
"nav.hardware": "Matériel",
|
||||||
|
"nav.storage": "Stockage",
|
||||||
|
"nav.calendar": "Calendrier",
|
||||||
|
"nav.sync": "Synchronisation",
|
||||||
|
"nav.ai": "IA",
|
||||||
|
"nav.mcp": "Serveur MCP",
|
||||||
|
"nav.privacy": "Confidentialité",
|
||||||
|
"nav.about": "À propos",
|
||||||
|
"nav.language": "Langue",
|
||||||
|
|
||||||
|
"settings.language.title": "Langue",
|
||||||
|
"settings.language.display": "Langue d'affichage",
|
||||||
|
"settings.language.display_hint": "La langue de l'interface de l'application. Ajouter une langue est aussi simple que de déposer un seul fichier de traduction.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "Langue de transcription",
|
||||||
|
"settings.transcription.label": "Langue",
|
||||||
|
"settings.transcription.auto": "Détection automatique",
|
||||||
|
"settings.transcription.applies_hint": "S'applique au prochain enregistrement. La langue réellement utilisée est ensuite affichée sur chaque réunion.",
|
||||||
|
"settings.transcription.english_only": "Anglais uniquement",
|
||||||
|
"settings.transcription.model_english_only": "« {model} » est en anglais uniquement.",
|
||||||
|
"settings.transcription.no_model": "Aucun modèle sélectionné.",
|
||||||
|
"settings.transcription.switch_multilingual": "Passez à un modèle multilingue ci-dessus pour choisir une langue.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "Matériel",
|
||||||
|
"settings.hardware.active_backend": "Backend actif",
|
||||||
|
"settings.hardware.model_meta": "· modèle {size}",
|
||||||
|
"settings.hardware.preferred_backend": "Backend préféré",
|
||||||
|
"settings.hardware.auto_backend": "Automatique (le meilleur disponible)",
|
||||||
|
"settings.hardware.not_available": "non disponible",
|
||||||
|
"settings.hardware.fallback_hint": "Bascule automatiquement (NPU → NVIDIA → AMD → Intel → CPU) si le backend choisi ne se charge pas.",
|
||||||
|
"settings.hardware.audio_devices": "Périphériques audio",
|
||||||
|
"settings.hardware.recording_device": "Périphérique d'enregistrement",
|
||||||
|
"settings.hardware.default_system_audio": "Audio système par défaut",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist enregistre ce que ce périphérique restitue (loopback) — l'autre côté de l'appel. Choisissez une sortie précise si vous ne voulez pas qu'il suive le périphérique par défaut de Windows.",
|
||||||
|
"settings.hardware.microphone": "Microphone",
|
||||||
|
"settings.hardware.mic_off": "Désactivé — ne pas capturer mon microphone",
|
||||||
|
"settings.hardware.default_mic": "Microphone par défaut",
|
||||||
|
"settings.hardware.mic_hint": "Ajoute votre propre voix à la transcription en direct afin que les deux côtés de la réunion soient capturés. Reste sur votre appareil — rien n'est téléversé. Choisissez « Désactivé » pour ne transcrire que l'audio système ci-dessus.",
|
||||||
|
"settings.hardware.npu_detected": "Accélérateur NPU détecté",
|
||||||
|
"settings.hardware.ready": "Prêt",
|
||||||
|
"settings.hardware.downloading": "Téléchargement…",
|
||||||
|
"settings.hardware.package_needed": "Paquet requis",
|
||||||
|
"settings.hardware.npu_hint": "Votre NPU nécessite un paquet d'accélération unique (modèle Whisper ONNX{extra}). Il se télécharge en arrière-plan ; vous pouvez aussi le démarrer maintenant.",
|
||||||
|
"settings.hardware.plus_openvino": " + environnement d'exécution OpenVINO",
|
||||||
|
"settings.hardware.download_npu": "Télécharger le paquet NPU",
|
||||||
|
"settings.hardware.dml_title": "Accélération GPU (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "Votre GPU peut être accéléré sans Vulkan via DirectML — un paquet unique (modèle Whisper ONNX{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + environnement d'exécution DirectML",
|
||||||
|
"settings.hardware.download_dml": "Télécharger le paquet DirectML",
|
||||||
|
"settings.hardware.detection_unavailable": "Détection du matériel indisponible.",
|
||||||
|
"settings.hardware.low_overhead": "Préréglage à faible charge",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + plus petit modèle, pour une utilisation sur batterie/en arrière-plan",
|
||||||
|
"settings.hardware.models": "Modèles",
|
||||||
|
"settings.hardware.downloading_model": "Téléchargement de {name}",
|
||||||
|
"settings.hardware.active_badge": "Actif",
|
||||||
|
"settings.hardware.installed": "Installé",
|
||||||
|
"settings.hardware.use": "Utiliser",
|
||||||
|
"settings.hardware.remove": "Supprimer",
|
||||||
|
"settings.hardware.cant_remove_active": "Impossible de supprimer le modèle actif",
|
||||||
|
"settings.hardware.download": "Télécharger",
|
||||||
|
"settings.hardware.diarization": "Diarisation des locuteurs",
|
||||||
|
"settings.hardware.diarization_hint": "Installez les deux modèles pour que les enregistrements terminés soient séparés par locuteur (Locuteur 1, Locuteur 2, …) au lieu d'une seule transcription continue. Fonctionne entièrement hors ligne, une fois par réunion après son arrêt. Sans eux, chaque ligne est attribuée à un seul locuteur.",
|
||||||
|
|
||||||
|
"settings.storage.title": "Stockage",
|
||||||
|
"settings.storage.location_label": "Emplacement",
|
||||||
|
"settings.storage.location_hint": "Changer l'emplacement de stockage n'est pas encore pris en charge — c'est ici que résident actuellement les réunions, les modèles et la base de données.",
|
||||||
|
"settings.storage.retention_title": "Conservation",
|
||||||
|
"settings.storage.retention_hint": "Supprime automatiquement les réunions les plus anciennes au-delà d'une limite. Vérifié une fois au démarrage ; vide signifie aucune limite. Ne touche jamais à une réunion en cours d'enregistrement.",
|
||||||
|
"settings.storage.max_age": "Âge maximal (jours)",
|
||||||
|
"settings.storage.max_size": "Taille maximale (Go)",
|
||||||
|
"settings.storage.no_limit": "aucune limite",
|
||||||
|
"settings.storage.export_import_title": "Exportation et importation",
|
||||||
|
"settings.storage.export_hint_1": "Déplacez les enregistrements entre ordinateurs sous forme de dossiers de paquets portables (audio, transcription, notes, résumé et un",
|
||||||
|
"settings.storage.export_hint_2": "manifeste). Exportez vers n'importe quel dossier — un lecteur synchronisé, une clé USB ou le point de montage local d'une cible de synchronisation — puis importez-le sur l'autre machine. Les réunions importées obtiennent un nouvel identifiant, de sorte qu'une réimportation n'écrase jamais rien.",
|
||||||
|
"settings.storage.export_dialog_title": "Exporter toutes les réunions vers…",
|
||||||
|
"settings.storage.import_dialog_title": "Importer un paquet (ou un dossier de paquets)…",
|
||||||
|
"settings.storage.exporting": "Exportation…",
|
||||||
|
"settings.storage.export_all": "Exporter toutes les réunions…",
|
||||||
|
"settings.storage.importing": "Importation…",
|
||||||
|
"settings.storage.import_all": "Importer des réunions…",
|
||||||
|
"settings.storage.exported_one": "{n} réunion exportée.",
|
||||||
|
"settings.storage.exported_many": "{n} réunions exportées.",
|
||||||
|
"settings.storage.imported_one": "{n} réunion importée.",
|
||||||
|
"settings.storage.imported_many": "{n} réunions importées.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "Calendrier et Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "Importez des événements et des participants depuis une sauvegarde Outlook locale",
|
||||||
|
"settings.calendar.intro_2": "— en lecture seule, rien n'est réécrit dans le fichier. Rien ne quitte cet appareil.",
|
||||||
|
"settings.calendar.pst_file": "Fichier .pst",
|
||||||
|
"settings.calendar.no_file": "Aucun fichier sélectionné",
|
||||||
|
"settings.calendar.browse": "Parcourir…",
|
||||||
|
"settings.calendar.password": "Mot de passe",
|
||||||
|
"settings.calendar.password_note": "rarement nécessaire",
|
||||||
|
"settings.calendar.optional": "facultatif",
|
||||||
|
"settings.calendar.import_range": "Plage d'importation",
|
||||||
|
"settings.calendar.range_30": "30 derniers jours",
|
||||||
|
"settings.calendar.range_90": "90 derniers jours",
|
||||||
|
"settings.calendar.range_180": "6 derniers mois",
|
||||||
|
"settings.calendar.range_365": "Dernière année",
|
||||||
|
"settings.calendar.range_all": "Toute la période",
|
||||||
|
"settings.calendar.range_hint": "Une boîte aux lettres ancienne peut contenir des années d'entrées récurrentes/de vacances — réduire la plage limite le calendrier importé à ce qui est réellement pertinent. S'applique à ce bouton d'importation ainsi qu'à la réimportation automatique au lancement.",
|
||||||
|
"settings.calendar.importing": "Importation…",
|
||||||
|
"settings.calendar.import": "Importer",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} événements",
|
||||||
|
"settings.calendar.auto_reimport": "Réimporter ce fichier automatiquement au lancement",
|
||||||
|
"settings.calendar.auto_reimport_hint": "S'exécute une fois au démarrage, pas sur une minuterie — la réimportation peut être répétée sans risque (les événements existants sont mis en correspondance et mis à jour, pas dupliqués).",
|
||||||
|
"settings.calendar.import_failed": "Échec de l'importation : {error} — le fichier lui-même n'est pas modifié ; vérifiez le chemin et réessayez.",
|
||||||
|
"settings.calendar.cleanup_title": "Nettoyer",
|
||||||
|
"settings.calendar.cleanup_hint": "Les événements déjà rattachés à une réunion enregistrée sont toujours conservés, quelle que soit l'option choisie ci-dessous.",
|
||||||
|
"settings.calendar.cleanup_30": "Plus anciens que 30 jours",
|
||||||
|
"settings.calendar.cleanup_90": "Plus anciens que 90 jours",
|
||||||
|
"settings.calendar.cleanup_180": "Plus anciens que 6 mois",
|
||||||
|
"settings.calendar.cleanup_365": "Plus anciens que 1 an",
|
||||||
|
"settings.calendar.cleanup_all": "Tout supprimer",
|
||||||
|
"settings.calendar.cleaning": "Nettoyage…",
|
||||||
|
"settings.calendar.cleanup_btn": "Nettoyer le calendrier",
|
||||||
|
"settings.calendar.cleanup_result": "{deleted}{extra} supprimés.",
|
||||||
|
"settings.calendar.cleanup_kept": ", {n} conservés (liés à une réunion)",
|
||||||
|
"settings.calendar.cleanup_failed": "Échec du nettoyage : {error}",
|
||||||
|
"settings.calendar.imported_events": "Événements importés",
|
||||||
|
"settings.calendar.no_events": "Aucun événement importé pour l'instant.",
|
||||||
|
"settings.calendar.search_title": "Rechercher un titre",
|
||||||
|
"settings.calendar.search_placeholder": "Nom de la réunion…",
|
||||||
|
"settings.calendar.date_label": "Date",
|
||||||
|
"settings.calendar.events_count": "{shown} sur {total} événements",
|
||||||
|
"settings.calendar.untitled": "(sans titre)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "Paramètres",
|
||||||
|
"settings.close": "Fermer",
|
||||||
|
"settings.close_title": "Fermer (Échap)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "Mode aperçu — le backend de synchronisation n'est pas encore implémenté (Phase 9). Les modifications ne sont conservées que dans l'interface.",
|
||||||
|
"settings.sync.title": "Synchronisation et téléversement",
|
||||||
|
"settings.sync.enable_label": "Activer le téléversement des artefacts de réunion vers les cibles configurées",
|
||||||
|
"settings.sync.enable_hint": "Désactivé par défaut. Rien n'est téléversé sauf si ceci est activé et qu'une cible est activée. Les cibles auto-hébergées conservent les données sur votre propre serveur ; les clouds tiers sont clairement étiquetés.",
|
||||||
|
"settings.sync.targets_title": "Cibles",
|
||||||
|
"settings.sync.no_targets": "Aucune cible pour l'instant. Ajoutez-en une ci-dessous.",
|
||||||
|
"settings.sync.third_party": "tiers",
|
||||||
|
"settings.sync.your_server": "votre serveur",
|
||||||
|
"settings.sync.edit": "Modifier",
|
||||||
|
"settings.sync.remove": "Supprimer",
|
||||||
|
"settings.sync.edit_target": "Modifier la cible",
|
||||||
|
"settings.sync.add_target": "Ajouter une cible",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV couvre Nextcloud, ownCloud, Cloudreve, Seafile et Synology. (Seafile : activez SeafDAV côté serveur.)",
|
||||||
|
"settings.sync.name": "Nom",
|
||||||
|
"settings.sync.name_placeholder": "Nextcloud domestique",
|
||||||
|
"settings.sync.provider": "Fournisseur",
|
||||||
|
"settings.sync.server_url": "URL du serveur",
|
||||||
|
"settings.sync.server_url_note_1": "Juste l'URL de votre serveur — WhispAssist ajoute",
|
||||||
|
"settings.sync.server_url_note_2": "automatiquement.",
|
||||||
|
"settings.sync.remote_folder": "Dossier distant",
|
||||||
|
"settings.sync.username": "Nom d'utilisateur",
|
||||||
|
"settings.sync.app_password": "Mot de passe d'application",
|
||||||
|
"settings.sync.pw_keep": "laissez vide pour conserver le mot de passe actuel",
|
||||||
|
"settings.sync.pw_store": "stocké dans le gestionnaire d'identifiants du système d'exploitation",
|
||||||
|
"settings.sync.upload_legend": "Téléverser",
|
||||||
|
"settings.sync.artifact_transcript": "transcription",
|
||||||
|
"settings.sync.artifact_notes": "notes",
|
||||||
|
"settings.sync.artifact_summary": "résumé",
|
||||||
|
"settings.sync.artifact_recording": "enregistrement (.wav, si conservé)",
|
||||||
|
"settings.sync.allow_plaintext": "Autoriser le http en clair pour une adresse LAN (non recommandé)",
|
||||||
|
"settings.sync.encrypt_before": "Chiffrer avant le téléversement (la destination ne stocke que le texte chiffré)",
|
||||||
|
"settings.sync.requires_vault": "— nécessite un coffre déverrouillé",
|
||||||
|
"settings.sync.test_connection": "Tester la connexion",
|
||||||
|
"settings.sync.save_changes": "Enregistrer les modifications",
|
||||||
|
"settings.sync.add_target_btn": "Ajouter la cible",
|
||||||
|
"settings.sync.cancel": "Annuler",
|
||||||
|
"settings.sync.third_party_banner": "{kind} est un cloud tiers — le téléversement envoie vos données hors de votre appareil vers {kind}.",
|
||||||
|
"settings.sync.link_account": "Lier le compte {kind}…",
|
||||||
|
"settings.sync.oauth_hint": "Ouvre une connexion OAuth (redirection loopback) ; le jeton est stocké dans le gestionnaire d'identifiants du système d'exploitation.",
|
||||||
|
"settings.sync.footnote": "Les identifiants ne sont jamais écrits dans les paramètres ni dans la base de données — uniquement dans le gestionnaire d'identifiants du système d'exploitation. TLS est requis pour les cibles hors LAN.",
|
||||||
|
|
||||||
|
"settings.ai.title": "Fournisseur de résumés IA",
|
||||||
|
"settings.ai.intro_1": "Les résumés s'exécutent par défaut sur un LLM local. Pointez ceci vers Ollama sur ce PC ou une autre machine de votre LAN (par ex.",
|
||||||
|
"settings.ai.intro_2": "— les deux comptent comme local, donc rien ne quitte votre réseau. Les fournisseurs hébergés (Anthropic) sont facultatifs, désactivés par défaut, et envoient la transcription à un tiers dès que vous en activez un.",
|
||||||
|
"settings.ai.provider": "Fournisseur",
|
||||||
|
"settings.ai.off": "Désactivé",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (local / LAN)",
|
||||||
|
"settings.ai.provider_custom": "Personnalisé (compatible OpenAI)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — hébergé, quitte cet appareil",
|
||||||
|
"settings.ai.endpoint": "Point de terminaison",
|
||||||
|
"settings.ai.model": "Modèle",
|
||||||
|
"settings.ai.api_key": "Clé API",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (déjà définie — laissez vide pour la conserver)",
|
||||||
|
"settings.ai.show_api_key": "Afficher la clé API",
|
||||||
|
"settings.ai.hide_api_key": "Masquer la clé API",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic est un service hébergé tiers — la transcription de cette réunion quitte votre appareil lorsque vous générez un résumé.",
|
||||||
|
"settings.ai.endpoint_banner": "Ce point de terminaison n'est pas sur votre machine ni sur votre LAN — votre transcription quitterait votre réseau.",
|
||||||
|
"settings.ai.saving": "Enregistrement…",
|
||||||
|
"settings.ai.save": "Enregistrer",
|
||||||
|
"settings.ai.test_connection": "Tester la connexion",
|
||||||
|
"settings.ai.this_hosted_provider": "ce fournisseur hébergé",
|
||||||
|
"settings.ai.reachable": "Joignable",
|
||||||
|
"settings.ai.unreachable": "Injoignable",
|
||||||
|
"settings.ai.model_count_one": "· {n} modèle",
|
||||||
|
"settings.ai.model_count_many": "· {n} modèles",
|
||||||
|
"settings.ai.on_lan": "· sur votre machine / LAN",
|
||||||
|
"settings.ai.leaves_network": "· quitte votre réseau",
|
||||||
|
"settings.ai.advanced_title": "Configuration avancée d'Ollama",
|
||||||
|
"settings.ai.changed": "{n} modifiés",
|
||||||
|
"settings.ai.reset_all": "Tout réinitialiser aux valeurs par défaut",
|
||||||
|
"settings.ai.system_prompt": "Invite système",
|
||||||
|
"settings.ai.system_prompt_placeholder": "par ex. Vous êtes un rédacteur de résumés de réunion concis.",
|
||||||
|
"settings.ai.system_prompt_hint": "Ajoutée avant le format de sortie requis par WhispAssist, afin que vos instructions ne puissent pas casser l'analyse du résumé/des actions à mener.",
|
||||||
|
"settings.ai.think": "Réfléchir",
|
||||||
|
"settings.ai.think_help": "Effort de raisonnement (modèles de raisonnement uniquement).",
|
||||||
|
"settings.ai.keep_alive": "Maintenir en vie",
|
||||||
|
"settings.ai.keep_alive_help": "Durée pendant laquelle le modèle reste en RAM. par ex. 5m, 1h, 0 (décharger), -1 (indéfiniment).",
|
||||||
|
"settings.ai.runtime_hardware": "Exécution et matériel",
|
||||||
|
"settings.ai.rarely_needed": "— rarement nécessaire",
|
||||||
|
"settings.ai.save_advanced": "Enregistrer les options avancées",
|
||||||
|
"settings.ai.saved": "Enregistré",
|
||||||
|
"settings.ai.turn_off": "Désactiver",
|
||||||
|
"settings.ai.default_value": "Par défaut {value}.",
|
||||||
|
"settings.ai.reset_default": "Réinitialiser à la valeur par défaut",
|
||||||
|
"settings.ai.reset_field": "Réinitialiser {name}",
|
||||||
|
|
||||||
|
"settings.mcp.title": "Serveur MCP",
|
||||||
|
"settings.mcp.banner_1": "Cela permet à votre propre agent de codage (Claude Code, Codex, Copilot, OpenCode, …) de récupérer le contexte de la réunion sur votre machine locale. Une fois connecté,",
|
||||||
|
"settings.mcp.banner_agent": "cet agent",
|
||||||
|
"settings.mcp.banner_2": "peut transmettre ce qu'il lit au cloud de son propre fournisseur de modèle — hors du contrôle de WhispAssist. WhispAssist lui-même n'envoie jamais ces données où que ce soit ; le serveur n'écoute que sur cet appareil (",
|
||||||
|
"settings.mcp.banner_3": ") et chaque lecture est journalisée ci-dessous.",
|
||||||
|
"settings.mcp.enable": "Activer le serveur MCP",
|
||||||
|
"settings.mcp.enable_hint": "Désactivé par défaut. Loopback uniquement, protégé par jeton — rien n'est accessible depuis le réseau.",
|
||||||
|
"settings.mcp.transport": "Transport",
|
||||||
|
"settings.mcp.transport_http": "HTTP en flux continu",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (l'agent lance un processus)",
|
||||||
|
"settings.mcp.port": "Port",
|
||||||
|
"settings.mcp.transport_change_hint": "Pour changer de transport/port, désactivez d'abord le serveur, puis réactivez-le.",
|
||||||
|
"settings.mcp.scope_title": "Portée",
|
||||||
|
"settings.mcp.expose": "Exposer",
|
||||||
|
"settings.mcp.scope_none": "Aucune — rien n'est partagé",
|
||||||
|
"settings.mcp.scope_selected": "Sélectionnés — uniquement les briefs de fonctionnalité que vous avez marqués comme partagés",
|
||||||
|
"settings.mcp.scope_all": "Tout — réunions, transcriptions, actions à mener et briefs partagés",
|
||||||
|
"settings.mcp.expose_recordings": "Autoriser aussi les réunions avec un enregistrement sauvegardé (désactivé par défaut — la transcription d'une réunion enregistrée est retenue même en portée « Tout » jusqu'à ce que ceci soit activé)",
|
||||||
|
"settings.mcp.token_title": "Nouveau jeton d'authentification — affiché une seule fois, copiez-le maintenant",
|
||||||
|
"settings.mcp.token_hint": "Ceci ne sera plus affiché. Il est stocké dans le gestionnaire d'identifiants du système d'exploitation ; si vous le perdez, désactivez puis réactivez le serveur pour en générer un nouveau.",
|
||||||
|
"settings.mcp.copied": "Copié",
|
||||||
|
"settings.mcp.copy": "Copier",
|
||||||
|
"settings.mcp.endpoint": "Point de terminaison",
|
||||||
|
"settings.mcp.endpoint_hint": "Pointez la configuration du client MCP de votre agent vers {kind}, avec le jeton ci-dessus comme identifiant bearer.",
|
||||||
|
"settings.mcp.kind_command": "commande",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "Journal d'accès",
|
||||||
|
"settings.mcp.access_log_hint": "Chaque lecture d'outil effectuée par un agent, autorisée ou refusée (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "Aucun agent n'a encore rien lu.",
|
||||||
|
"settings.mcp.log_meeting": "réunion {id}",
|
||||||
|
"settings.mcp.refresh": "Actualiser",
|
||||||
|
|
||||||
|
"settings.privacy.title": "Confidentialité",
|
||||||
|
"settings.privacy.intro": "Ce que WhispAssist est réellement autorisé à envoyer hors de cet appareil actuellement. Avec tout désactivé (le réglage par défaut), rien ne quitte l'appareil.",
|
||||||
|
"settings.privacy.llm_endpoint": "Point de terminaison LLM",
|
||||||
|
"settings.privacy.off": "désactivé",
|
||||||
|
"settings.privacy.local_only": "local uniquement",
|
||||||
|
"settings.privacy.leaves_device": "quitte cet appareil",
|
||||||
|
"settings.privacy.sync_label": "Synchronisation",
|
||||||
|
"settings.privacy.enabled": "activée",
|
||||||
|
"settings.privacy.mcp_label": "Serveur MCP",
|
||||||
|
"settings.privacy.mcp_on": "activé · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "Entrant sur loopback uniquement — n'ajoute rien à la liste de sortie ci-dessus. Un agent connecté peut néanmoins transmettre ce qu'il lit à son propre fournisseur de modèle ; voir l'onglet du serveur MCP.",
|
||||||
|
"settings.privacy.egress_title": "Liste d'autorisation de sortie",
|
||||||
|
"settings.privacy.no_egress": "Aucun hôte n'est autorisé — WA n'effectue aucune sortie de contenu.",
|
||||||
|
"settings.privacy.sync_targets_title": "Cibles de synchronisation",
|
||||||
|
"settings.privacy.third_party": "tiers",
|
||||||
|
"settings.privacy.your_server": "votre serveur",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "sans TLS",
|
||||||
|
"settings.privacy.refresh": "Actualiser",
|
||||||
|
"settings.privacy.unavailable": "Autovérification de confidentialité indisponible.",
|
||||||
|
"settings.privacy.vault_title": "Coffre de chiffrement",
|
||||||
|
"settings.privacy.vault_intro": "Chiffrez les notes, transcriptions et résumés au repos avec un mot de passe. (Les fichiers audio ne sont pas encore chiffrés.)",
|
||||||
|
"settings.privacy.vault_password": "Mot de passe du coffre",
|
||||||
|
"settings.privacy.enable_vault": "Activer le coffre",
|
||||||
|
"settings.privacy.vault_pw_hint": "Utilisez au moins 8 caractères. Si vous l'oubliez, le contenu chiffré ne pourra pas être récupéré.",
|
||||||
|
"settings.privacy.vault_locked_1": "Le coffre est ",
|
||||||
|
"settings.privacy.locked_word": "verrouillé",
|
||||||
|
"settings.privacy.vault_locked_2": ". Déverrouillez pour lire les réunions chiffrées.",
|
||||||
|
"settings.privacy.password": "Mot de passe",
|
||||||
|
"settings.privacy.unlock": "Déverrouiller",
|
||||||
|
"settings.privacy.vault_unlocked_1": "Le coffre est ",
|
||||||
|
"settings.privacy.unlocked_word": "déverrouillé",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". Les nouvelles notes, transcriptions et résumés sont chiffrés au repos.",
|
||||||
|
"settings.privacy.lock_now": "Verrouiller maintenant",
|
||||||
|
"settings.privacy.change_password": "Changer le mot de passe",
|
||||||
|
"settings.privacy.current_password": "Mot de passe actuel",
|
||||||
|
"settings.privacy.new_password": "Nouveau mot de passe",
|
||||||
|
|
||||||
|
"settings.about.title": "À propos",
|
||||||
|
"settings.about.tagline": "Un assistant de réunion entièrement local, open source et natif Windows.",
|
||||||
|
"settings.about.build_commit": "Commit de compilation",
|
||||||
|
|
||||||
|
"consent.heading": "Avant d'enregistrer",
|
||||||
|
"consent.body": "Enregistrer des conversations sans le consentement des participants peut être illégal dans votre région. Vérifiez les lois locales sur l'enregistrement. Ceci est une mise en garde, pas un conseil juridique.",
|
||||||
|
"consent.accept": "Je comprends — activer l'enregistrement",
|
||||||
|
"consent.cancel": "Annuler",
|
||||||
|
|
||||||
|
"hosted.heading": "Avant d'utiliser {provider}",
|
||||||
|
"hosted.body_1": "Générer avec",
|
||||||
|
"hosted.body_2": "envoie la transcription de cette réunion sur leurs serveurs — elle quitte cet appareil et est soumise à leur politique de confidentialité. WhispAssist n'a aucun contrôle sur le traitement des données une fois qu'elles quittent votre appareil. Ceci est désactivé par défaut ; vous le choisissez maintenant.",
|
||||||
|
"hosted.accept": "Je comprends — continuer",
|
||||||
|
"hosted.cancel": "Annuler",
|
||||||
|
|
||||||
|
"theme.group": "Thème",
|
||||||
|
"theme.system": "Suivre le système",
|
||||||
|
"theme.light": "Clair",
|
||||||
|
"theme.dark": "Sombre",
|
||||||
|
|
||||||
|
"import.dialog_label": "Ajouter une réunion depuis un fichier ou une URL",
|
||||||
|
"import.title": "Ajouter une réunion",
|
||||||
|
"import.close": "Fermer",
|
||||||
|
"import.close_title": "Fermer (Échap)",
|
||||||
|
"import.body": "Importez un enregistrement existant — un fichier audio/vidéo local ou un lien (YouTube, une page de streaming ou une URL média directe). Il est transcrit et diarisé comme un enregistrement en direct.",
|
||||||
|
"import.file_or_url": "Fichier ou URL",
|
||||||
|
"import.source_placeholder": "Collez une URL ou parcourez pour trouver un fichier…",
|
||||||
|
"import.choose_file": "Choisir un fichier local",
|
||||||
|
"import.browse": "Parcourir…",
|
||||||
|
"import.title_label": "Titre",
|
||||||
|
"import.optional": "facultatif",
|
||||||
|
"import.title_placeholder": "Par défaut, le nom du fichier",
|
||||||
|
"import.filter_av": "Audio / vidéo",
|
||||||
|
"import.requires_1": "Nécessite",
|
||||||
|
"import.requires_2": "installé et dans votre PATH (plus",
|
||||||
|
"import.requires_3": "pour les URL). WhispAssist ne les inclut pas.",
|
||||||
|
"import.importing": "Importation… cela peut prendre un moment",
|
||||||
|
"import.import": "Importer",
|
||||||
|
"import.cancel": "Annuler",
|
||||||
|
|
||||||
|
"tagchip.filter": "Filtrer les réunions marquées « {tag} »",
|
||||||
|
"tagchip.remove": "Supprimer l'étiquette {tag}",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "Niveau d'entrée du système et du microphone",
|
||||||
|
"levelmeter.system": "Niveau d'entrée du système",
|
||||||
|
|
||||||
|
"app.tagline": "local · privé",
|
||||||
|
"app.note_template": "Modèle de notes",
|
||||||
|
"app.no_template": "Aucun modèle",
|
||||||
|
"app.add_meeting": "Ajouter une réunion",
|
||||||
|
"app.add_meeting_title": "Ajouter une réunion depuis un fichier ou une URL",
|
||||||
|
"app.record": "Enregistrer",
|
||||||
|
"app.record_title": "Démarrer l'enregistrement (Ctrl+Maj+R)",
|
||||||
|
"app.stop": "Arrêter",
|
||||||
|
"app.stop_title": "Arrêter l'enregistrement (Ctrl+Maj+R)",
|
||||||
|
"app.cancel": "Annuler",
|
||||||
|
"app.cancel_title": "Abandonner cet enregistrement et le supprimer",
|
||||||
|
"app.recording": "Enregistrement…",
|
||||||
|
"app.backend_title": "Backend de transcription actif",
|
||||||
|
"app.retention_title": "Enregistrer l'audio en .wav pour cette réunion",
|
||||||
|
"app.saving": "enregistrement",
|
||||||
|
"app.not_saved": "non enregistré",
|
||||||
|
"app.reconnecting": "reconnexion du périphérique audio…",
|
||||||
|
"app.settings": "Paramètres",
|
||||||
|
"app.settings_title": "Paramètres (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "Abandonner cet enregistrement ? Son audio et sa transcription seront supprimés.",
|
||||||
|
"app.announce_started": "Enregistrement démarré",
|
||||||
|
"app.announce_paused": "Enregistrement en pause",
|
||||||
|
"app.announce_stopped": "Enregistrement arrêté",
|
||||||
|
"app.consent_dialog": "Consentement à l'enregistrement",
|
||||||
|
"app.vault_dialog": "Déverrouiller le coffre de chiffrement",
|
||||||
|
"app.vault_title": "Déverrouiller le coffre de chiffrement",
|
||||||
|
"app.vault_desc": "Vos réunions sont chiffrées au repos. Saisissez votre mot de passe pour les lire.",
|
||||||
|
"app.vault_password": "Mot de passe du coffre",
|
||||||
|
"app.vault_incorrect": "Mot de passe incorrect",
|
||||||
|
"app.unlock": "Déverrouiller",
|
||||||
|
"app.continue_locked": "Continuer verrouillé",
|
||||||
|
"app.show_meetings": "Afficher la liste des réunions",
|
||||||
|
"app.hide_meetings": "Masquer la liste des réunions",
|
||||||
|
"app.show_summary": "Afficher le panneau de résumé",
|
||||||
|
"app.hide_summary": "Masquer le panneau de résumé",
|
||||||
|
"app.resize_meetings": "Redimensionner la liste des réunions",
|
||||||
|
"app.resize_summary": "Redimensionner le panneau de résumé",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "Rechercher des réunions…",
|
||||||
|
"meetings.search_aria": "Rechercher des réunions",
|
||||||
|
"meetings.filter_tag_aria": "Filtrer par étiquette",
|
||||||
|
"meetings.all_tags": "Toutes les étiquettes",
|
||||||
|
"meetings.from_date_aria": "À partir de la date",
|
||||||
|
"meetings.to_date_aria": "Jusqu'à la date",
|
||||||
|
"meetings.bulk_format_aria": "Format d'exportation groupée",
|
||||||
|
"meetings.bulk_export": "Exportation groupée",
|
||||||
|
"meetings.bulk_export_title": "Exporter toutes les réunions correspondant aux filtres d'étiquette/date ci-dessus",
|
||||||
|
"meetings.exporting": "Exportation…",
|
||||||
|
"meetings.exported_one": "{n} réunion exportée.",
|
||||||
|
"meetings.exported_many": "{n} réunions exportées.",
|
||||||
|
"meetings.searching": "Recherche…",
|
||||||
|
"meetings.loading": "Chargement des réunions…",
|
||||||
|
"meetings.no_matches": "Aucun résultat.",
|
||||||
|
"meetings.empty": "Aucune réunion pour l'instant — cliquez sur Enregistrer ci-dessus pour commencer.",
|
||||||
|
"meetings.status.recording": "enregistrement",
|
||||||
|
"meetings.status.transcribing": "transcription",
|
||||||
|
"meetings.status.recovering": "récupération",
|
||||||
|
"meetings.status.error": "erreur",
|
||||||
|
"meetings.resume": "Reprendre la transcription",
|
||||||
|
"meetings.delete_aria": "Supprimer la réunion",
|
||||||
|
"meetings.delete_confirm": "Supprimer cette réunion ? Cela supprime son enregistrement, sa transcription et ses notes.",
|
||||||
|
|
||||||
|
"transcript.heading": "Transcription",
|
||||||
|
"transcript.title_aria": "Titre de la réunion",
|
||||||
|
"transcript.lang_title": "Langue de transcription",
|
||||||
|
"transcript.lang_auto": "détection en cours…",
|
||||||
|
"transcript.show": "Afficher la transcription",
|
||||||
|
"transcript.hide": "Masquer la transcription",
|
||||||
|
"transcript.empty": "Aucune transcription pour cette réunion.",
|
||||||
|
"transcript.will_appear": "La transcription apparaîtra ici au fur et à mesure de l'enregistrement.",
|
||||||
|
"transcript.play_from_here": "Lire à partir d'ici",
|
||||||
|
"transcript.resize": "Redimensionner la transcription et les notes",
|
||||||
|
"transcript.reprocess_placeholder": "Re-transcrire avec…",
|
||||||
|
"transcript.reprocess_lang_aria": "Langue de retraitement",
|
||||||
|
"transcript.reprocess_keep_lang": "Conserver la langue actuelle",
|
||||||
|
"transcript.reprocess_go": "Lancer",
|
||||||
|
"transcript.retranscribing": "Re-transcription…",
|
||||||
|
"transcript.has_note": "Comporte une note",
|
||||||
|
"transcript.note_placeholder": "Ajouter une note pour ce moment…",
|
||||||
|
"transcript.note_aria": "Note pour cette ligne de transcription",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Document Word",
|
||||||
|
"transcript.filter_obsidian": "Note Obsidian",
|
||||||
|
|
||||||
|
"notes.heading": "Notes",
|
||||||
|
"notes.show": "Afficher les notes",
|
||||||
|
"notes.hide": "Masquer les notes",
|
||||||
|
"notes.placeholder": "Notes…",
|
||||||
|
"notes.live_placeholder": "Tapez des notes pendant que vous parlez…",
|
||||||
|
"notes.toolbar_aria": "Mise en forme des notes",
|
||||||
|
"notes.bold": "Gras",
|
||||||
|
"notes.italic": "Italique",
|
||||||
|
"notes.h1": "Titre 1",
|
||||||
|
"notes.h2": "Titre 2",
|
||||||
|
"notes.bullet": "Liste à puces",
|
||||||
|
"notes.checkbox_title": "Case à cocher",
|
||||||
|
"notes.checkbox_aria": "Élément de liste à cocher",
|
||||||
|
"notes.edit_raw": "Modifier le markdown brut",
|
||||||
|
"notes.render": "Afficher le markdown",
|
||||||
|
"notes.editor": "Éditeur",
|
||||||
|
"notes.preview": "Aperçu",
|
||||||
|
"notes.export_md_title": "Exporter les notes en .md",
|
||||||
|
"notes.export_pdf_title": "Exporter les notes en .pdf",
|
||||||
|
"notes.export_docx_title": "Exporter les notes en .docx",
|
||||||
|
"notes.export_bundle_title": "Exporter audio + transcription + notes vers un dossier",
|
||||||
|
"notes.export_obsidian_title": "Exporter une note Obsidian (notes + résumé + transcription, sans audio)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "Enregistrement",
|
||||||
|
"summary.loading_recording": "Chargement de l'enregistrement…",
|
||||||
|
"summary.sync_heading": "Synchronisation",
|
||||||
|
"summary.uploading": "Téléversement…",
|
||||||
|
"summary.upload_now": "Téléverser maintenant",
|
||||||
|
"summary.no_uploads": "Aucun téléversement pour cette réunion pour l'instant.",
|
||||||
|
"summary.retry": "réessayer",
|
||||||
|
"summary.tags_heading": "Étiquettes",
|
||||||
|
"summary.tags_select": "Sélectionnez une réunion pour l'étiqueter.",
|
||||||
|
"summary.tag_add_placeholder": "Ajouter une étiquette…",
|
||||||
|
"summary.tag_first_placeholder": "projet, client, sujet…",
|
||||||
|
"summary.generating": "Génération…",
|
||||||
|
"summary.generate_tags": "Générer des étiquettes",
|
||||||
|
"summary.saving": "Enregistrement…",
|
||||||
|
"summary.save_tags": "Enregistrer les étiquettes",
|
||||||
|
"summary.summary_heading": "Résumé",
|
||||||
|
"summary.ai_provider": "Fournisseur d'IA",
|
||||||
|
"summary.provider_off": "Désactivé",
|
||||||
|
"summary.provider_custom": "Personnalisé",
|
||||||
|
"summary.local": "local",
|
||||||
|
"summary.leaves_device": "quitte cet appareil",
|
||||||
|
"summary.this_hosted_provider": "ce fournisseur hébergé",
|
||||||
|
"summary.select_generate": "Sélectionnez une réunion pour générer un résumé.",
|
||||||
|
"summary.decisions_heading": "Décisions",
|
||||||
|
"summary.regenerate": "Régénérer",
|
||||||
|
"summary.generated_hint": "Généré localement après la réunion (nécessite un fournisseur LLM local).",
|
||||||
|
"summary.no_provider": "Aucun fournisseur d'IA configuré — activez-en un dans les Paramètres.",
|
||||||
|
"summary.generate_summary": "Générer un résumé",
|
||||||
|
"summary.briefs_heading": "Briefs de fonctionnalité",
|
||||||
|
"summary.briefs_select": "Sélectionnez une réunion pour créer ou consulter des briefs de fonctionnalité.",
|
||||||
|
"summary.briefs_desc": "Distille cette réunion en une spécification prête pour un agent (nécessite un fournisseur LLM local) — remettez-la à un agent de codage, ou servez-la via le serveur MCP une fois celui-ci activé.",
|
||||||
|
"summary.brief_repo_placeholder": "Dépôt cible (facultatif), par ex. acme/reporting-web",
|
||||||
|
"summary.distilling": "Distillation…",
|
||||||
|
"summary.create_brief": "Créer un brief de fonctionnalité",
|
||||||
|
"summary.brief_mcp_title": "Disponible lorsque le serveur MCP est activé",
|
||||||
|
"summary.copied": "Copié",
|
||||||
|
"summary.copy_md": "Copier en Markdown",
|
||||||
|
"summary.copy_json": "Copier en JSON",
|
||||||
|
"summary.brief_problem": "Problème",
|
||||||
|
"summary.brief_outcome": "Résultat souhaité",
|
||||||
|
"summary.brief_criteria": "Critères d'acceptation",
|
||||||
|
"summary.brief_none": "Rien de capturé.",
|
||||||
|
"summary.brief_context": "Contexte",
|
||||||
|
"summary.action_items_heading": "Actions à mener",
|
||||||
|
"summary.ai_select": "Sélectionnez une réunion pour voir ses actions à mener.",
|
||||||
|
"summary.ai_empty": "Aucune pour l'instant — ajoutez-en une ci-dessous, ou générez un résumé pour les extraire automatiquement.",
|
||||||
|
"summary.confirmed": "Confirmée",
|
||||||
|
"summary.ai_text_placeholder": "Action à mener…",
|
||||||
|
"summary.ai_text_aria": "Texte de l'action à mener",
|
||||||
|
"summary.owner": "Responsable",
|
||||||
|
"summary.due_date": "Date d'échéance",
|
||||||
|
"summary.reminder_title": "Planifier un rappel local pour cette date d'échéance",
|
||||||
|
"summary.ai_delete_title": "Supprimer cette action à mener",
|
||||||
|
"summary.ai_delete_aria": "Supprimer l'action à mener",
|
||||||
|
"summary.add_action_item": "Ajouter une action à mener",
|
||||||
|
"summary.save_action_items": "Enregistrer les actions à mener",
|
||||||
|
"summary.calendar_heading": "Événement du calendrier",
|
||||||
|
"summary.cal_select": "Sélectionnez une réunion pour la lier à un événement du calendrier.",
|
||||||
|
"summary.event_search_placeholder": "Rechercher un titre d'événement…",
|
||||||
|
"summary.linked_event": "Événement lié",
|
||||||
|
"summary.change_event": "Changer d'événement…",
|
||||||
|
"summary.link_event": "Lier un événement…",
|
||||||
|
"summary.untitled_event": "(sans titre)",
|
||||||
|
"summary.no_events_1": "Aucun événement importé pour l'instant — importez-en un",
|
||||||
|
"summary.no_events_2": "depuis Paramètres → Calendrier.",
|
||||||
|
"summary.no_events_match": "Aucun événement ne correspond à cette recherche/date — essayez d'en effacer une.",
|
||||||
|
"summary.organizer_label": "Organisateur : {name}",
|
||||||
|
"summary.participants_heading": "Participants",
|
||||||
|
"summary.participants_hint": "Renseigné à partir de l'événement du calendrier lié.",
|
||||||
|
"summary.speakers_heading": "Locuteurs",
|
||||||
|
"summary.speakers_select": "Sélectionnez une réunion pour nommer ses locuteurs.",
|
||||||
|
"summary.no_speakers": "Aucun locuteur détecté pour l'instant.",
|
||||||
|
"summary.speaker_name_placeholder": "Nom du locuteur",
|
||||||
|
"summary.save": "Enregistrer",
|
||||||
|
"summary.cancel": "Annuler",
|
||||||
|
"summary.name_speaker": "Nommer ce locuteur…",
|
||||||
|
"summary.add_new_name": "+ Ajouter un nouveau nom…",
|
||||||
|
|
||||||
|
"settings.recording.title": "Enregistrement",
|
||||||
|
"settings.recording.record_default": "Enregistrer les réunions par défaut",
|
||||||
|
"settings.recording.save_audio_as": "enregistrer l'audio en",
|
||||||
|
"settings.recording.default_hint": "Désactivé par défaut. Lorsqu'il est désactivé, l'audio n'est utilisé que pour produire la transcription et est supprimé lorsque la réunion est finalisée. Vous pouvez aussi activer l'enregistrement par réunion.",
|
||||||
|
"settings.recording.consent_label": "Consentement : {status}.",
|
||||||
|
"settings.recording.consent_ack": "accepté",
|
||||||
|
"settings.recording.consent_not": "pas encore accepté",
|
||||||
|
"settings.recording.auto_label": "Démarrer automatiquement l'enregistrement au début d'un événement du calendrier",
|
||||||
|
"settings.recording.auto_hint": "Uniquement lorsque WhispAssist est ouvert. Lorsque l'heure de début d'un événement de calendrier importé arrive, un enregistrement commence automatiquement (en utilisant votre réglage de conservation par défaut ci-dessus). Rien ne s'exécute en arrière-plan — la minuterie n'est armée que pendant que l'application est en cours d'exécution. Importez des événements sous Paramètres → Calendrier."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "रिकॉर्डिंग",
|
||||||
|
"nav.hardware": "हार्डवेयर",
|
||||||
|
"nav.storage": "संग्रहण",
|
||||||
|
"nav.calendar": "कैलेंडर",
|
||||||
|
"nav.sync": "सिंक",
|
||||||
|
"nav.ai": "एआई",
|
||||||
|
"nav.mcp": "MCP सर्वर",
|
||||||
|
"nav.privacy": "गोपनीयता",
|
||||||
|
"nav.about": "परिचय",
|
||||||
|
"nav.language": "भाषा",
|
||||||
|
|
||||||
|
"settings.language.title": "भाषा",
|
||||||
|
"settings.language.display": "प्रदर्शन भाषा",
|
||||||
|
"settings.language.display_hint": "ऐप इंटरफ़ेस की भाषा। भाषा जोड़ना उतना ही आसान है जितना एक अनुवाद फ़ाइल डालना।",
|
||||||
|
|
||||||
|
"settings.transcription.title": "ट्रांसक्रिप्शन भाषा",
|
||||||
|
"settings.transcription.label": "भाषा",
|
||||||
|
"settings.transcription.auto": "स्वतः पहचानें",
|
||||||
|
"settings.transcription.applies_hint": "अगली रिकॉर्डिंग पर लागू होता है। वास्तव में उपयोग की गई भाषा बाद में प्रत्येक मीटिंग पर दिखाई जाती है।",
|
||||||
|
"settings.transcription.english_only": "केवल अंग्रेज़ी",
|
||||||
|
"settings.transcription.model_english_only": "“{model}” केवल अंग्रेज़ी में है।",
|
||||||
|
"settings.transcription.no_model": "कोई मॉडल चयनित नहीं।",
|
||||||
|
"settings.transcription.switch_multilingual": "भाषा चुनने के लिए ऊपर किसी बहुभाषी मॉडल पर स्विच करें।",
|
||||||
|
|
||||||
|
"settings.hardware.title": "हार्डवेयर",
|
||||||
|
"settings.hardware.active_backend": "सक्रिय बैकएंड",
|
||||||
|
"settings.hardware.model_meta": "· मॉडल {size}",
|
||||||
|
"settings.hardware.preferred_backend": "पसंदीदा बैकएंड",
|
||||||
|
"settings.hardware.auto_backend": "स्वतः (सर्वोत्तम उपलब्ध)",
|
||||||
|
"settings.hardware.not_available": "उपलब्ध नहीं",
|
||||||
|
"settings.hardware.fallback_hint": "यदि चयनित बैकएंड लोड नहीं होता तो स्वचालित रूप से फ़ॉलबैक करता है (NPU → NVIDIA → AMD → Intel → CPU)।",
|
||||||
|
"settings.hardware.audio_devices": "ऑडियो डिवाइस",
|
||||||
|
"settings.hardware.recording_device": "रिकॉर्डिंग डिवाइस",
|
||||||
|
"settings.hardware.default_system_audio": "डिफ़ॉल्ट सिस्टम ऑडियो",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist वह रिकॉर्ड करता है जो यह डिवाइस बजाता है (लूपबैक) — कॉल का दूसरा पक्ष। यदि आप नहीं चाहते कि यह Windows के सिस्टम डिफ़ॉल्ट का अनुसरण करे तो कोई विशिष्ट आउटपुट चुनें।",
|
||||||
|
"settings.hardware.microphone": "माइक्रोफ़ोन",
|
||||||
|
"settings.hardware.mic_off": "बंद — मेरा माइक्रोफ़ोन कैप्चर न करें",
|
||||||
|
"settings.hardware.default_mic": "डिफ़ॉल्ट माइक्रोफ़ोन",
|
||||||
|
"settings.hardware.mic_hint": "आपकी अपनी आवाज़ को लाइव ट्रांसक्रिप्ट में जोड़ता है ताकि मीटिंग के दोनों पक्ष कैप्चर हों। आपके डिवाइस पर रहता है — कुछ भी अपलोड नहीं होता। केवल ऊपर के सिस्टम ऑडियो को ट्रांसक्राइब करने के लिए “बंद” चुनें।",
|
||||||
|
"settings.hardware.npu_detected": "NPU एक्सेलेरेटर पाया गया",
|
||||||
|
"settings.hardware.ready": "तैयार",
|
||||||
|
"settings.hardware.downloading": "डाउनलोड हो रहा है…",
|
||||||
|
"settings.hardware.package_needed": "पैकेज आवश्यक",
|
||||||
|
"settings.hardware.npu_hint": "आपके NPU को एक बार का एक्सेलेरेशन पैकेज (Whisper ONNX मॉडल{extra}) चाहिए। यह पृष्ठभूमि में डाउनलोड होता है; आप इसे अभी भी शुरू कर सकते हैं।",
|
||||||
|
"settings.hardware.plus_openvino": " + OpenVINO रनटाइम",
|
||||||
|
"settings.hardware.download_npu": "NPU पैकेज डाउनलोड करें",
|
||||||
|
"settings.hardware.dml_title": "GPU एक्सेलेरेशन (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "आपके GPU को Vulkan के बिना DirectML के माध्यम से एक्सेलेरेट किया जा सकता है — एक बार का पैकेज (Whisper ONNX मॉडल{extra})।",
|
||||||
|
"settings.hardware.plus_directml": " + DirectML रनटाइम",
|
||||||
|
"settings.hardware.download_dml": "DirectML पैकेज डाउनलोड करें",
|
||||||
|
"settings.hardware.detection_unavailable": "हार्डवेयर पहचान उपलब्ध नहीं।",
|
||||||
|
"settings.hardware.low_overhead": "कम ओवरहेड प्रीसेट",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + सबसे छोटा मॉडल, बैटरी/पृष्ठभूमि उपयोग के लिए",
|
||||||
|
"settings.hardware.models": "मॉडल",
|
||||||
|
"settings.hardware.downloading_model": "{name} डाउनलोड हो रहा है",
|
||||||
|
"settings.hardware.active_badge": "सक्रिय",
|
||||||
|
"settings.hardware.installed": "इंस्टॉल किया गया",
|
||||||
|
"settings.hardware.use": "उपयोग करें",
|
||||||
|
"settings.hardware.remove": "हटाएँ",
|
||||||
|
"settings.hardware.cant_remove_active": "सक्रिय मॉडल को नहीं हटाया जा सकता",
|
||||||
|
"settings.hardware.download": "डाउनलोड करें",
|
||||||
|
"settings.hardware.diarization": "वक्ता डायराइज़ेशन",
|
||||||
|
"settings.hardware.diarization_hint": "दोनों मॉडल इंस्टॉल करें ताकि पूर्ण रिकॉर्डिंग एक चालू ट्रांसक्रिप्ट के बजाय वक्ता के अनुसार (वक्ता 1, वक्ता 2, …) अलग हों। पूरी तरह ऑफ़लाइन चलता है, रुकने के बाद प्रति मीटिंग एक बार। इनके बिना, प्रत्येक पंक्ति एक ही वक्ता को दी जाती है।",
|
||||||
|
|
||||||
|
"settings.storage.title": "संग्रहण",
|
||||||
|
"settings.storage.location_label": "स्थान",
|
||||||
|
"settings.storage.location_hint": "संग्रहण स्थान बदलना अभी समर्थित नहीं है — यहीं वर्तमान में मीटिंग, मॉडल और डेटाबेस रहते हैं।",
|
||||||
|
"settings.storage.retention_title": "प्रतिधारण",
|
||||||
|
"settings.storage.retention_hint": "सीमा से अधिक सबसे पुरानी मीटिंग को स्वचालित रूप से हटाता है। स्टार्टअप पर एक बार जाँचा जाता है; खाली का अर्थ कोई सीमा नहीं। वर्तमान में रिकॉर्ड हो रही मीटिंग को कभी नहीं छूता।",
|
||||||
|
"settings.storage.max_age": "अधिकतम आयु (दिन)",
|
||||||
|
"settings.storage.max_size": "अधिकतम आकार (GB)",
|
||||||
|
"settings.storage.no_limit": "कोई सीमा नहीं",
|
||||||
|
"settings.storage.export_import_title": "निर्यात और आयात",
|
||||||
|
"settings.storage.export_hint_1": "रिकॉर्डिंग को कंप्यूटरों के बीच पोर्टेबल बंडल फ़ोल्डर के रूप में ले जाएँ (ऑडियो, ट्रांसक्रिप्ट, नोट्स, सारांश और एक",
|
||||||
|
"settings.storage.export_hint_2": "मैनिफ़ेस्ट)। किसी भी फ़ोल्डर में निर्यात करें — एक सिंक की गई ड्राइव, एक USB स्टिक, या किसी सिंक लक्ष्य के स्थानीय माउंट में — फिर दूसरी मशीन पर इसे आयात करें। आयातित मीटिंग को नया id मिलता है, इसलिए फिर से आयात करने से कभी कुछ अधिलेखित नहीं होता।",
|
||||||
|
"settings.storage.export_dialog_title": "सभी मीटिंग को यहाँ निर्यात करें…",
|
||||||
|
"settings.storage.import_dialog_title": "एक बंडल (या बंडलों का फ़ोल्डर) आयात करें…",
|
||||||
|
"settings.storage.exporting": "निर्यात हो रहा है…",
|
||||||
|
"settings.storage.export_all": "सभी मीटिंग निर्यात करें…",
|
||||||
|
"settings.storage.importing": "आयात हो रहा है…",
|
||||||
|
"settings.storage.import_all": "मीटिंग आयात करें…",
|
||||||
|
"settings.storage.exported_one": "{n} मीटिंग निर्यात की गई।",
|
||||||
|
"settings.storage.exported_many": "{n} मीटिंग निर्यात की गईं।",
|
||||||
|
"settings.storage.imported_one": "{n} मीटिंग आयात की गई।",
|
||||||
|
"settings.storage.imported_many": "{n} मीटिंग आयात की गईं।",
|
||||||
|
|
||||||
|
"settings.calendar.title": "कैलेंडर और Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "किसी स्थानीय Outlook बैकअप से ईवेंट और उपस्थित लोगों को आयात करें",
|
||||||
|
"settings.calendar.intro_2": "— केवल पढ़ने योग्य, फ़ाइल में कुछ भी वापस नहीं लिखा जाता। इस डिवाइस से कुछ भी बाहर नहीं जाता।",
|
||||||
|
"settings.calendar.pst_file": ".pst फ़ाइल",
|
||||||
|
"settings.calendar.no_file": "कोई फ़ाइल चयनित नहीं",
|
||||||
|
"settings.calendar.browse": "ब्राउज़ करें…",
|
||||||
|
"settings.calendar.password": "पासवर्ड",
|
||||||
|
"settings.calendar.password_note": "शायद ही कभी आवश्यक",
|
||||||
|
"settings.calendar.optional": "वैकल्पिक",
|
||||||
|
"settings.calendar.import_range": "आयात सीमा",
|
||||||
|
"settings.calendar.range_30": "पिछले 30 दिन",
|
||||||
|
"settings.calendar.range_90": "पिछले 90 दिन",
|
||||||
|
"settings.calendar.range_180": "पिछले 6 महीने",
|
||||||
|
"settings.calendar.range_365": "पिछला 1 वर्ष",
|
||||||
|
"settings.calendar.range_all": "सभी समय",
|
||||||
|
"settings.calendar.range_hint": "एक लंबे समय तक चलने वाले मेलबॉक्स में वर्षों की आवर्ती/अवकाश प्रविष्टियाँ हो सकती हैं — सीमा को संकुचित करने से आयातित कैलेंडर उसी तक सीमित रहता है जो वास्तव में प्रासंगिक है। यह इस आयात बटन और लॉन्च पर स्वचालित पुनः आयात दोनों पर लागू होता है।",
|
||||||
|
"settings.calendar.importing": "आयात हो रहा है…",
|
||||||
|
"settings.calendar.import": "आयात करें",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} ईवेंट",
|
||||||
|
"settings.calendar.auto_reimport": "लॉन्च पर इस फ़ाइल को स्वचालित रूप से पुनः आयात करें",
|
||||||
|
"settings.calendar.auto_reimport_hint": "स्टार्टअप पर एक बार चलता है, टाइमर पर नहीं — पुनः आयात दोहराना सुरक्षित है (मौजूदा ईवेंट मिलान कर अपडेट किए जाते हैं, डुप्लिकेट नहीं)।",
|
||||||
|
"settings.calendar.import_failed": "आयात विफल: {error} — फ़ाइल स्वयं अछूती रहती है; पथ जाँचें और फिर से प्रयास करें।",
|
||||||
|
"settings.calendar.cleanup_title": "साफ़ करें",
|
||||||
|
"settings.calendar.cleanup_hint": "किसी रिकॉर्ड की गई मीटिंग से पहले से जुड़े ईवेंट हमेशा रखे जाते हैं, चाहे नीचे कोई भी विकल्प चुना जाए।",
|
||||||
|
"settings.calendar.cleanup_30": "30 दिन से पुराने",
|
||||||
|
"settings.calendar.cleanup_90": "90 दिन से पुराने",
|
||||||
|
"settings.calendar.cleanup_180": "6 महीने से पुराने",
|
||||||
|
"settings.calendar.cleanup_365": "1 वर्ष से पुराने",
|
||||||
|
"settings.calendar.cleanup_all": "सभी हटाएँ",
|
||||||
|
"settings.calendar.cleaning": "साफ़ किया जा रहा है…",
|
||||||
|
"settings.calendar.cleanup_btn": "कैलेंडर साफ़ करें",
|
||||||
|
"settings.calendar.cleanup_result": "{deleted}{extra} हटाए गए।",
|
||||||
|
"settings.calendar.cleanup_kept": ", {n} रखे गए (किसी मीटिंग से जुड़े)",
|
||||||
|
"settings.calendar.cleanup_failed": "साफ़ करना विफल: {error}",
|
||||||
|
"settings.calendar.imported_events": "आयातित ईवेंट",
|
||||||
|
"settings.calendar.no_events": "अभी तक कोई ईवेंट आयात नहीं किया गया।",
|
||||||
|
"settings.calendar.search_title": "शीर्षक खोजें",
|
||||||
|
"settings.calendar.search_placeholder": "मीटिंग का नाम…",
|
||||||
|
"settings.calendar.date_label": "तारीख़",
|
||||||
|
"settings.calendar.events_count": "{total} में से {shown} ईवेंट",
|
||||||
|
"settings.calendar.untitled": "(अशीर्षक)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "सेटिंग्स",
|
||||||
|
"settings.close": "बंद करें",
|
||||||
|
"settings.close_title": "बंद करें (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "पूर्वावलोकन मोड — सिंक बैकएंड अभी लागू नहीं हुआ है (चरण 9)। परिवर्तन केवल इंटरफ़ेस में रखे जाते हैं।",
|
||||||
|
"settings.sync.title": "सिंक और अपलोड",
|
||||||
|
"settings.sync.enable_label": "कॉन्फ़िगर किए गए लक्ष्यों पर मीटिंग आर्टिफ़ैक्ट अपलोड करना सक्षम करें",
|
||||||
|
"settings.sync.enable_hint": "डिफ़ॉल्ट रूप से बंद। जब तक यह चालू न हो और कोई लक्ष्य सक्षम न हो, कुछ भी अपलोड नहीं होता। स्व-होस्ट किए गए लक्ष्य डेटा को आपके अपने सर्वर पर रखते हैं; तृतीय-पक्ष क्लाउड स्पष्ट रूप से लेबल किए गए हैं।",
|
||||||
|
"settings.sync.targets_title": "लक्ष्य",
|
||||||
|
"settings.sync.no_targets": "अभी तक कोई लक्ष्य नहीं। नीचे एक जोड़ें।",
|
||||||
|
"settings.sync.third_party": "तृतीय-पक्ष",
|
||||||
|
"settings.sync.your_server": "आपका सर्वर",
|
||||||
|
"settings.sync.edit": "संपादित करें",
|
||||||
|
"settings.sync.remove": "हटाएँ",
|
||||||
|
"settings.sync.edit_target": "लक्ष्य संपादित करें",
|
||||||
|
"settings.sync.add_target": "एक लक्ष्य जोड़ें",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV Nextcloud, ownCloud, Cloudreve, Seafile और Synology को कवर करता है। (Seafile: सर्वर-साइड पर SeafDAV सक्षम करें।)",
|
||||||
|
"settings.sync.name": "नाम",
|
||||||
|
"settings.sync.name_placeholder": "होम Nextcloud",
|
||||||
|
"settings.sync.provider": "प्रदाता",
|
||||||
|
"settings.sync.server_url": "सर्वर URL",
|
||||||
|
"settings.sync.server_url_note_1": "बस आपका सर्वर URL — WhispAssist स्वचालित रूप से",
|
||||||
|
"settings.sync.server_url_note_2": "जोड़ता है।",
|
||||||
|
"settings.sync.remote_folder": "रिमोट फ़ोल्डर",
|
||||||
|
"settings.sync.username": "उपयोगकर्ता नाम",
|
||||||
|
"settings.sync.app_password": "ऐप पासवर्ड",
|
||||||
|
"settings.sync.pw_keep": "वर्तमान पासवर्ड रखने के लिए खाली छोड़ें",
|
||||||
|
"settings.sync.pw_store": "OS क्रेडेंशियल स्टोर में संग्रहीत",
|
||||||
|
"settings.sync.upload_legend": "अपलोड",
|
||||||
|
"settings.sync.artifact_transcript": "ट्रांसक्रिप्ट",
|
||||||
|
"settings.sync.artifact_notes": "नोट्स",
|
||||||
|
"settings.sync.artifact_summary": "सारांश",
|
||||||
|
"settings.sync.artifact_recording": "रिकॉर्डिंग (.wav, यदि रखी गई)",
|
||||||
|
"settings.sync.allow_plaintext": "किसी LAN पते के लिए सादा http अनुमति दें (अनुशंसित नहीं)",
|
||||||
|
"settings.sync.encrypt_before": "अपलोड से पहले एन्क्रिप्ट करें (गंतव्य केवल सिफरटेक्स्ट संग्रहीत करता है)",
|
||||||
|
"settings.sync.requires_vault": "— एक अनलॉक किए गए वॉल्ट की आवश्यकता है",
|
||||||
|
"settings.sync.test_connection": "कनेक्शन जाँचें",
|
||||||
|
"settings.sync.save_changes": "परिवर्तन सहेजें",
|
||||||
|
"settings.sync.add_target_btn": "लक्ष्य जोड़ें",
|
||||||
|
"settings.sync.cancel": "रद्द करें",
|
||||||
|
"settings.sync.third_party_banner": "{kind} एक तृतीय-पक्ष क्लाउड है — अपलोड करने से आपका डेटा आपके डिवाइस से बाहर {kind} पर भेजा जाता है।",
|
||||||
|
"settings.sync.link_account": "{kind} खाता लिंक करें…",
|
||||||
|
"settings.sync.oauth_hint": "एक OAuth साइन-इन खोलता है (लूपबैक रीडायरेक्ट); टोकन आपके OS क्रेडेंशियल स्टोर में संग्रहीत होता है।",
|
||||||
|
"settings.sync.footnote": "क्रेडेंशियल कभी भी सेटिंग्स या डेटाबेस में नहीं लिखे जाते — केवल OS क्रेडेंशियल स्टोर में। गैर-LAN लक्ष्यों के लिए TLS आवश्यक है।",
|
||||||
|
|
||||||
|
"settings.ai.title": "एआई सारांश प्रदाता",
|
||||||
|
"settings.ai.intro_1": "सारांश डिफ़ॉल्ट रूप से एक स्थानीय LLM पर चलते हैं। इसे इस PC या आपके LAN की किसी अन्य मशीन पर Ollama की ओर इंगित करें (जैसे",
|
||||||
|
"settings.ai.intro_2": "— दोनों स्थानीय के रूप में गिने जाते हैं, इसलिए आपके नेटवर्क से कुछ भी बाहर नहीं जाता। होस्ट किए गए प्रदाता (Anthropic) वैकल्पिक हैं, डिफ़ॉल्ट रूप से बंद हैं, और जैसे ही आप किसी एक को चालू करते हैं ट्रांसक्रिप्ट को किसी तृतीय पक्ष को भेजते हैं।",
|
||||||
|
"settings.ai.provider": "प्रदाता",
|
||||||
|
"settings.ai.off": "बंद",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (स्थानीय / LAN)",
|
||||||
|
"settings.ai.provider_custom": "कस्टम (OpenAI-संगत)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — होस्ट किया गया, इस डिवाइस से बाहर जाता है",
|
||||||
|
"settings.ai.endpoint": "एंडपॉइंट",
|
||||||
|
"settings.ai.model": "मॉडल",
|
||||||
|
"settings.ai.api_key": "API कुंजी",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (पहले से सेट — रखने के लिए खाली छोड़ें)",
|
||||||
|
"settings.ai.show_api_key": "API कुंजी दिखाएँ",
|
||||||
|
"settings.ai.hide_api_key": "API कुंजी छिपाएँ",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic एक होस्ट की गई तृतीय-पक्ष सेवा है — जब आप सारांश उत्पन्न करते हैं तो इस मीटिंग का ट्रांसक्रिप्ट आपके डिवाइस से बाहर जाता है।",
|
||||||
|
"settings.ai.endpoint_banner": "यह एंडपॉइंट आपकी मशीन या LAN पर नहीं है — आपका ट्रांसक्रिप्ट आपके नेटवर्क से बाहर चला जाएगा।",
|
||||||
|
"settings.ai.saving": "सहेजा जा रहा है…",
|
||||||
|
"settings.ai.save": "सहेजें",
|
||||||
|
"settings.ai.test_connection": "कनेक्शन जाँचें",
|
||||||
|
"settings.ai.this_hosted_provider": "यह होस्ट किया गया प्रदाता",
|
||||||
|
"settings.ai.reachable": "पहुँच योग्य",
|
||||||
|
"settings.ai.unreachable": "पहुँच से बाहर",
|
||||||
|
"settings.ai.model_count_one": "· {n} मॉडल",
|
||||||
|
"settings.ai.model_count_many": "· {n} मॉडल",
|
||||||
|
"settings.ai.on_lan": "· आपकी मशीन / LAN पर",
|
||||||
|
"settings.ai.leaves_network": "· आपके नेटवर्क से बाहर जाता है",
|
||||||
|
"settings.ai.advanced_title": "उन्नत Ollama कॉन्फ़िगरेशन",
|
||||||
|
"settings.ai.changed": "{n} बदले गए",
|
||||||
|
"settings.ai.reset_all": "सभी को डिफ़ॉल्ट पर रीसेट करें",
|
||||||
|
"settings.ai.system_prompt": "सिस्टम प्रॉम्प्ट",
|
||||||
|
"settings.ai.system_prompt_placeholder": "जैसे आप एक संक्षिप्त मीटिंग सारांशकर्ता हैं।",
|
||||||
|
"settings.ai.system_prompt_hint": "WhispAssist के आवश्यक आउटपुट प्रारूप से पहले जोड़ा जाता है, ताकि आपके निर्देश सारांश/कार्य-आइटम पार्सिंग को न तोड़ सकें।",
|
||||||
|
"settings.ai.think": "सोचें",
|
||||||
|
"settings.ai.think_help": "तर्क प्रयास (केवल तर्क मॉडल)।",
|
||||||
|
"settings.ai.keep_alive": "सक्रिय रखें",
|
||||||
|
"settings.ai.keep_alive_help": "मॉडल RAM में कितनी देर रहता है। जैसे 5m, 1h, 0 (अनलोड), -1 (हमेशा)।",
|
||||||
|
"settings.ai.runtime_hardware": "रनटाइम और हार्डवेयर",
|
||||||
|
"settings.ai.rarely_needed": "— शायद ही कभी आवश्यक",
|
||||||
|
"settings.ai.save_advanced": "उन्नत सहेजें",
|
||||||
|
"settings.ai.saved": "सहेजा गया",
|
||||||
|
"settings.ai.turn_off": "बंद करें",
|
||||||
|
"settings.ai.default_value": "डिफ़ॉल्ट {value}।",
|
||||||
|
"settings.ai.reset_default": "डिफ़ॉल्ट पर रीसेट करें",
|
||||||
|
"settings.ai.reset_field": "{name} रीसेट करें",
|
||||||
|
|
||||||
|
"settings.mcp.title": "MCP सर्वर",
|
||||||
|
"settings.mcp.banner_1": "यह आपके अपने कोडिंग एजेंट (Claude Code, Codex, Copilot, OpenCode, …) को आपकी स्थानीय मशीन पर मीटिंग संदर्भ प्राप्त करने देता है। कनेक्ट होने के बाद,",
|
||||||
|
"settings.mcp.banner_agent": "वह एजेंट",
|
||||||
|
"settings.mcp.banner_2": "जो पढ़ता है उसे अपने स्वयं के मॉडल प्रदाता के क्लाउड में अग्रेषित कर सकता है — WhispAssist के नियंत्रण से बाहर। WhispAssist स्वयं यह डेटा कभी कहीं नहीं भेजता; सर्वर केवल इस डिवाइस पर सुनता है (",
|
||||||
|
"settings.mcp.banner_3": ") और प्रत्येक रीड नीचे लॉग किया जाता है।",
|
||||||
|
"settings.mcp.enable": "MCP सर्वर सक्षम करें",
|
||||||
|
"settings.mcp.enable_hint": "डिफ़ॉल्ट रूप से बंद। केवल लूपबैक, टोकन-सुरक्षित — नेटवर्क से कुछ भी पहुँच योग्य नहीं।",
|
||||||
|
"settings.mcp.transport": "ट्रांसपोर्ट",
|
||||||
|
"settings.mcp.transport_http": "स्ट्रीमिंग HTTP",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (एजेंट एक प्रक्रिया उत्पन्न करता है)",
|
||||||
|
"settings.mcp.port": "पोर्ट",
|
||||||
|
"settings.mcp.transport_change_hint": "ट्रांसपोर्ट/पोर्ट बदलने के लिए, पहले सर्वर बंद करें, फिर वापस चालू करें।",
|
||||||
|
"settings.mcp.scope_title": "दायरा",
|
||||||
|
"settings.mcp.expose": "उजागर करें",
|
||||||
|
"settings.mcp.scope_none": "कोई नहीं — कुछ भी साझा नहीं",
|
||||||
|
"settings.mcp.scope_selected": "चयनित — केवल वे फ़ीचर ब्रीफ़ जिन्हें आपने साझा के रूप में चिह्नित किया है",
|
||||||
|
"settings.mcp.scope_all": "सभी — मीटिंग, ट्रांसक्रिप्ट, कार्य-आइटम और साझा ब्रीफ़",
|
||||||
|
"settings.mcp.expose_recordings": "सहेजी गई रिकॉर्डिंग वाली मीटिंग को भी अनुमति दें (डिफ़ॉल्ट रूप से बंद — जब तक यह चालू न हो, किसी रिकॉर्ड की गई मीटिंग का ट्रांसक्रिप्ट “सभी” दायरे में भी रोका जाता है)",
|
||||||
|
"settings.mcp.token_title": "नया प्रमाणीकरण टोकन — एक बार दिखाया गया, इसे अभी कॉपी करें",
|
||||||
|
"settings.mcp.token_hint": "यह फिर से नहीं दिखाया जाएगा। यह आपके OS क्रेडेंशियल स्टोर में संग्रहीत है; यदि आप इसे खो देते हैं, तो नया बनाने के लिए सर्वर बंद करें और वापस चालू करें।",
|
||||||
|
"settings.mcp.copied": "कॉपी किया गया",
|
||||||
|
"settings.mcp.copy": "कॉपी करें",
|
||||||
|
"settings.mcp.endpoint": "एंडपॉइंट",
|
||||||
|
"settings.mcp.endpoint_hint": "अपने एजेंट के MCP क्लाइंट कॉन्फ़िगरेशन को {kind} की ओर इंगित करें, ऊपर के टोकन को bearer क्रेडेंशियल के रूप में उपयोग करते हुए।",
|
||||||
|
"settings.mcp.kind_command": "कमांड",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "एक्सेस लॉग",
|
||||||
|
"settings.mcp.access_log_hint": "एजेंट द्वारा किया गया प्रत्येक टूल रीड, अनुमत या अस्वीकृत (FR-MCP-5)।",
|
||||||
|
"settings.mcp.no_reads": "अभी तक किसी एजेंट ने कुछ नहीं पढ़ा है।",
|
||||||
|
"settings.mcp.log_meeting": "मीटिंग {id}",
|
||||||
|
"settings.mcp.refresh": "रिफ़्रेश करें",
|
||||||
|
|
||||||
|
"settings.privacy.title": "गोपनीयता",
|
||||||
|
"settings.privacy.intro": "WhispAssist को अभी वास्तव में इस डिवाइस से बाहर क्या भेजने की अनुमति है। सब कुछ बंद होने पर (डिफ़ॉल्ट), डिवाइस से कुछ भी बाहर नहीं जाता।",
|
||||||
|
"settings.privacy.llm_endpoint": "LLM एंडपॉइंट",
|
||||||
|
"settings.privacy.off": "बंद",
|
||||||
|
"settings.privacy.local_only": "केवल स्थानीय",
|
||||||
|
"settings.privacy.leaves_device": "इस डिवाइस से बाहर जाता है",
|
||||||
|
"settings.privacy.sync_label": "सिंक",
|
||||||
|
"settings.privacy.enabled": "सक्षम",
|
||||||
|
"settings.privacy.mcp_label": "MCP सर्वर",
|
||||||
|
"settings.privacy.mcp_on": "चालू · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "केवल लूपबैक पर इनबाउंड — यह ऊपर की आउटगोइंग सूची में कुछ नहीं जोड़ता। एक कनेक्ट किया गया एजेंट फिर भी जो पढ़ता है उसे अपने स्वयं के मॉडल प्रदाता को अग्रेषित कर सकता है; MCP सर्वर टैब देखें।",
|
||||||
|
"settings.privacy.egress_title": "आउटगोइंग अनुमति सूची",
|
||||||
|
"settings.privacy.no_egress": "कोई होस्ट अनुमत सूची में नहीं है — WA कोई सामग्री आउटगोइंग नहीं करता।",
|
||||||
|
"settings.privacy.sync_targets_title": "सिंक लक्ष्य",
|
||||||
|
"settings.privacy.third_party": "तृतीय-पक्ष",
|
||||||
|
"settings.privacy.your_server": "आपका सर्वर",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "कोई TLS नहीं",
|
||||||
|
"settings.privacy.refresh": "रिफ़्रेश करें",
|
||||||
|
"settings.privacy.unavailable": "गोपनीयता स्व-जाँच उपलब्ध नहीं।",
|
||||||
|
"settings.privacy.vault_title": "एन्क्रिप्शन वॉल्ट",
|
||||||
|
"settings.privacy.vault_intro": "एक पासवर्ड के साथ नोट्स, ट्रांसक्रिप्ट और सारांश को स्थिर अवस्था में एन्क्रिप्ट करें। (ऑडियो फ़ाइलें अभी एन्क्रिप्ट नहीं की जातीं।)",
|
||||||
|
"settings.privacy.vault_password": "वॉल्ट पासवर्ड",
|
||||||
|
"settings.privacy.enable_vault": "वॉल्ट सक्षम करें",
|
||||||
|
"settings.privacy.vault_pw_hint": "कम से कम 8 वर्ण उपयोग करें। यदि आप इसे भूल जाते हैं, तो एन्क्रिप्ट की गई सामग्री पुनर्प्राप्त नहीं की जा सकती।",
|
||||||
|
"settings.privacy.vault_locked_1": "वॉल्ट ",
|
||||||
|
"settings.privacy.locked_word": "लॉक",
|
||||||
|
"settings.privacy.vault_locked_2": " है। एन्क्रिप्ट की गई मीटिंग पढ़ने के लिए अनलॉक करें।",
|
||||||
|
"settings.privacy.password": "पासवर्ड",
|
||||||
|
"settings.privacy.unlock": "अनलॉक करें",
|
||||||
|
"settings.privacy.vault_unlocked_1": "वॉल्ट ",
|
||||||
|
"settings.privacy.unlocked_word": "अनलॉक",
|
||||||
|
"settings.privacy.vault_unlocked_2": " है। नए नोट्स, ट्रांसक्रिप्ट और सारांश स्थिर अवस्था में एन्क्रिप्ट किए जाते हैं।",
|
||||||
|
"settings.privacy.lock_now": "अभी लॉक करें",
|
||||||
|
"settings.privacy.change_password": "पासवर्ड बदलें",
|
||||||
|
"settings.privacy.current_password": "वर्तमान पासवर्ड",
|
||||||
|
"settings.privacy.new_password": "नया पासवर्ड",
|
||||||
|
|
||||||
|
"settings.about.title": "परिचय",
|
||||||
|
"settings.about.tagline": "एक पूर्णतः स्थानीय, ओपन-सोर्स, Windows-नेटिव मीटिंग सहायक।",
|
||||||
|
"settings.about.build_commit": "बिल्ड कमिट",
|
||||||
|
|
||||||
|
"consent.heading": "रिकॉर्ड करने से पहले",
|
||||||
|
"consent.body": "प्रतिभागियों की सहमति के बिना बातचीत रिकॉर्ड करना आपके क्षेत्र में अवैध हो सकता है। अपने स्थानीय रिकॉर्डिंग कानूनों की जाँच करें। यह एक सावधानी है, कानूनी सलाह नहीं।",
|
||||||
|
"consent.accept": "मैं समझता हूँ — रिकॉर्डिंग सक्षम करें",
|
||||||
|
"consent.cancel": "रद्द करें",
|
||||||
|
|
||||||
|
"hosted.heading": "{provider} का उपयोग करने से पहले",
|
||||||
|
"hosted.body_1": "के साथ उत्पन्न करना",
|
||||||
|
"hosted.body_2": "इस मीटिंग के ट्रांसक्रिप्ट को उनके सर्वर पर भेजता है — यह इस डिवाइस से बाहर जाता है और उनकी गोपनीयता नीति के अधीन है। एक बार डेटा आपके डिवाइस से बाहर जाने पर WhispAssist का डेटा हैंडलिंग पर कोई नियंत्रण नहीं रहता। यह डिफ़ॉल्ट रूप से बंद है; आप इसे अभी चुन रहे हैं।",
|
||||||
|
"hosted.accept": "मैं समझता हूँ — जारी रखें",
|
||||||
|
"hosted.cancel": "रद्द करें",
|
||||||
|
|
||||||
|
"theme.group": "थीम",
|
||||||
|
"theme.system": "सिस्टम से मिलाएँ",
|
||||||
|
"theme.light": "हल्का",
|
||||||
|
"theme.dark": "गहरा",
|
||||||
|
|
||||||
|
"import.dialog_label": "किसी फ़ाइल या URL से मीटिंग जोड़ें",
|
||||||
|
"import.title": "मीटिंग जोड़ें",
|
||||||
|
"import.close": "बंद करें",
|
||||||
|
"import.close_title": "बंद करें (Esc)",
|
||||||
|
"import.body": "एक मौजूदा रिकॉर्डिंग आयात करें — एक स्थानीय ऑडियो/वीडियो फ़ाइल, या एक लिंक (YouTube, एक स्ट्रीमिंग पेज, या एक सीधा मीडिया URL)। इसे लाइव रिकॉर्डिंग की तरह ही ट्रांसक्राइब और डायराइज़ किया जाता है।",
|
||||||
|
"import.file_or_url": "फ़ाइल या URL",
|
||||||
|
"import.source_placeholder": "एक URL पेस्ट करें, या फ़ाइल के लिए ब्राउज़ करें…",
|
||||||
|
"import.choose_file": "एक स्थानीय फ़ाइल चुनें",
|
||||||
|
"import.browse": "ब्राउज़ करें…",
|
||||||
|
"import.title_label": "शीर्षक",
|
||||||
|
"import.optional": "वैकल्पिक",
|
||||||
|
"import.title_placeholder": "डिफ़ॉल्ट रूप से फ़ाइल का नाम",
|
||||||
|
"import.filter_av": "ऑडियो / वीडियो",
|
||||||
|
"import.requires_1": "आवश्यक है",
|
||||||
|
"import.requires_2": "इंस्टॉल किया गया और आपके PATH में (साथ ही",
|
||||||
|
"import.requires_3": "URL के लिए)। WhispAssist उन्हें बंडल नहीं करता।",
|
||||||
|
"import.importing": "आयात हो रहा है… इसमें कुछ समय लग सकता है",
|
||||||
|
"import.import": "आयात करें",
|
||||||
|
"import.cancel": "रद्द करें",
|
||||||
|
|
||||||
|
"tagchip.filter": "“{tag}” टैग वाली मीटिंग फ़िल्टर करें",
|
||||||
|
"tagchip.remove": "टैग {tag} हटाएँ",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "सिस्टम और माइक्रोफ़ोन इनपुट स्तर",
|
||||||
|
"levelmeter.system": "सिस्टम इनपुट स्तर",
|
||||||
|
|
||||||
|
"app.tagline": "स्थानीय · निजी",
|
||||||
|
"app.note_template": "नोट टेम्पलेट",
|
||||||
|
"app.no_template": "कोई टेम्पलेट नहीं",
|
||||||
|
"app.add_meeting": "मीटिंग जोड़ें",
|
||||||
|
"app.add_meeting_title": "किसी फ़ाइल या URL से मीटिंग जोड़ें",
|
||||||
|
"app.record": "रिकॉर्ड करें",
|
||||||
|
"app.record_title": "रिकॉर्डिंग शुरू करें (Ctrl+Shift+R)",
|
||||||
|
"app.stop": "रोकें",
|
||||||
|
"app.stop_title": "रिकॉर्डिंग रोकें (Ctrl+Shift+R)",
|
||||||
|
"app.cancel": "रद्द करें",
|
||||||
|
"app.cancel_title": "इस रिकॉर्डिंग को छोड़ें और हटाएँ",
|
||||||
|
"app.recording": "रिकॉर्ड हो रहा है…",
|
||||||
|
"app.backend_title": "सक्रिय ट्रांसक्रिप्शन बैकएंड",
|
||||||
|
"app.retention_title": "इस मीटिंग के लिए ऑडियो को .wav के रूप में सहेजें",
|
||||||
|
"app.saving": "सहेजा जा रहा है",
|
||||||
|
"app.not_saved": "सहेजा नहीं गया",
|
||||||
|
"app.reconnecting": "ऑडियो डिवाइस फिर से कनेक्ट हो रहा है…",
|
||||||
|
"app.settings": "सेटिंग्स",
|
||||||
|
"app.settings_title": "सेटिंग्स (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "इस रिकॉर्डिंग को छोड़ें? इसका ऑडियो और ट्रांसक्रिप्ट हटा दिया जाएगा।",
|
||||||
|
"app.announce_started": "रिकॉर्डिंग शुरू हुई",
|
||||||
|
"app.announce_paused": "रिकॉर्डिंग रोकी गई",
|
||||||
|
"app.announce_stopped": "रिकॉर्डिंग रुकी",
|
||||||
|
"app.consent_dialog": "रिकॉर्डिंग सहमति",
|
||||||
|
"app.vault_dialog": "एन्क्रिप्शन वॉल्ट अनलॉक करें",
|
||||||
|
"app.vault_title": "एन्क्रिप्शन वॉल्ट अनलॉक करें",
|
||||||
|
"app.vault_desc": "आपकी मीटिंग स्थिर अवस्था में एन्क्रिप्ट की गई हैं। उन्हें पढ़ने के लिए अपना पासवर्ड दर्ज करें।",
|
||||||
|
"app.vault_password": "वॉल्ट पासवर्ड",
|
||||||
|
"app.vault_incorrect": "गलत पासवर्ड",
|
||||||
|
"app.unlock": "अनलॉक करें",
|
||||||
|
"app.continue_locked": "लॉक रहते हुए जारी रखें",
|
||||||
|
"app.show_meetings": "मीटिंग सूची दिखाएँ",
|
||||||
|
"app.hide_meetings": "मीटिंग सूची छिपाएँ",
|
||||||
|
"app.show_summary": "सारांश पैनल दिखाएँ",
|
||||||
|
"app.hide_summary": "सारांश पैनल छिपाएँ",
|
||||||
|
"app.resize_meetings": "मीटिंग सूची का आकार बदलें",
|
||||||
|
"app.resize_summary": "सारांश पैनल का आकार बदलें",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "मीटिंग खोजें…",
|
||||||
|
"meetings.search_aria": "मीटिंग खोजें",
|
||||||
|
"meetings.filter_tag_aria": "टैग के अनुसार फ़िल्टर करें",
|
||||||
|
"meetings.all_tags": "सभी टैग",
|
||||||
|
"meetings.from_date_aria": "तारीख़ से",
|
||||||
|
"meetings.to_date_aria": "तारीख़ तक",
|
||||||
|
"meetings.bulk_format_aria": "बल्क निर्यात प्रारूप",
|
||||||
|
"meetings.bulk_export": "बल्क निर्यात",
|
||||||
|
"meetings.bulk_export_title": "ऊपर के टैग/तारीख़ फ़िल्टर से मेल खाने वाली प्रत्येक मीटिंग निर्यात करें",
|
||||||
|
"meetings.exporting": "निर्यात हो रहा है…",
|
||||||
|
"meetings.exported_one": "{n} मीटिंग निर्यात की गई।",
|
||||||
|
"meetings.exported_many": "{n} मीटिंग निर्यात की गईं।",
|
||||||
|
"meetings.searching": "खोज हो रही है…",
|
||||||
|
"meetings.loading": "मीटिंग लोड हो रही हैं…",
|
||||||
|
"meetings.no_matches": "कोई मिलान नहीं।",
|
||||||
|
"meetings.empty": "अभी तक कोई मीटिंग नहीं — शुरू करने के लिए ऊपर रिकॉर्ड पर क्लिक करें।",
|
||||||
|
"meetings.status.recording": "रिकॉर्ड हो रहा है",
|
||||||
|
"meetings.status.transcribing": "ट्रांसक्राइब हो रहा है",
|
||||||
|
"meetings.status.recovering": "पुनर्प्राप्त हो रहा है",
|
||||||
|
"meetings.status.error": "त्रुटि",
|
||||||
|
"meetings.resume": "ट्रांसक्रिप्शन फिर से शुरू करें",
|
||||||
|
"meetings.delete_aria": "मीटिंग हटाएँ",
|
||||||
|
"meetings.delete_confirm": "इस मीटिंग को हटाएँ? यह इसकी रिकॉर्डिंग, ट्रांसक्रिप्ट और नोट्स हटा देता है।",
|
||||||
|
|
||||||
|
"transcript.heading": "ट्रांसक्रिप्ट",
|
||||||
|
"transcript.title_aria": "मीटिंग शीर्षक",
|
||||||
|
"transcript.lang_title": "ट्रांसक्रिप्शन भाषा",
|
||||||
|
"transcript.lang_auto": "पहचान हो रही है…",
|
||||||
|
"transcript.show": "ट्रांसक्रिप्ट दिखाएँ",
|
||||||
|
"transcript.hide": "ट्रांसक्रिप्ट छिपाएँ",
|
||||||
|
"transcript.empty": "इस मीटिंग के लिए कोई ट्रांसक्रिप्ट नहीं।",
|
||||||
|
"transcript.will_appear": "जैसे-जैसे आप रिकॉर्ड करते हैं ट्रांसक्रिप्ट यहाँ दिखाई देगा।",
|
||||||
|
"transcript.play_from_here": "यहाँ से चलाएँ",
|
||||||
|
"transcript.resize": "ट्रांसक्रिप्ट और नोट्स का आकार बदलें",
|
||||||
|
"transcript.reprocess_placeholder": "इसके साथ फिर से ट्रांसक्राइब करें…",
|
||||||
|
"transcript.reprocess_lang_aria": "पुनर्प्रसंस्करण भाषा",
|
||||||
|
"transcript.reprocess_keep_lang": "वर्तमान भाषा रखें",
|
||||||
|
"transcript.reprocess_go": "चलाएँ",
|
||||||
|
"transcript.retranscribing": "फिर से ट्रांसक्राइब हो रहा है…",
|
||||||
|
"transcript.has_note": "एक नोट है",
|
||||||
|
"transcript.note_placeholder": "इस पल के लिए एक नोट जोड़ें…",
|
||||||
|
"transcript.note_aria": "इस ट्रांसक्रिप्ट पंक्ति के लिए नोट",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Word दस्तावेज़",
|
||||||
|
"transcript.filter_obsidian": "Obsidian नोट",
|
||||||
|
|
||||||
|
"notes.heading": "नोट्स",
|
||||||
|
"notes.show": "नोट्स दिखाएँ",
|
||||||
|
"notes.hide": "नोट्स छिपाएँ",
|
||||||
|
"notes.placeholder": "नोट्स…",
|
||||||
|
"notes.live_placeholder": "बात करते समय नोट्स टाइप करें…",
|
||||||
|
"notes.toolbar_aria": "नोट्स स्वरूपण",
|
||||||
|
"notes.bold": "बोल्ड",
|
||||||
|
"notes.italic": "इटैलिक",
|
||||||
|
"notes.h1": "शीर्षक 1",
|
||||||
|
"notes.h2": "शीर्षक 2",
|
||||||
|
"notes.bullet": "बुलेट सूची",
|
||||||
|
"notes.checkbox_title": "चेकबॉक्स",
|
||||||
|
"notes.checkbox_aria": "चेकबॉक्स सूची आइटम",
|
||||||
|
"notes.edit_raw": "कच्चा markdown संपादित करें",
|
||||||
|
"notes.render": "markdown रेंडर करें",
|
||||||
|
"notes.editor": "संपादक",
|
||||||
|
"notes.preview": "पूर्वावलोकन",
|
||||||
|
"notes.export_md_title": "नोट्स को .md के रूप में निर्यात करें",
|
||||||
|
"notes.export_pdf_title": "नोट्स को .pdf के रूप में निर्यात करें",
|
||||||
|
"notes.export_docx_title": "नोट्स को .docx के रूप में निर्यात करें",
|
||||||
|
"notes.export_bundle_title": "ऑडियो + ट्रांसक्रिप्ट + नोट्स को एक फ़ोल्डर में निर्यात करें",
|
||||||
|
"notes.export_obsidian_title": "एक Obsidian नोट निर्यात करें (नोट्स + सारांश + ट्रांसक्रिप्ट, कोई ऑडियो नहीं)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "रिकॉर्डिंग",
|
||||||
|
"summary.loading_recording": "रिकॉर्डिंग लोड हो रही है…",
|
||||||
|
"summary.sync_heading": "सिंक",
|
||||||
|
"summary.uploading": "अपलोड हो रहा है…",
|
||||||
|
"summary.upload_now": "अभी अपलोड करें",
|
||||||
|
"summary.no_uploads": "इस मीटिंग के लिए अभी तक कोई अपलोड नहीं।",
|
||||||
|
"summary.retry": "पुनः प्रयास करें",
|
||||||
|
"summary.tags_heading": "टैग",
|
||||||
|
"summary.tags_select": "किसी मीटिंग को टैग करने के लिए उसे चुनें।",
|
||||||
|
"summary.tag_add_placeholder": "टैग जोड़ें…",
|
||||||
|
"summary.tag_first_placeholder": "प्रोजेक्ट, ग्राहक, विषय…",
|
||||||
|
"summary.generating": "उत्पन्न हो रहा है…",
|
||||||
|
"summary.generate_tags": "टैग उत्पन्न करें",
|
||||||
|
"summary.saving": "सहेजा जा रहा है…",
|
||||||
|
"summary.save_tags": "टैग सहेजें",
|
||||||
|
"summary.summary_heading": "सारांश",
|
||||||
|
"summary.ai_provider": "एआई प्रदाता",
|
||||||
|
"summary.provider_off": "बंद",
|
||||||
|
"summary.provider_custom": "कस्टम",
|
||||||
|
"summary.local": "स्थानीय",
|
||||||
|
"summary.leaves_device": "इस डिवाइस से बाहर जाता है",
|
||||||
|
"summary.this_hosted_provider": "यह होस्ट किया गया प्रदाता",
|
||||||
|
"summary.select_generate": "सारांश उत्पन्न करने के लिए एक मीटिंग चुनें।",
|
||||||
|
"summary.decisions_heading": "निर्णय",
|
||||||
|
"summary.regenerate": "फिर से उत्पन्न करें",
|
||||||
|
"summary.generated_hint": "मीटिंग के बाद स्थानीय रूप से उत्पन्न (एक स्थानीय LLM प्रदाता की आवश्यकता है)।",
|
||||||
|
"summary.no_provider": "कोई एआई प्रदाता कॉन्फ़िगर नहीं — सेटिंग्स में एक सक्षम करें।",
|
||||||
|
"summary.generate_summary": "सारांश उत्पन्न करें",
|
||||||
|
"summary.briefs_heading": "फ़ीचर ब्रीफ़",
|
||||||
|
"summary.briefs_select": "फ़ीचर ब्रीफ़ बनाने या देखने के लिए एक मीटिंग चुनें।",
|
||||||
|
"summary.briefs_desc": "इस मीटिंग को एजेंट के लिए तैयार एक विनिर्देश में परिशोधित करता है (एक स्थानीय LLM प्रदाता की आवश्यकता है) — इसे किसी कोडिंग एजेंट को दें, या MCP सर्वर चालू होने पर उसके माध्यम से परोसें।",
|
||||||
|
"summary.brief_repo_placeholder": "लक्ष्य रेपो (वैकल्पिक), जैसे acme/reporting-web",
|
||||||
|
"summary.distilling": "परिशोधित हो रहा है…",
|
||||||
|
"summary.create_brief": "फ़ीचर ब्रीफ़ बनाएँ",
|
||||||
|
"summary.brief_mcp_title": "MCP सर्वर चालू होने पर उपलब्ध",
|
||||||
|
"summary.copied": "कॉपी किया गया",
|
||||||
|
"summary.copy_md": "Markdown के रूप में कॉपी करें",
|
||||||
|
"summary.copy_json": "JSON के रूप में कॉपी करें",
|
||||||
|
"summary.brief_problem": "समस्या",
|
||||||
|
"summary.brief_outcome": "वांछित परिणाम",
|
||||||
|
"summary.brief_criteria": "स्वीकृति मानदंड",
|
||||||
|
"summary.brief_none": "कुछ भी कैप्चर नहीं किया गया।",
|
||||||
|
"summary.brief_context": "संदर्भ",
|
||||||
|
"summary.action_items_heading": "कार्य-आइटम",
|
||||||
|
"summary.ai_select": "इसके कार्य-आइटम देखने के लिए एक मीटिंग चुनें।",
|
||||||
|
"summary.ai_empty": "अभी तक कोई नहीं — नीचे एक जोड़ें, या उन्हें स्वचालित रूप से निकालने के लिए एक सारांश उत्पन्न करें।",
|
||||||
|
"summary.confirmed": "पुष्ट",
|
||||||
|
"summary.ai_text_placeholder": "कार्य-आइटम…",
|
||||||
|
"summary.ai_text_aria": "कार्य-आइटम पाठ",
|
||||||
|
"summary.owner": "स्वामी",
|
||||||
|
"summary.due_date": "नियत तारीख़",
|
||||||
|
"summary.reminder_title": "इस नियत तारीख़ के लिए एक स्थानीय अनुस्मारक शेड्यूल करें",
|
||||||
|
"summary.ai_delete_title": "इस कार्य-आइटम को हटाएँ",
|
||||||
|
"summary.ai_delete_aria": "कार्य-आइटम हटाएँ",
|
||||||
|
"summary.add_action_item": "कार्य-आइटम जोड़ें",
|
||||||
|
"summary.save_action_items": "कार्य-आइटम सहेजें",
|
||||||
|
"summary.calendar_heading": "कैलेंडर ईवेंट",
|
||||||
|
"summary.cal_select": "किसी मीटिंग को कैलेंडर ईवेंट से लिंक करने के लिए उसे चुनें।",
|
||||||
|
"summary.event_search_placeholder": "ईवेंट शीर्षक खोजें…",
|
||||||
|
"summary.linked_event": "लिंक किया गया ईवेंट",
|
||||||
|
"summary.change_event": "ईवेंट बदलें…",
|
||||||
|
"summary.link_event": "एक ईवेंट लिंक करें…",
|
||||||
|
"summary.untitled_event": "(अशीर्षक)",
|
||||||
|
"summary.no_events_1": "अभी तक कोई ईवेंट आयात नहीं — एक",
|
||||||
|
"summary.no_events_2": "सेटिंग्स → कैलेंडर से आयात करें।",
|
||||||
|
"summary.no_events_match": "कोई ईवेंट इस खोज/तारीख़ से मेल नहीं खाता — किसी एक को साफ़ करने का प्रयास करें।",
|
||||||
|
"summary.organizer_label": "आयोजक: {name}",
|
||||||
|
"summary.participants_heading": "प्रतिभागी",
|
||||||
|
"summary.participants_hint": "लिंक किए गए कैलेंडर ईवेंट से भरा जाता है।",
|
||||||
|
"summary.speakers_heading": "वक्ता",
|
||||||
|
"summary.speakers_select": "इसके वक्ताओं को नाम देने के लिए एक मीटिंग चुनें।",
|
||||||
|
"summary.no_speakers": "अभी तक कोई वक्ता नहीं पाया गया।",
|
||||||
|
"summary.speaker_name_placeholder": "वक्ता का नाम",
|
||||||
|
"summary.save": "सहेजें",
|
||||||
|
"summary.cancel": "रद्द करें",
|
||||||
|
"summary.name_speaker": "इस वक्ता को नाम दें…",
|
||||||
|
"summary.add_new_name": "+ नया नाम जोड़ें…",
|
||||||
|
|
||||||
|
"settings.recording.title": "रिकॉर्डिंग",
|
||||||
|
"settings.recording.record_default": "डिफ़ॉल्ट रूप से मीटिंग रिकॉर्ड करें",
|
||||||
|
"settings.recording.save_audio_as": "ऑडियो को इस रूप में सहेजें",
|
||||||
|
"settings.recording.default_hint": "डिफ़ॉल्ट रूप से बंद। जब बंद हो, तो ऑडियो का उपयोग केवल ट्रांसक्रिप्ट बनाने के लिए किया जाता है और मीटिंग अंतिम रूप देने पर हटा दिया जाता है। आप प्रति मीटिंग रिकॉर्डिंग भी टॉगल कर सकते हैं।",
|
||||||
|
"settings.recording.consent_label": "सहमति: {status}।",
|
||||||
|
"settings.recording.consent_ack": "स्वीकृत",
|
||||||
|
"settings.recording.consent_not": "अभी तक स्वीकृत नहीं",
|
||||||
|
"settings.recording.auto_label": "जब कोई कैलेंडर ईवेंट शुरू होता है तो स्वचालित रूप से रिकॉर्डिंग शुरू करें",
|
||||||
|
"settings.recording.auto_hint": "केवल जब WhispAssist खुला हो। जब किसी आयातित कैलेंडर ईवेंट का प्रारंभ समय आता है, तो रिकॉर्डिंग स्वचालित रूप से शुरू होती है (ऊपर की आपकी डिफ़ॉल्ट प्रतिधारण सेटिंग का उपयोग करते हुए)। पृष्ठभूमि में कुछ नहीं चलता — टाइमर केवल तब सक्रिय होता है जब ऐप चल रहा हो। सेटिंग्स → कैलेंडर के अंतर्गत ईवेंट आयात करें।"
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
// Minimal hand-rolled i18n — no dependency. A reactive `locale` (persisted to
|
||||||
|
// localStorage, UI ephemera like the layout store) plus a `t(key)` lookup over
|
||||||
|
// per-language JSON dictionaries. English is the fallback for any missing key,
|
||||||
|
// so a partial translation degrades to English rather than showing raw keys.
|
||||||
|
//
|
||||||
|
// Adding a language: create `<code>.json` next to en.json, import it, and add
|
||||||
|
// it to DICTS + LOCALES below. Translating that one file is the whole job.
|
||||||
|
|
||||||
|
import en from "./en.json";
|
||||||
|
import de from "./de.json";
|
||||||
|
import esES from "./es-ES.json";
|
||||||
|
import esMX from "./es-MX.json";
|
||||||
|
import frFR from "./fr-FR.json";
|
||||||
|
import frCA from "./fr-CA.json";
|
||||||
|
import ptPT from "./pt-PT.json";
|
||||||
|
import ptBR from "./pt-BR.json";
|
||||||
|
import ru from "./ru.json";
|
||||||
|
import sv from "./sv.json";
|
||||||
|
import nb from "./nb.json";
|
||||||
|
import fi from "./fi.json";
|
||||||
|
import zhCN from "./zh-CN.json";
|
||||||
|
import ko from "./ko.json";
|
||||||
|
import hi from "./hi.json";
|
||||||
|
import bn from "./bn.json";
|
||||||
|
import ta from "./ta.json";
|
||||||
|
import si from "./si.json";
|
||||||
|
import ar from "./ar.json";
|
||||||
|
import ur from "./ur.json";
|
||||||
|
|
||||||
|
type Dict = Record<string, string>;
|
||||||
|
|
||||||
|
// Register languages here. en is the source-of-truth key set + fallback.
|
||||||
|
const DICTS: Record<string, Dict> = {
|
||||||
|
en, de, "es-ES": esES, "es-MX": esMX, "fr-FR": frFR, "fr-CA": frCA,
|
||||||
|
"pt-PT": ptPT, "pt-BR": ptBR, ru, sv, nb, fi, "zh-CN": zhCN, ko,
|
||||||
|
hi, bn, ta, si, ar, ur,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Right-to-left scripts; used to flip document direction on locale change.
|
||||||
|
const RTL = new Set(["ar", "ur"]);
|
||||||
|
|
||||||
|
// `label` is the language's endonym so users find their own language in the list.
|
||||||
|
export const LOCALES: { code: string; label: string }[] = [
|
||||||
|
{ code: "en", label: "English" },
|
||||||
|
{ code: "ar", label: "العربية" },
|
||||||
|
{ code: "bn", label: "বাংলা" },
|
||||||
|
{ code: "de", label: "Deutsch" },
|
||||||
|
{ code: "es-ES", label: "Español (España)" },
|
||||||
|
{ code: "es-MX", label: "Español (México)" },
|
||||||
|
{ code: "fi", label: "Suomi" },
|
||||||
|
{ code: "fr-FR", label: "Français (France)" },
|
||||||
|
{ code: "fr-CA", label: "Français (Canada)" },
|
||||||
|
{ code: "hi", label: "हिन्दी" },
|
||||||
|
{ code: "ko", label: "한국어" },
|
||||||
|
{ code: "nb", label: "Norsk bokmål" },
|
||||||
|
{ code: "pt-BR", label: "Português (Brasil)" },
|
||||||
|
{ code: "pt-PT", label: "Português (Portugal)" },
|
||||||
|
{ code: "ru", label: "Русский" },
|
||||||
|
{ code: "si", label: "සිංහල" },
|
||||||
|
{ code: "sv", label: "Svenska" },
|
||||||
|
{ code: "ta", label: "தமிழ்" },
|
||||||
|
{ code: "ur", label: "اردو" },
|
||||||
|
{ code: "zh-CN", label: "中文(简体)" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const KEY = "wa-locale-v1";
|
||||||
|
|
||||||
|
function load(): string {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(KEY);
|
||||||
|
if (saved && saved in DICTS) return saved;
|
||||||
|
} catch {
|
||||||
|
/* localStorage unavailable — fall through to default */
|
||||||
|
}
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flip the document's text direction so RTL locales (Arabic, Urdu) lay out
|
||||||
|
// correctly. Safe outside a browser (SSR/tests): guarded by `document`.
|
||||||
|
function applyDir(code: string) {
|
||||||
|
if (typeof document === "undefined") return;
|
||||||
|
document.documentElement.dir = RTL.has(code) ? "rtl" : "ltr";
|
||||||
|
document.documentElement.lang = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
class I18n {
|
||||||
|
locale = $state(load());
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
applyDir(this.locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocale(code: string) {
|
||||||
|
if (!(code in DICTS)) return;
|
||||||
|
this.locale = code;
|
||||||
|
applyDir(code);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(KEY, code);
|
||||||
|
} catch {
|
||||||
|
/* non-fatal: preference just won't persist */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Look up `key` in the active locale, falling back to English then the key
|
||||||
|
* itself. `vars` fills `{name}` placeholders. Reads `locale` so components
|
||||||
|
* that call `t()` in markup re-render when the language changes. */
|
||||||
|
t = (key: string, vars?: Record<string, string | number>): string => {
|
||||||
|
const dict = DICTS[this.locale] ?? en;
|
||||||
|
let s = dict[key] ?? (en as Dict)[key] ?? key;
|
||||||
|
if (vars) {
|
||||||
|
for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, String(v));
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const i18n = new I18n();
|
||||||
|
export const t = i18n.t;
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "녹음",
|
||||||
|
"nav.hardware": "하드웨어",
|
||||||
|
"nav.storage": "저장소",
|
||||||
|
"nav.calendar": "캘린더",
|
||||||
|
"nav.sync": "동기화",
|
||||||
|
"nav.ai": "AI",
|
||||||
|
"nav.mcp": "MCP 서버",
|
||||||
|
"nav.privacy": "개인정보 보호",
|
||||||
|
"nav.about": "정보",
|
||||||
|
"nav.language": "언어",
|
||||||
|
|
||||||
|
"settings.language.title": "언어",
|
||||||
|
"settings.language.display": "표시 언어",
|
||||||
|
"settings.language.display_hint": "앱 인터페이스의 언어입니다. 언어를 추가하는 것은 번역 파일 하나를 넣는 것만큼 간단합니다.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "전사 언어",
|
||||||
|
"settings.transcription.label": "언어",
|
||||||
|
"settings.transcription.auto": "자동 감지",
|
||||||
|
"settings.transcription.applies_hint": "다음 녹음에 적용됩니다. 실제로 사용된 언어는 이후 각 회의에 표시됩니다.",
|
||||||
|
"settings.transcription.english_only": "영어 전용",
|
||||||
|
"settings.transcription.model_english_only": "“{model}”은(는) 영어 전용입니다.",
|
||||||
|
"settings.transcription.no_model": "선택된 모델이 없습니다.",
|
||||||
|
"settings.transcription.switch_multilingual": "언어를 선택하려면 위에서 다국어 모델로 전환하세요.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "하드웨어",
|
||||||
|
"settings.hardware.active_backend": "활성 백엔드",
|
||||||
|
"settings.hardware.model_meta": "· 모델 {size}",
|
||||||
|
"settings.hardware.preferred_backend": "선호 백엔드",
|
||||||
|
"settings.hardware.auto_backend": "자동(사용 가능한 최적)",
|
||||||
|
"settings.hardware.not_available": "사용 불가",
|
||||||
|
"settings.hardware.fallback_hint": "선택한 백엔드를 로드하지 못하면 자동으로 대체됩니다(NPU → NVIDIA → AMD → Intel → CPU).",
|
||||||
|
"settings.hardware.audio_devices": "오디오 장치",
|
||||||
|
"settings.hardware.recording_device": "녹음 장치",
|
||||||
|
"settings.hardware.default_system_audio": "기본 시스템 오디오",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist는 이 장치가 재생하는 것(루프백)—통화의 상대편—을 녹음합니다. Windows 시스템 기본값을 따르지 않게 하려면 특정 출력을 선택하세요.",
|
||||||
|
"settings.hardware.microphone": "마이크",
|
||||||
|
"settings.hardware.mic_off": "끔 — 내 마이크를 캡처하지 않음",
|
||||||
|
"settings.hardware.default_mic": "기본 마이크",
|
||||||
|
"settings.hardware.mic_hint": "회의의 양쪽이 모두 캡처되도록 실시간 전사에 본인의 음성을 추가합니다. 장치에 남아 있으며 아무것도 업로드되지 않습니다. 위의 시스템 오디오만 전사하려면 “끔”을 선택하세요.",
|
||||||
|
"settings.hardware.npu_detected": "NPU 가속기 감지됨",
|
||||||
|
"settings.hardware.ready": "준비됨",
|
||||||
|
"settings.hardware.downloading": "다운로드 중…",
|
||||||
|
"settings.hardware.package_needed": "패키지 필요",
|
||||||
|
"settings.hardware.npu_hint": "NPU에는 일회성 가속 패키지(Whisper ONNX 모델{extra})가 필요합니다. 백그라운드에서 다운로드되며 지금 시작할 수도 있습니다.",
|
||||||
|
"settings.hardware.plus_openvino": " + OpenVINO 런타임",
|
||||||
|
"settings.hardware.download_npu": "NPU 패키지 다운로드",
|
||||||
|
"settings.hardware.dml_title": "GPU 가속(DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "GPU는 Vulkan 없이 DirectML을 통해 가속할 수 있습니다 — 일회성 패키지(Whisper ONNX 모델{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + DirectML 런타임",
|
||||||
|
"settings.hardware.download_dml": "DirectML 패키지 다운로드",
|
||||||
|
"settings.hardware.detection_unavailable": "하드웨어 감지를 사용할 수 없습니다.",
|
||||||
|
"settings.hardware.low_overhead": "낮은 오버헤드 사전 설정",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + 가장 작은 모델, 배터리/백그라운드 사용용",
|
||||||
|
"settings.hardware.models": "모델",
|
||||||
|
"settings.hardware.downloading_model": "{name} 다운로드 중",
|
||||||
|
"settings.hardware.active_badge": "활성",
|
||||||
|
"settings.hardware.installed": "설치됨",
|
||||||
|
"settings.hardware.use": "사용",
|
||||||
|
"settings.hardware.remove": "제거",
|
||||||
|
"settings.hardware.cant_remove_active": "활성 모델은 제거할 수 없습니다",
|
||||||
|
"settings.hardware.download": "다운로드",
|
||||||
|
"settings.hardware.diarization": "화자 분리",
|
||||||
|
"settings.hardware.diarization_hint": "두 모델을 모두 설치하면 완료된 녹음이 하나의 연속 전사가 아니라 화자별(화자 1, 화자 2, …)로 분리됩니다. 완전히 오프라인으로 실행되며, 중지 후 회의당 한 번 실행됩니다. 이들이 없으면 모든 줄이 단일 화자에게 귀속됩니다.",
|
||||||
|
|
||||||
|
"settings.storage.title": "저장소",
|
||||||
|
"settings.storage.location_label": "위치",
|
||||||
|
"settings.storage.location_hint": "저장 위치 변경은 아직 지원되지 않습니다 — 현재 회의, 모델, 데이터베이스가 여기에 있습니다.",
|
||||||
|
"settings.storage.retention_title": "보존",
|
||||||
|
"settings.storage.retention_hint": "한도를 초과하는 가장 오래된 회의를 자동으로 삭제합니다. 시작 시 한 번 확인하며, 비워 두면 한도가 없습니다. 현재 녹음 중인 회의는 절대 건드리지 않습니다.",
|
||||||
|
"settings.storage.max_age": "최대 보관 기간(일)",
|
||||||
|
"settings.storage.max_size": "최대 크기(GB)",
|
||||||
|
"settings.storage.no_limit": "한도 없음",
|
||||||
|
"settings.storage.export_import_title": "내보내기 및 가져오기",
|
||||||
|
"settings.storage.export_hint_1": "녹음을 이동식 번들 폴더(오디오, 전사, 노트, 요약 및",
|
||||||
|
"settings.storage.export_hint_2": "매니페스트)로 컴퓨터 간에 이동하세요. 아무 폴더 — 동기화된 드라이브, USB 메모리, 동기화 대상의 로컬 마운트 지점 — 로 내보낸 다음 다른 컴퓨터에서 가져오세요. 가져온 회의는 새 id를 받으므로 다시 가져와도 아무것도 덮어쓰지 않습니다.",
|
||||||
|
"settings.storage.export_dialog_title": "모든 회의를 다음으로 내보내기…",
|
||||||
|
"settings.storage.import_dialog_title": "번들(또는 번들 폴더) 가져오기…",
|
||||||
|
"settings.storage.exporting": "내보내는 중…",
|
||||||
|
"settings.storage.export_all": "모든 회의 내보내기…",
|
||||||
|
"settings.storage.importing": "가져오는 중…",
|
||||||
|
"settings.storage.import_all": "회의 가져오기…",
|
||||||
|
"settings.storage.exported_one": "회의 {n}개를 내보냈습니다.",
|
||||||
|
"settings.storage.exported_many": "회의 {n}개를 내보냈습니다.",
|
||||||
|
"settings.storage.imported_one": "회의 {n}개를 가져왔습니다.",
|
||||||
|
"settings.storage.imported_many": "회의 {n}개를 가져왔습니다.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "캘린더 및 Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "로컬 Outlook 백업에서 이벤트와 참석자를 가져옵니다",
|
||||||
|
"settings.calendar.intro_2": "— 읽기 전용이며 파일에 아무것도 다시 쓰지 않습니다. 이 장치를 벗어나는 것은 없습니다.",
|
||||||
|
"settings.calendar.pst_file": ".pst 파일",
|
||||||
|
"settings.calendar.no_file": "선택된 파일 없음",
|
||||||
|
"settings.calendar.browse": "찾아보기…",
|
||||||
|
"settings.calendar.password": "비밀번호",
|
||||||
|
"settings.calendar.password_note": "거의 필요 없음",
|
||||||
|
"settings.calendar.optional": "선택 사항",
|
||||||
|
"settings.calendar.import_range": "가져오기 범위",
|
||||||
|
"settings.calendar.range_30": "최근 30일",
|
||||||
|
"settings.calendar.range_90": "최근 90일",
|
||||||
|
"settings.calendar.range_180": "최근 6개월",
|
||||||
|
"settings.calendar.range_365": "최근 1년",
|
||||||
|
"settings.calendar.range_all": "전체 기간",
|
||||||
|
"settings.calendar.range_hint": "오래된 사서함에는 수년간의 반복/휴일 항목이 있을 수 있습니다 — 범위를 좁히면 가져온 캘린더가 실제로 관련 있는 것만 유지됩니다. 이 가져오기 버튼과 시작 시 자동 재가져오기 모두에 적용됩니다.",
|
||||||
|
"settings.calendar.importing": "가져오는 중…",
|
||||||
|
"settings.calendar.import": "가져오기",
|
||||||
|
"settings.calendar.import_progress": "이벤트 {processed}/{total}개",
|
||||||
|
"settings.calendar.auto_reimport": "시작 시 이 파일을 자동으로 다시 가져오기",
|
||||||
|
"settings.calendar.auto_reimport_hint": "타이머가 아니라 시작 시 한 번 실행됩니다 — 재가져오기는 안전하게 반복할 수 있습니다(기존 이벤트는 매칭되어 업데이트되며 중복되지 않습니다).",
|
||||||
|
"settings.calendar.import_failed": "가져오기 실패: {error} — 파일 자체는 손대지 않습니다. 경로를 확인하고 다시 시도하세요.",
|
||||||
|
"settings.calendar.cleanup_title": "정리",
|
||||||
|
"settings.calendar.cleanup_hint": "이미 녹음된 회의에 연결된 이벤트는 아래에서 어떤 옵션을 선택하든 항상 유지됩니다.",
|
||||||
|
"settings.calendar.cleanup_30": "30일 이상 지난 항목",
|
||||||
|
"settings.calendar.cleanup_90": "90일 이상 지난 항목",
|
||||||
|
"settings.calendar.cleanup_180": "6개월 이상 지난 항목",
|
||||||
|
"settings.calendar.cleanup_365": "1년 이상 지난 항목",
|
||||||
|
"settings.calendar.cleanup_all": "모두 삭제",
|
||||||
|
"settings.calendar.cleaning": "정리 중…",
|
||||||
|
"settings.calendar.cleanup_btn": "캘린더 정리",
|
||||||
|
"settings.calendar.cleanup_result": "{deleted}{extra} 삭제됨.",
|
||||||
|
"settings.calendar.cleanup_kept": ", {n}개 유지됨(회의에 연결됨)",
|
||||||
|
"settings.calendar.cleanup_failed": "정리 실패: {error}",
|
||||||
|
"settings.calendar.imported_events": "가져온 이벤트",
|
||||||
|
"settings.calendar.no_events": "아직 가져온 이벤트가 없습니다.",
|
||||||
|
"settings.calendar.search_title": "제목 검색",
|
||||||
|
"settings.calendar.search_placeholder": "회의 이름…",
|
||||||
|
"settings.calendar.date_label": "날짜",
|
||||||
|
"settings.calendar.events_count": "이벤트 {total}개 중 {shown}개",
|
||||||
|
"settings.calendar.untitled": "(제목 없음)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "설정",
|
||||||
|
"settings.close": "닫기",
|
||||||
|
"settings.close_title": "닫기(Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "미리 보기 모드 — 동기화 백엔드는 아직 구현되지 않았습니다(9단계). 변경 사항은 인터페이스에만 유지됩니다.",
|
||||||
|
"settings.sync.title": "동기화 및 업로드",
|
||||||
|
"settings.sync.enable_label": "구성된 대상으로 회의 아티팩트 업로드 활성화",
|
||||||
|
"settings.sync.enable_hint": "기본적으로 꺼져 있습니다. 이 항목이 켜져 있고 대상이 활성화되어 있지 않으면 아무것도 업로드되지 않습니다. 자체 호스팅 대상은 데이터를 본인의 서버에 보관하며, 타사 클라우드는 명확히 표시됩니다.",
|
||||||
|
"settings.sync.targets_title": "대상",
|
||||||
|
"settings.sync.no_targets": "아직 대상이 없습니다. 아래에서 하나 추가하세요.",
|
||||||
|
"settings.sync.third_party": "타사",
|
||||||
|
"settings.sync.your_server": "내 서버",
|
||||||
|
"settings.sync.edit": "편집",
|
||||||
|
"settings.sync.remove": "제거",
|
||||||
|
"settings.sync.edit_target": "대상 편집",
|
||||||
|
"settings.sync.add_target": "대상 추가",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV는 Nextcloud, ownCloud, Cloudreve, Seafile, Synology를 지원합니다. (Seafile: 서버 측에서 SeafDAV를 활성화하세요.)",
|
||||||
|
"settings.sync.name": "이름",
|
||||||
|
"settings.sync.name_placeholder": "홈 Nextcloud",
|
||||||
|
"settings.sync.provider": "공급자",
|
||||||
|
"settings.sync.server_url": "서버 URL",
|
||||||
|
"settings.sync.server_url_note_1": "서버 URL만 입력하세요 — WhispAssist가",
|
||||||
|
"settings.sync.server_url_note_2": "을(를) 자동으로 추가합니다.",
|
||||||
|
"settings.sync.remote_folder": "원격 폴더",
|
||||||
|
"settings.sync.username": "사용자 이름",
|
||||||
|
"settings.sync.app_password": "앱 비밀번호",
|
||||||
|
"settings.sync.pw_keep": "현재 비밀번호를 유지하려면 비워 두세요",
|
||||||
|
"settings.sync.pw_store": "운영 체제 자격 증명 저장소에 저장됨",
|
||||||
|
"settings.sync.upload_legend": "업로드",
|
||||||
|
"settings.sync.artifact_transcript": "전사",
|
||||||
|
"settings.sync.artifact_notes": "노트",
|
||||||
|
"settings.sync.artifact_summary": "요약",
|
||||||
|
"settings.sync.artifact_recording": "녹음(.wav, 보존된 경우)",
|
||||||
|
"settings.sync.allow_plaintext": "LAN 주소에 대해 평문 http 허용(권장하지 않음)",
|
||||||
|
"settings.sync.encrypt_before": "업로드 전 암호화(대상은 암호문만 저장)",
|
||||||
|
"settings.sync.requires_vault": "— 잠금 해제된 보관소가 필요합니다",
|
||||||
|
"settings.sync.test_connection": "연결 테스트",
|
||||||
|
"settings.sync.save_changes": "변경 사항 저장",
|
||||||
|
"settings.sync.add_target_btn": "대상 추가",
|
||||||
|
"settings.sync.cancel": "취소",
|
||||||
|
"settings.sync.third_party_banner": "{kind}은(는) 타사 클라우드입니다 — 업로드하면 데이터가 장치를 벗어나 {kind}(으)로 전송됩니다.",
|
||||||
|
"settings.sync.link_account": "{kind} 계정 연결…",
|
||||||
|
"settings.sync.oauth_hint": "OAuth 로그인(루프백 리디렉션)을 엽니다. 토큰은 운영 체제 자격 증명 저장소에 저장됩니다.",
|
||||||
|
"settings.sync.footnote": "자격 증명은 설정이나 데이터베이스에 절대 기록되지 않습니다 — 운영 체제 자격 증명 저장소에만 저장됩니다. LAN 이외의 대상에는 TLS가 필요합니다.",
|
||||||
|
|
||||||
|
"settings.ai.title": "AI 요약 공급자",
|
||||||
|
"settings.ai.intro_1": "요약은 기본적으로 로컬 LLM에서 실행됩니다. 이 PC 또는 LAN의 다른 컴퓨터에 있는 Ollama를 가리키도록 설정하세요(예:",
|
||||||
|
"settings.ai.intro_2": "— 둘 다 로컬로 간주되므로 네트워크를 벗어나는 것이 없습니다. 호스팅 공급자(Anthropic)는 선택 사항이며 기본적으로 꺼져 있고, 하나를 켜는 순간 전사를 제3자에게 전송합니다.",
|
||||||
|
"settings.ai.provider": "공급자",
|
||||||
|
"settings.ai.off": "끔",
|
||||||
|
"settings.ai.provider_ollama": "Ollama(로컬 / LAN)",
|
||||||
|
"settings.ai.provider_custom": "사용자 지정(OpenAI 호환)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic(Claude) — 호스팅됨, 이 장치를 벗어남",
|
||||||
|
"settings.ai.endpoint": "엔드포인트",
|
||||||
|
"settings.ai.model": "모델",
|
||||||
|
"settings.ai.api_key": "API 키",
|
||||||
|
"settings.ai.api_key_set_placeholder": "••••••••••••••••(이미 설정됨 — 유지하려면 비워 두세요)",
|
||||||
|
"settings.ai.show_api_key": "API 키 표시",
|
||||||
|
"settings.ai.hide_api_key": "API 키 숨기기",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic은 호스팅된 타사 서비스입니다 — 요약을 생성하면 이 회의의 전사가 장치를 벗어납니다.",
|
||||||
|
"settings.ai.endpoint_banner": "이 엔드포인트는 내 컴퓨터나 LAN에 없습니다 — 전사가 네트워크를 벗어나게 됩니다.",
|
||||||
|
"settings.ai.saving": "저장 중…",
|
||||||
|
"settings.ai.save": "저장",
|
||||||
|
"settings.ai.test_connection": "연결 테스트",
|
||||||
|
"settings.ai.this_hosted_provider": "이 호스팅 공급자",
|
||||||
|
"settings.ai.reachable": "연결 가능",
|
||||||
|
"settings.ai.unreachable": "연결 불가",
|
||||||
|
"settings.ai.model_count_one": "· 모델 {n}개",
|
||||||
|
"settings.ai.model_count_many": "· 모델 {n}개",
|
||||||
|
"settings.ai.on_lan": "· 내 컴퓨터 / LAN에서",
|
||||||
|
"settings.ai.leaves_network": "· 네트워크를 벗어남",
|
||||||
|
"settings.ai.advanced_title": "Ollama 고급 구성",
|
||||||
|
"settings.ai.changed": "{n}개 변경됨",
|
||||||
|
"settings.ai.reset_all": "모두 기본값으로 재설정",
|
||||||
|
"settings.ai.system_prompt": "시스템 프롬프트",
|
||||||
|
"settings.ai.system_prompt_placeholder": "예: 당신은 간결한 회의 요약가입니다.",
|
||||||
|
"settings.ai.system_prompt_hint": "WhispAssist에 필요한 출력 형식 앞에 추가되므로, 지침이 요약/작업 항목 구문 분석을 깨뜨릴 수 없습니다.",
|
||||||
|
"settings.ai.think": "사고",
|
||||||
|
"settings.ai.think_help": "추론 노력(추론 모델 전용).",
|
||||||
|
"settings.ai.keep_alive": "활성 유지",
|
||||||
|
"settings.ai.keep_alive_help": "모델이 RAM에 유지되는 시간. 예: 5m, 1h, 0(언로드), -1(영구).",
|
||||||
|
"settings.ai.runtime_hardware": "런타임 및 하드웨어",
|
||||||
|
"settings.ai.rarely_needed": "— 거의 필요하지 않음",
|
||||||
|
"settings.ai.save_advanced": "고급 저장",
|
||||||
|
"settings.ai.saved": "저장됨",
|
||||||
|
"settings.ai.turn_off": "끄기",
|
||||||
|
"settings.ai.default_value": "기본값 {value}.",
|
||||||
|
"settings.ai.reset_default": "기본값으로 재설정",
|
||||||
|
"settings.ai.reset_field": "{name} 재설정",
|
||||||
|
|
||||||
|
"settings.mcp.title": "MCP 서버",
|
||||||
|
"settings.mcp.banner_1": "이를 통해 본인의 코딩 에이전트(Claude Code, Codex, Copilot, OpenCode 등)가 로컬 컴퓨터에서 회의 컨텍스트를 가져올 수 있습니다. 연결되면",
|
||||||
|
"settings.mcp.banner_agent": "해당 에이전트",
|
||||||
|
"settings.mcp.banner_2": "가 읽은 내용을 자체 모델 공급자의 클라우드로 전달할 수 있습니다 — WhispAssist의 제어를 벗어납니다. WhispAssist 자체는 이 데이터를 어디에도 전송하지 않으며, 서버는 이 장치에서만 수신 대기합니다(",
|
||||||
|
"settings.mcp.banner_3": "). 모든 읽기는 아래에 기록됩니다.",
|
||||||
|
"settings.mcp.enable": "MCP 서버 활성화",
|
||||||
|
"settings.mcp.enable_hint": "기본적으로 꺼져 있습니다. 루프백 전용, 토큰 보호 — 네트워크에서 접근할 수 있는 것이 없습니다.",
|
||||||
|
"settings.mcp.transport": "전송",
|
||||||
|
"settings.mcp.transport_http": "스트리밍 HTTP",
|
||||||
|
"settings.mcp.transport_stdio": "stdio(에이전트가 프로세스를 생성함)",
|
||||||
|
"settings.mcp.port": "포트",
|
||||||
|
"settings.mcp.transport_change_hint": "전송/포트를 변경하려면 먼저 서버를 끈 다음 다시 켜세요.",
|
||||||
|
"settings.mcp.scope_title": "범위",
|
||||||
|
"settings.mcp.expose": "노출",
|
||||||
|
"settings.mcp.scope_none": "없음 — 아무것도 공유하지 않음",
|
||||||
|
"settings.mcp.scope_selected": "선택됨 — 공유로 표시한 기능 브리프만",
|
||||||
|
"settings.mcp.scope_all": "전체 — 회의, 전사, 작업 항목 및 공유 브리프",
|
||||||
|
"settings.mcp.expose_recordings": "저장된 녹음이 있는 회의도 허용(기본적으로 꺼짐 — 이 항목이 켜질 때까지 녹음된 회의의 전사는 “전체” 범위에서도 보류됩니다)",
|
||||||
|
"settings.mcp.token_title": "새 인증 토큰 — 한 번만 표시되므로 지금 복사하세요",
|
||||||
|
"settings.mcp.token_hint": "이 내용은 다시 표시되지 않습니다. 운영 체제 자격 증명 저장소에 저장됩니다. 분실한 경우 서버를 껐다가 다시 켜 새 토큰을 발급하세요.",
|
||||||
|
"settings.mcp.copied": "복사됨",
|
||||||
|
"settings.mcp.copy": "복사",
|
||||||
|
"settings.mcp.endpoint": "엔드포인트",
|
||||||
|
"settings.mcp.endpoint_hint": "에이전트의 MCP 클라이언트 구성을 {kind}(으)로 지정하고, 위의 토큰을 bearer 자격 증명으로 사용하세요.",
|
||||||
|
"settings.mcp.kind_command": "명령",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "접근 로그",
|
||||||
|
"settings.mcp.access_log_hint": "에이전트가 수행한 모든 도구 읽기(허용 또는 거부)(FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "아직 아무 에이전트도 읽지 않았습니다.",
|
||||||
|
"settings.mcp.log_meeting": "회의 {id}",
|
||||||
|
"settings.mcp.refresh": "새로 고침",
|
||||||
|
|
||||||
|
"settings.privacy.title": "개인정보 보호",
|
||||||
|
"settings.privacy.intro": "WhispAssist가 현재 이 장치 밖으로 실제로 보낼 수 있도록 허용된 내용입니다. 모두 꺼져 있으면(기본값) 장치를 벗어나는 것이 전혀 없습니다.",
|
||||||
|
"settings.privacy.llm_endpoint": "LLM 엔드포인트",
|
||||||
|
"settings.privacy.off": "끔",
|
||||||
|
"settings.privacy.local_only": "로컬 전용",
|
||||||
|
"settings.privacy.leaves_device": "이 장치를 벗어남",
|
||||||
|
"settings.privacy.sync_label": "동기화",
|
||||||
|
"settings.privacy.enabled": "활성화됨",
|
||||||
|
"settings.privacy.mcp_label": "MCP 서버",
|
||||||
|
"settings.privacy.mcp_on": "켬 · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "루프백에서만 수신 — 위의 송신 목록에 아무것도 추가하지 않습니다. 연결된 에이전트는 여전히 읽은 내용을 자체 모델 공급자에게 전달할 수 있습니다. MCP 서버 탭을 참조하세요.",
|
||||||
|
"settings.privacy.egress_title": "송신 허용 목록",
|
||||||
|
"settings.privacy.no_egress": "허용 목록에 있는 호스트가 없습니다 — WA는 콘텐츠 송신을 하지 않습니다.",
|
||||||
|
"settings.privacy.sync_targets_title": "동기화 대상",
|
||||||
|
"settings.privacy.third_party": "타사",
|
||||||
|
"settings.privacy.your_server": "내 서버",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "TLS 없음",
|
||||||
|
"settings.privacy.refresh": "새로 고침",
|
||||||
|
"settings.privacy.unavailable": "개인정보 자체 점검을 사용할 수 없습니다.",
|
||||||
|
"settings.privacy.vault_title": "암호화 보관소",
|
||||||
|
"settings.privacy.vault_intro": "비밀번호로 노트, 전사, 요약을 저장 시 암호화합니다. (오디오 파일은 아직 암호화되지 않습니다.)",
|
||||||
|
"settings.privacy.vault_password": "보관소 비밀번호",
|
||||||
|
"settings.privacy.enable_vault": "보관소 활성화",
|
||||||
|
"settings.privacy.vault_pw_hint": "8자 이상 사용하세요. 잊어버리면 암호화된 콘텐츠를 복구할 수 없습니다.",
|
||||||
|
"settings.privacy.vault_locked_1": "보관소가 ",
|
||||||
|
"settings.privacy.locked_word": "잠김",
|
||||||
|
"settings.privacy.vault_locked_2": " 상태입니다. 암호화된 회의를 읽으려면 잠금을 해제하세요.",
|
||||||
|
"settings.privacy.password": "비밀번호",
|
||||||
|
"settings.privacy.unlock": "잠금 해제",
|
||||||
|
"settings.privacy.vault_unlocked_1": "보관소가 ",
|
||||||
|
"settings.privacy.unlocked_word": "잠금 해제됨",
|
||||||
|
"settings.privacy.vault_unlocked_2": " 상태입니다. 새 노트, 전사, 요약은 저장 시 암호화됩니다.",
|
||||||
|
"settings.privacy.lock_now": "지금 잠그기",
|
||||||
|
"settings.privacy.change_password": "비밀번호 변경",
|
||||||
|
"settings.privacy.current_password": "현재 비밀번호",
|
||||||
|
"settings.privacy.new_password": "새 비밀번호",
|
||||||
|
|
||||||
|
"settings.about.title": "정보",
|
||||||
|
"settings.about.tagline": "완전히 로컬이며 오픈 소스인 Windows 네이티브 회의 도우미입니다.",
|
||||||
|
"settings.about.build_commit": "빌드 커밋",
|
||||||
|
|
||||||
|
"consent.heading": "녹음하기 전에",
|
||||||
|
"consent.body": "참가자의 동의 없이 대화를 녹음하는 것은 귀하의 지역에서 불법일 수 있습니다. 현지 녹음 법률을 확인하세요. 이것은 주의 사항이며 법률 자문이 아닙니다.",
|
||||||
|
"consent.accept": "이해했습니다 — 녹음 활성화",
|
||||||
|
"consent.cancel": "취소",
|
||||||
|
|
||||||
|
"hosted.heading": "{provider} 사용 전",
|
||||||
|
"hosted.body_1": "다음으로 생성하면",
|
||||||
|
"hosted.body_2": "이 회의의 전사가 그들의 서버로 전송됩니다 — 이 장치를 벗어나며 그들의 개인정보 처리방침이 적용됩니다. 데이터가 장치를 벗어난 후에는 WhispAssist가 데이터 처리에 대해 아무런 제어권이 없습니다. 이 항목은 기본적으로 꺼져 있으며, 지금 선택하는 것입니다.",
|
||||||
|
"hosted.accept": "이해했습니다 — 계속",
|
||||||
|
"hosted.cancel": "취소",
|
||||||
|
|
||||||
|
"theme.group": "테마",
|
||||||
|
"theme.system": "시스템에 맞춤",
|
||||||
|
"theme.light": "밝게",
|
||||||
|
"theme.dark": "어둡게",
|
||||||
|
|
||||||
|
"import.dialog_label": "파일 또는 URL에서 회의 추가",
|
||||||
|
"import.title": "회의 추가",
|
||||||
|
"import.close": "닫기",
|
||||||
|
"import.close_title": "닫기(Esc)",
|
||||||
|
"import.body": "기존 녹음을 가져옵니다 — 로컬 오디오/비디오 파일 또는 링크(YouTube, 스트리밍 페이지 또는 직접 미디어 URL). 실시간 녹음과 마찬가지로 전사되고 화자 분리됩니다.",
|
||||||
|
"import.file_or_url": "파일 또는 URL",
|
||||||
|
"import.source_placeholder": "URL을 붙여넣거나 파일을 찾아보세요…",
|
||||||
|
"import.choose_file": "로컬 파일 선택",
|
||||||
|
"import.browse": "찾아보기…",
|
||||||
|
"import.title_label": "제목",
|
||||||
|
"import.optional": "선택 사항",
|
||||||
|
"import.title_placeholder": "기본값은 파일 이름",
|
||||||
|
"import.filter_av": "오디오 / 비디오",
|
||||||
|
"import.requires_1": "다음이 필요합니다:",
|
||||||
|
"import.requires_2": "이(가) 설치되어 PATH에 있어야 합니다(URL의 경우",
|
||||||
|
"import.requires_3": "도 필요). WhispAssist는 이들을 포함하지 않습니다.",
|
||||||
|
"import.importing": "가져오는 중… 시간이 걸릴 수 있습니다",
|
||||||
|
"import.import": "가져오기",
|
||||||
|
"import.cancel": "취소",
|
||||||
|
|
||||||
|
"tagchip.filter": "“{tag}” 태그가 지정된 회의 필터링",
|
||||||
|
"tagchip.remove": "{tag} 태그 제거",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "시스템 및 마이크 입력 레벨",
|
||||||
|
"levelmeter.system": "시스템 입력 레벨",
|
||||||
|
|
||||||
|
"app.tagline": "로컬 · 비공개",
|
||||||
|
"app.note_template": "노트 템플릿",
|
||||||
|
"app.no_template": "템플릿 없음",
|
||||||
|
"app.add_meeting": "회의 추가",
|
||||||
|
"app.add_meeting_title": "파일 또는 URL에서 회의 추가",
|
||||||
|
"app.record": "녹음",
|
||||||
|
"app.record_title": "녹음 시작(Ctrl+Shift+R)",
|
||||||
|
"app.stop": "중지",
|
||||||
|
"app.stop_title": "녹음 중지(Ctrl+Shift+R)",
|
||||||
|
"app.cancel": "취소",
|
||||||
|
"app.cancel_title": "이 녹음을 취소하고 삭제",
|
||||||
|
"app.recording": "녹음 중…",
|
||||||
|
"app.backend_title": "활성 전사 백엔드",
|
||||||
|
"app.retention_title": "이 회의의 오디오를 .wav로 저장",
|
||||||
|
"app.saving": "저장 중",
|
||||||
|
"app.not_saved": "저장되지 않음",
|
||||||
|
"app.reconnecting": "오디오 장치를 다시 연결하는 중…",
|
||||||
|
"app.settings": "설정",
|
||||||
|
"app.settings_title": "설정(Ctrl+,)",
|
||||||
|
"app.discard_confirm": "이 녹음을 취소하시겠습니까? 오디오와 전사가 삭제됩니다.",
|
||||||
|
"app.announce_started": "녹음 시작됨",
|
||||||
|
"app.announce_paused": "녹음 일시 중지됨",
|
||||||
|
"app.announce_stopped": "녹음 중지됨",
|
||||||
|
"app.consent_dialog": "녹음 동의",
|
||||||
|
"app.vault_dialog": "암호화 보관소 잠금 해제",
|
||||||
|
"app.vault_title": "암호화 보관소 잠금 해제",
|
||||||
|
"app.vault_desc": "회의가 저장 시 암호화되어 있습니다. 읽으려면 비밀번호를 입력하세요.",
|
||||||
|
"app.vault_password": "보관소 비밀번호",
|
||||||
|
"app.vault_incorrect": "비밀번호가 올바르지 않습니다",
|
||||||
|
"app.unlock": "잠금 해제",
|
||||||
|
"app.continue_locked": "잠긴 상태로 계속",
|
||||||
|
"app.show_meetings": "회의 목록 표시",
|
||||||
|
"app.hide_meetings": "회의 목록 숨기기",
|
||||||
|
"app.show_summary": "요약 패널 표시",
|
||||||
|
"app.hide_summary": "요약 패널 숨기기",
|
||||||
|
"app.resize_meetings": "회의 목록 크기 조정",
|
||||||
|
"app.resize_summary": "요약 패널 크기 조정",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "회의 검색…",
|
||||||
|
"meetings.search_aria": "회의 검색",
|
||||||
|
"meetings.filter_tag_aria": "태그로 필터링",
|
||||||
|
"meetings.all_tags": "모든 태그",
|
||||||
|
"meetings.from_date_aria": "시작 날짜",
|
||||||
|
"meetings.to_date_aria": "종료 날짜",
|
||||||
|
"meetings.bulk_format_aria": "일괄 내보내기 형식",
|
||||||
|
"meetings.bulk_export": "일괄 내보내기",
|
||||||
|
"meetings.bulk_export_title": "위의 태그/날짜 필터와 일치하는 모든 회의 내보내기",
|
||||||
|
"meetings.exporting": "내보내는 중…",
|
||||||
|
"meetings.exported_one": "회의 {n}개를 내보냈습니다.",
|
||||||
|
"meetings.exported_many": "회의 {n}개를 내보냈습니다.",
|
||||||
|
"meetings.searching": "검색 중…",
|
||||||
|
"meetings.loading": "회의를 불러오는 중…",
|
||||||
|
"meetings.no_matches": "일치하는 항목이 없습니다.",
|
||||||
|
"meetings.empty": "아직 회의가 없습니다 — 위의 녹음을 클릭하여 시작하세요.",
|
||||||
|
"meetings.status.recording": "녹음 중",
|
||||||
|
"meetings.status.transcribing": "전사 중",
|
||||||
|
"meetings.status.recovering": "복구 중",
|
||||||
|
"meetings.status.error": "오류",
|
||||||
|
"meetings.resume": "전사 재개",
|
||||||
|
"meetings.delete_aria": "회의 삭제",
|
||||||
|
"meetings.delete_confirm": "이 회의를 삭제하시겠습니까? 녹음, 전사, 노트가 제거됩니다.",
|
||||||
|
|
||||||
|
"transcript.heading": "전사",
|
||||||
|
"transcript.title_aria": "회의 제목",
|
||||||
|
"transcript.lang_title": "전사 언어",
|
||||||
|
"transcript.lang_auto": "감지 중…",
|
||||||
|
"transcript.show": "전사 표시",
|
||||||
|
"transcript.hide": "전사 숨기기",
|
||||||
|
"transcript.empty": "이 회의에 대한 전사가 없습니다.",
|
||||||
|
"transcript.will_appear": "녹음하는 동안 전사가 여기에 표시됩니다.",
|
||||||
|
"transcript.play_from_here": "여기서부터 재생",
|
||||||
|
"transcript.resize": "전사 및 노트 크기 조정",
|
||||||
|
"transcript.reprocess_placeholder": "다음으로 다시 전사…",
|
||||||
|
"transcript.reprocess_lang_aria": "재처리 언어",
|
||||||
|
"transcript.reprocess_keep_lang": "현재 언어 유지",
|
||||||
|
"transcript.reprocess_go": "실행",
|
||||||
|
"transcript.retranscribing": "다시 전사하는 중…",
|
||||||
|
"transcript.has_note": "노트 있음",
|
||||||
|
"transcript.note_placeholder": "이 순간에 대한 노트 추가…",
|
||||||
|
"transcript.note_aria": "이 전사 줄에 대한 노트",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Word 문서",
|
||||||
|
"transcript.filter_obsidian": "Obsidian 노트",
|
||||||
|
|
||||||
|
"notes.heading": "노트",
|
||||||
|
"notes.show": "노트 표시",
|
||||||
|
"notes.hide": "노트 숨기기",
|
||||||
|
"notes.placeholder": "노트…",
|
||||||
|
"notes.live_placeholder": "말하면서 노트를 입력하세요…",
|
||||||
|
"notes.toolbar_aria": "노트 서식",
|
||||||
|
"notes.bold": "굵게",
|
||||||
|
"notes.italic": "기울임꼴",
|
||||||
|
"notes.h1": "제목 1",
|
||||||
|
"notes.h2": "제목 2",
|
||||||
|
"notes.bullet": "글머리 기호 목록",
|
||||||
|
"notes.checkbox_title": "체크박스",
|
||||||
|
"notes.checkbox_aria": "체크박스 목록 항목",
|
||||||
|
"notes.edit_raw": "원본 markdown 편집",
|
||||||
|
"notes.render": "markdown 렌더링",
|
||||||
|
"notes.editor": "편집기",
|
||||||
|
"notes.preview": "미리 보기",
|
||||||
|
"notes.export_md_title": "노트를 .md로 내보내기",
|
||||||
|
"notes.export_pdf_title": "노트를 .pdf로 내보내기",
|
||||||
|
"notes.export_docx_title": "노트를 .docx로 내보내기",
|
||||||
|
"notes.export_bundle_title": "오디오 + 전사 + 노트를 폴더로 내보내기",
|
||||||
|
"notes.export_obsidian_title": "하나의 Obsidian 노트 내보내기(노트 + 요약 + 전사, 오디오 제외)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "녹음",
|
||||||
|
"summary.loading_recording": "녹음을 불러오는 중…",
|
||||||
|
"summary.sync_heading": "동기화",
|
||||||
|
"summary.uploading": "업로드 중…",
|
||||||
|
"summary.upload_now": "지금 업로드",
|
||||||
|
"summary.no_uploads": "이 회의에 대한 업로드가 아직 없습니다.",
|
||||||
|
"summary.retry": "재시도",
|
||||||
|
"summary.tags_heading": "태그",
|
||||||
|
"summary.tags_select": "태그를 지정하려면 회의를 선택하세요.",
|
||||||
|
"summary.tag_add_placeholder": "태그 추가…",
|
||||||
|
"summary.tag_first_placeholder": "프로젝트, 고객, 주제…",
|
||||||
|
"summary.generating": "생성 중…",
|
||||||
|
"summary.generate_tags": "태그 생성",
|
||||||
|
"summary.saving": "저장 중…",
|
||||||
|
"summary.save_tags": "태그 저장",
|
||||||
|
"summary.summary_heading": "요약",
|
||||||
|
"summary.ai_provider": "AI 공급자",
|
||||||
|
"summary.provider_off": "끔",
|
||||||
|
"summary.provider_custom": "사용자 지정",
|
||||||
|
"summary.local": "로컬",
|
||||||
|
"summary.leaves_device": "이 장치를 벗어남",
|
||||||
|
"summary.this_hosted_provider": "이 호스팅 공급자",
|
||||||
|
"summary.select_generate": "요약을 생성하려면 회의를 선택하세요.",
|
||||||
|
"summary.decisions_heading": "결정 사항",
|
||||||
|
"summary.regenerate": "다시 생성",
|
||||||
|
"summary.generated_hint": "회의 후 로컬에서 생성됨(로컬 LLM 공급자 필요).",
|
||||||
|
"summary.no_provider": "구성된 AI 공급자가 없습니다 — 설정에서 하나 활성화하세요.",
|
||||||
|
"summary.generate_summary": "요약 생성",
|
||||||
|
"summary.briefs_heading": "기능 브리프",
|
||||||
|
"summary.briefs_select": "기능 브리프를 만들거나 보려면 회의를 선택하세요.",
|
||||||
|
"summary.briefs_desc": "이 회의를 에이전트가 사용할 수 있는 사양으로 정제합니다(로컬 LLM 공급자 필요) — 코딩 에이전트에 전달하거나, MCP 서버가 켜지면 이를 통해 제공하세요.",
|
||||||
|
"summary.brief_repo_placeholder": "대상 저장소(선택 사항), 예: acme/reporting-web",
|
||||||
|
"summary.distilling": "정제 중…",
|
||||||
|
"summary.create_brief": "기능 브리프 만들기",
|
||||||
|
"summary.brief_mcp_title": "MCP 서버가 켜져 있을 때 사용 가능",
|
||||||
|
"summary.copied": "복사됨",
|
||||||
|
"summary.copy_md": "Markdown으로 복사",
|
||||||
|
"summary.copy_json": "JSON으로 복사",
|
||||||
|
"summary.brief_problem": "문제",
|
||||||
|
"summary.brief_outcome": "원하는 결과",
|
||||||
|
"summary.brief_criteria": "수락 기준",
|
||||||
|
"summary.brief_none": "캡처된 내용 없음.",
|
||||||
|
"summary.brief_context": "컨텍스트",
|
||||||
|
"summary.action_items_heading": "작업 항목",
|
||||||
|
"summary.ai_select": "작업 항목을 보려면 회의를 선택하세요.",
|
||||||
|
"summary.ai_empty": "아직 없습니다 — 아래에서 하나 추가하거나 요약을 생성하여 자동으로 추출하세요.",
|
||||||
|
"summary.confirmed": "확인됨",
|
||||||
|
"summary.ai_text_placeholder": "작업 항목…",
|
||||||
|
"summary.ai_text_aria": "작업 항목 텍스트",
|
||||||
|
"summary.owner": "담당자",
|
||||||
|
"summary.due_date": "기한",
|
||||||
|
"summary.reminder_title": "이 기한에 대한 로컬 알림 예약",
|
||||||
|
"summary.ai_delete_title": "이 작업 항목 삭제",
|
||||||
|
"summary.ai_delete_aria": "작업 항목 삭제",
|
||||||
|
"summary.add_action_item": "작업 항목 추가",
|
||||||
|
"summary.save_action_items": "작업 항목 저장",
|
||||||
|
"summary.calendar_heading": "캘린더 이벤트",
|
||||||
|
"summary.cal_select": "회의를 캘린더 이벤트에 연결하려면 회의를 선택하세요.",
|
||||||
|
"summary.event_search_placeholder": "이벤트 제목 검색…",
|
||||||
|
"summary.linked_event": "연결된 이벤트",
|
||||||
|
"summary.change_event": "이벤트 변경…",
|
||||||
|
"summary.link_event": "이벤트 연결…",
|
||||||
|
"summary.untitled_event": "(제목 없음)",
|
||||||
|
"summary.no_events_1": "아직 가져온 이벤트가 없습니다 — ",
|
||||||
|
"summary.no_events_2": "설정 → 캘린더에서 가져오세요.",
|
||||||
|
"summary.no_events_match": "이 검색/날짜와 일치하는 이벤트가 없습니다 — 하나를 지워 보세요.",
|
||||||
|
"summary.organizer_label": "주최자: {name}",
|
||||||
|
"summary.participants_heading": "참가자",
|
||||||
|
"summary.participants_hint": "연결된 캘린더 이벤트에서 채워집니다.",
|
||||||
|
"summary.speakers_heading": "화자",
|
||||||
|
"summary.speakers_select": "화자 이름을 지정하려면 회의를 선택하세요.",
|
||||||
|
"summary.no_speakers": "아직 감지된 화자가 없습니다.",
|
||||||
|
"summary.speaker_name_placeholder": "화자 이름",
|
||||||
|
"summary.save": "저장",
|
||||||
|
"summary.cancel": "취소",
|
||||||
|
"summary.name_speaker": "이 화자 이름 지정…",
|
||||||
|
"summary.add_new_name": "+ 새 이름 추가…",
|
||||||
|
|
||||||
|
"settings.recording.title": "녹음",
|
||||||
|
"settings.recording.record_default": "기본적으로 회의 녹음",
|
||||||
|
"settings.recording.save_audio_as": "오디오를 다음으로 저장",
|
||||||
|
"settings.recording.default_hint": "기본적으로 꺼져 있습니다. 꺼져 있으면 오디오는 전사를 생성하는 데만 사용되며 회의가 완료되면 삭제됩니다. 회의별로 녹음을 전환할 수도 있습니다.",
|
||||||
|
"settings.recording.consent_label": "동의: {status}.",
|
||||||
|
"settings.recording.consent_ack": "확인됨",
|
||||||
|
"settings.recording.consent_not": "아직 확인되지 않음",
|
||||||
|
"settings.recording.auto_label": "캘린더 이벤트가 시작될 때 자동으로 녹음 시작",
|
||||||
|
"settings.recording.auto_hint": "WhispAssist가 열려 있는 동안에만 작동합니다. 가져온 캘린더 이벤트의 시작 시간이 되면 녹음이 자동으로 시작됩니다(위의 기본 보존 설정 사용). 백그라운드에서 실행되는 것은 없습니다 — 타이머는 앱이 실행 중일 때만 설정됩니다. 설정 → 캘린더에서 이벤트를 가져오세요."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "Opptak",
|
||||||
|
"nav.hardware": "Maskinvare",
|
||||||
|
"nav.storage": "Lagring",
|
||||||
|
"nav.calendar": "Kalender",
|
||||||
|
"nav.sync": "Synkronisering",
|
||||||
|
"nav.ai": "KI",
|
||||||
|
"nav.mcp": "MCP-server",
|
||||||
|
"nav.privacy": "Personvern",
|
||||||
|
"nav.about": "Om",
|
||||||
|
"nav.language": "Språk",
|
||||||
|
|
||||||
|
"settings.language.title": "Språk",
|
||||||
|
"settings.language.display": "Visningsspråk",
|
||||||
|
"settings.language.display_hint": "Språket for appens grensesnitt. Å legge til et språk er like enkelt som å slippe inn én enkelt oversettelsesfil.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "Transkripsjonsspråk",
|
||||||
|
"settings.transcription.label": "Språk",
|
||||||
|
"settings.transcription.auto": "Oppdag automatisk",
|
||||||
|
"settings.transcription.applies_hint": "Gjelder neste opptak. Språket som faktisk ble brukt vises deretter på hvert møte.",
|
||||||
|
"settings.transcription.english_only": "Kun engelsk",
|
||||||
|
"settings.transcription.model_english_only": "«{model}» er kun på engelsk.",
|
||||||
|
"settings.transcription.no_model": "Ingen modell valgt.",
|
||||||
|
"settings.transcription.switch_multilingual": "Bytt til en flerspråklig modell ovenfor for å velge et språk.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "Maskinvare",
|
||||||
|
"settings.hardware.active_backend": "Aktiv backend",
|
||||||
|
"settings.hardware.model_meta": "· modell {size}",
|
||||||
|
"settings.hardware.preferred_backend": "Foretrukket backend",
|
||||||
|
"settings.hardware.auto_backend": "Automatisk (best tilgjengelig)",
|
||||||
|
"settings.hardware.not_available": "ikke tilgjengelig",
|
||||||
|
"settings.hardware.fallback_hint": "Faller automatisk tilbake (NPU → NVIDIA → AMD → Intel → CPU) hvis den valgte backenden ikke lastes inn.",
|
||||||
|
"settings.hardware.audio_devices": "Lydenheter",
|
||||||
|
"settings.hardware.recording_device": "Opptaksenhet",
|
||||||
|
"settings.hardware.default_system_audio": "Standard systemlyd",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist tar opp det denne enheten spiller av (loopback) — den andre siden av samtalen. Velg en bestemt utgang hvis du ikke vil at den skal følge Windows' systemstandard.",
|
||||||
|
"settings.hardware.microphone": "Mikrofon",
|
||||||
|
"settings.hardware.mic_off": "Av — ikke fang min mikrofon",
|
||||||
|
"settings.hardware.default_mic": "Standardmikrofon",
|
||||||
|
"settings.hardware.mic_hint": "Legger til din egen stemme i den direkte transkripsjonen slik at begge sider av møtet fanges. Blir på enheten din — ingenting lastes opp. Velg «Av» for kun å transkribere systemlyden ovenfor.",
|
||||||
|
"settings.hardware.npu_detected": "NPU-akselerator oppdaget",
|
||||||
|
"settings.hardware.ready": "Klar",
|
||||||
|
"settings.hardware.downloading": "Laster ned…",
|
||||||
|
"settings.hardware.package_needed": "Pakke kreves",
|
||||||
|
"settings.hardware.npu_hint": "NPU-en din trenger en engangs akselerasjonspakke (Whisper ONNX-modell{extra}). Den lastes ned i bakgrunnen; du kan også starte den nå.",
|
||||||
|
"settings.hardware.plus_openvino": " + OpenVINO-kjøretid",
|
||||||
|
"settings.hardware.download_npu": "Last ned NPU-pakke",
|
||||||
|
"settings.hardware.dml_title": "GPU-akselerasjon (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "GPU-en din kan akselereres uten Vulkan via DirectML — en engangspakke (Whisper ONNX-modell{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + DirectML-kjøretid",
|
||||||
|
"settings.hardware.download_dml": "Last ned DirectML-pakke",
|
||||||
|
"settings.hardware.detection_unavailable": "Maskinvaregjenkjenning utilgjengelig.",
|
||||||
|
"settings.hardware.low_overhead": "Forhåndsinnstilling for lav belastning",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + minste modell, for batteri-/bakgrunnsbruk",
|
||||||
|
"settings.hardware.models": "Modeller",
|
||||||
|
"settings.hardware.downloading_model": "Laster ned {name}",
|
||||||
|
"settings.hardware.active_badge": "Aktiv",
|
||||||
|
"settings.hardware.installed": "Installert",
|
||||||
|
"settings.hardware.use": "Bruk",
|
||||||
|
"settings.hardware.remove": "Fjern",
|
||||||
|
"settings.hardware.cant_remove_active": "Kan ikke fjerne den aktive modellen",
|
||||||
|
"settings.hardware.download": "Last ned",
|
||||||
|
"settings.hardware.diarization": "Talerdiarisering",
|
||||||
|
"settings.hardware.diarization_hint": "Installer begge modellene for å få ferdige opptak delt opp per taler (Taler 1, Taler 2, …) i stedet for én sammenhengende transkripsjon. Kjører helt frakoblet, én gang per møte etter at det stopper. Uten dem tilskrives hver linje én enkelt taler.",
|
||||||
|
|
||||||
|
"settings.storage.title": "Lagring",
|
||||||
|
"settings.storage.location_label": "Plassering",
|
||||||
|
"settings.storage.location_hint": "Å endre lagringsplassering støttes ikke ennå — det er her møter, modeller og databasen for øyeblikket ligger.",
|
||||||
|
"settings.storage.retention_title": "Oppbevaring",
|
||||||
|
"settings.storage.retention_hint": "Sletter automatisk de eldste møtene over en grense. Sjekkes én gang ved oppstart; tomt betyr ingen grense. Berører aldri et møte som tas opp for øyeblikket.",
|
||||||
|
"settings.storage.max_age": "Maksalder (dager)",
|
||||||
|
"settings.storage.max_size": "Maksstørrelse (GB)",
|
||||||
|
"settings.storage.no_limit": "ingen grense",
|
||||||
|
"settings.storage.export_import_title": "Eksport og import",
|
||||||
|
"settings.storage.export_hint_1": "Flytt opptak mellom datamaskiner som bærbare pakkemapper (lyd, transkripsjon, notater, sammendrag og et",
|
||||||
|
"settings.storage.export_hint_2": "manifest). Eksporter til en hvilken som helst mappe — en synkronisert stasjon, en USB-pinne eller et synkroniseringsmåls lokale monteringspunkt — og importer det så på den andre maskinen. Importerte møter får en ny id, så re-import overskriver aldri noe.",
|
||||||
|
"settings.storage.export_dialog_title": "Eksporter alle møter til…",
|
||||||
|
"settings.storage.import_dialog_title": "Importer en pakke (eller en mappe med pakker)…",
|
||||||
|
"settings.storage.exporting": "Eksporterer…",
|
||||||
|
"settings.storage.export_all": "Eksporter alle møter…",
|
||||||
|
"settings.storage.importing": "Importerer…",
|
||||||
|
"settings.storage.import_all": "Importer møter…",
|
||||||
|
"settings.storage.exported_one": "Eksporterte {n} møte.",
|
||||||
|
"settings.storage.exported_many": "Eksporterte {n} møter.",
|
||||||
|
"settings.storage.imported_one": "Importerte {n} møte.",
|
||||||
|
"settings.storage.imported_many": "Importerte {n} møter.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "Kalender og Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "Importer hendelser og deltakere fra en lokal Outlook-",
|
||||||
|
"settings.calendar.intro_2": "sikkerhetskopi — skrivebeskyttet, ingenting skrives tilbake til filen. Ingenting forlater denne enheten.",
|
||||||
|
"settings.calendar.pst_file": ".pst-fil",
|
||||||
|
"settings.calendar.no_file": "Ingen fil valgt",
|
||||||
|
"settings.calendar.browse": "Bla gjennom…",
|
||||||
|
"settings.calendar.password": "Passord",
|
||||||
|
"settings.calendar.password_note": "sjelden nødvendig",
|
||||||
|
"settings.calendar.optional": "valgfritt",
|
||||||
|
"settings.calendar.import_range": "Importområde",
|
||||||
|
"settings.calendar.range_30": "Siste 30 dager",
|
||||||
|
"settings.calendar.range_90": "Siste 90 dager",
|
||||||
|
"settings.calendar.range_180": "Siste 6 måneder",
|
||||||
|
"settings.calendar.range_365": "Siste år",
|
||||||
|
"settings.calendar.range_all": "All tid",
|
||||||
|
"settings.calendar.range_hint": "En langlivet postkasse kan inneholde år med gjentakende/helligdagsoppføringer — å snevre inn området holder den importerte kalenderen til det som faktisk er relevant. Gjelder både denne importknappen og automatisk re-import ved oppstart.",
|
||||||
|
"settings.calendar.importing": "Importerer…",
|
||||||
|
"settings.calendar.import": "Importer",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} hendelser",
|
||||||
|
"settings.calendar.auto_reimport": "Importer denne filen automatisk ved oppstart",
|
||||||
|
"settings.calendar.auto_reimport_hint": "Kjører én gang ved oppstart, ikke på en tidtaker — re-import er trygt å gjenta (eksisterende hendelser matches og oppdateres, ikke dupliseres).",
|
||||||
|
"settings.calendar.import_failed": "Importen mislyktes: {error} — selve filen er urørt; sjekk banen og prøv igjen.",
|
||||||
|
"settings.calendar.cleanup_title": "Rydd opp",
|
||||||
|
"settings.calendar.cleanup_hint": "Hendelser som allerede er knyttet til et innspilt møte beholdes alltid, uansett hvilket alternativ som velges nedenfor.",
|
||||||
|
"settings.calendar.cleanup_30": "Eldre enn 30 dager",
|
||||||
|
"settings.calendar.cleanup_90": "Eldre enn 90 dager",
|
||||||
|
"settings.calendar.cleanup_180": "Eldre enn 6 måneder",
|
||||||
|
"settings.calendar.cleanup_365": "Eldre enn 1 år",
|
||||||
|
"settings.calendar.cleanup_all": "Slett alle",
|
||||||
|
"settings.calendar.cleaning": "Rydder…",
|
||||||
|
"settings.calendar.cleanup_btn": "Rydd opp i kalenderen",
|
||||||
|
"settings.calendar.cleanup_result": "Slettet {deleted}{extra}.",
|
||||||
|
"settings.calendar.cleanup_kept": ", beholdt {n} (knyttet til et møte)",
|
||||||
|
"settings.calendar.cleanup_failed": "Oppryddingen mislyktes: {error}",
|
||||||
|
"settings.calendar.imported_events": "Importerte hendelser",
|
||||||
|
"settings.calendar.no_events": "Ingen hendelser importert ennå.",
|
||||||
|
"settings.calendar.search_title": "Søk tittel",
|
||||||
|
"settings.calendar.search_placeholder": "Møtenavn…",
|
||||||
|
"settings.calendar.date_label": "Dato",
|
||||||
|
"settings.calendar.events_count": "{shown} av {total} hendelser",
|
||||||
|
"settings.calendar.untitled": "(uten tittel)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "Innstillinger",
|
||||||
|
"settings.close": "Lukk",
|
||||||
|
"settings.close_title": "Lukk (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "Forhåndsvisningsmodus — synkroniseringsbackenden er ikke implementert ennå (Fase 9). Endringer beholdes kun i grensesnittet.",
|
||||||
|
"settings.sync.title": "Synkronisering og opplasting",
|
||||||
|
"settings.sync.enable_label": "Aktiver opplasting av møteartefakter til konfigurerte mål",
|
||||||
|
"settings.sync.enable_hint": "Av som standard. Ingenting lastes opp med mindre dette er på og et mål er aktivert. Selvhostede mål holder data på din egen server; tredjepartsskyer er tydelig merket.",
|
||||||
|
"settings.sync.targets_title": "Mål",
|
||||||
|
"settings.sync.no_targets": "Ingen mål ennå. Legg til ett nedenfor.",
|
||||||
|
"settings.sync.third_party": "tredjepart",
|
||||||
|
"settings.sync.your_server": "din server",
|
||||||
|
"settings.sync.edit": "Rediger",
|
||||||
|
"settings.sync.remove": "Fjern",
|
||||||
|
"settings.sync.edit_target": "Rediger mål",
|
||||||
|
"settings.sync.add_target": "Legg til et mål",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV dekker Nextcloud, ownCloud, Cloudreve, Seafile og Synology. (Seafile: aktiver SeafDAV på serversiden.)",
|
||||||
|
"settings.sync.name": "Navn",
|
||||||
|
"settings.sync.name_placeholder": "Hjemme-Nextcloud",
|
||||||
|
"settings.sync.provider": "Leverandør",
|
||||||
|
"settings.sync.server_url": "Server-URL",
|
||||||
|
"settings.sync.server_url_note_1": "Bare server-URL-en din — WhispAssist legger til",
|
||||||
|
"settings.sync.server_url_note_2": "automatisk.",
|
||||||
|
"settings.sync.remote_folder": "Ekstern mappe",
|
||||||
|
"settings.sync.username": "Brukernavn",
|
||||||
|
"settings.sync.app_password": "App-passord",
|
||||||
|
"settings.sync.pw_keep": "la stå tomt for å beholde nåværende passord",
|
||||||
|
"settings.sync.pw_store": "lagret i operativsystemets legitimasjonslager",
|
||||||
|
"settings.sync.upload_legend": "Last opp",
|
||||||
|
"settings.sync.artifact_transcript": "transkripsjon",
|
||||||
|
"settings.sync.artifact_notes": "notater",
|
||||||
|
"settings.sync.artifact_summary": "sammendrag",
|
||||||
|
"settings.sync.artifact_recording": "opptak (.wav, hvis beholdt)",
|
||||||
|
"settings.sync.allow_plaintext": "Tillat klartekst-http for en LAN-adresse (ikke anbefalt)",
|
||||||
|
"settings.sync.encrypt_before": "Krypter før opplasting (destinasjonen lagrer kun kryptert tekst)",
|
||||||
|
"settings.sync.requires_vault": "— krever et opplåst hvelv",
|
||||||
|
"settings.sync.test_connection": "Test tilkobling",
|
||||||
|
"settings.sync.save_changes": "Lagre endringer",
|
||||||
|
"settings.sync.add_target_btn": "Legg til mål",
|
||||||
|
"settings.sync.cancel": "Avbryt",
|
||||||
|
"settings.sync.third_party_banner": "{kind} er en tredjepartssky — opplasting sender dataene dine bort fra enheten din til {kind}.",
|
||||||
|
"settings.sync.link_account": "Koble til {kind}-konto…",
|
||||||
|
"settings.sync.oauth_hint": "Åpner en OAuth-innlogging (loopback-omdirigering); tokenet lagres i operativsystemets legitimasjonslager.",
|
||||||
|
"settings.sync.footnote": "Legitimasjon skrives aldri til innstillingene eller databasen — kun til operativsystemets legitimasjonslager. TLS kreves for mål utenfor LAN.",
|
||||||
|
|
||||||
|
"settings.ai.title": "KI-sammendragsleverandør",
|
||||||
|
"settings.ai.intro_1": "Sammendrag kjører på en lokal LLM som standard. Pek dette mot Ollama på denne PC-en eller en annen maskin i LAN-et ditt (f.eks.",
|
||||||
|
"settings.ai.intro_2": "— begge teller som lokalt, så ingenting forlater nettverket ditt. Hostede leverandører (Anthropic) er valgfrie, av som standard, og sender transkripsjonen til en tredjepart så snart du aktiverer en.",
|
||||||
|
"settings.ai.provider": "Leverandør",
|
||||||
|
"settings.ai.off": "Av",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (lokal / LAN)",
|
||||||
|
"settings.ai.provider_custom": "Egendefinert (OpenAI-kompatibel)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — hostet, forlater denne enheten",
|
||||||
|
"settings.ai.endpoint": "Endepunkt",
|
||||||
|
"settings.ai.model": "Modell",
|
||||||
|
"settings.ai.api_key": "API-nøkkel",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (allerede angitt — la stå tomt for å beholde den)",
|
||||||
|
"settings.ai.show_api_key": "Vis API-nøkkel",
|
||||||
|
"settings.ai.hide_api_key": "Skjul API-nøkkel",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic er en hostet tredjepartstjeneste — dette møtets transkripsjon forlater enheten din når du genererer et sammendrag.",
|
||||||
|
"settings.ai.endpoint_banner": "Dette endepunktet er ikke på maskinen din eller LAN-et ditt — transkripsjonen din ville forlate nettverket ditt.",
|
||||||
|
"settings.ai.saving": "Lagrer…",
|
||||||
|
"settings.ai.save": "Lagre",
|
||||||
|
"settings.ai.test_connection": "Test tilkobling",
|
||||||
|
"settings.ai.this_hosted_provider": "denne hostede leverandøren",
|
||||||
|
"settings.ai.reachable": "Tilgjengelig",
|
||||||
|
"settings.ai.unreachable": "Utilgjengelig",
|
||||||
|
"settings.ai.model_count_one": "· {n} modell",
|
||||||
|
"settings.ai.model_count_many": "· {n} modeller",
|
||||||
|
"settings.ai.on_lan": "· på maskinen din / LAN",
|
||||||
|
"settings.ai.leaves_network": "· forlater nettverket ditt",
|
||||||
|
"settings.ai.advanced_title": "Avansert Ollama-konfigurasjon",
|
||||||
|
"settings.ai.changed": "{n} endret",
|
||||||
|
"settings.ai.reset_all": "Tilbakestill alt til standard",
|
||||||
|
"settings.ai.system_prompt": "Systemprompt",
|
||||||
|
"settings.ai.system_prompt_placeholder": "f.eks. Du er en kortfattet møtesammenfatter.",
|
||||||
|
"settings.ai.system_prompt_hint": "Legges til før WhispAssists påkrevde utdataformat, slik at instruksjonene dine ikke kan bryte tolkingen av sammendrag/oppgavepunkter.",
|
||||||
|
"settings.ai.think": "Tenk",
|
||||||
|
"settings.ai.think_help": "Resonneringsinnsats (kun resonneringsmodeller).",
|
||||||
|
"settings.ai.keep_alive": "Hold i live",
|
||||||
|
"settings.ai.keep_alive_help": "Hvor lenge modellen blir i RAM. f.eks. 5m, 1h, 0 (last ut), -1 (for alltid).",
|
||||||
|
"settings.ai.runtime_hardware": "Kjøretid og maskinvare",
|
||||||
|
"settings.ai.rarely_needed": "— sjelden nødvendig",
|
||||||
|
"settings.ai.save_advanced": "Lagre avansert",
|
||||||
|
"settings.ai.saved": "Lagret",
|
||||||
|
"settings.ai.turn_off": "Slå av",
|
||||||
|
"settings.ai.default_value": "Standard {value}.",
|
||||||
|
"settings.ai.reset_default": "Tilbakestill til standard",
|
||||||
|
"settings.ai.reset_field": "Tilbakestill {name}",
|
||||||
|
|
||||||
|
"settings.mcp.title": "MCP-server",
|
||||||
|
"settings.mcp.banner_1": "Dette lar din egen kodeagent (Claude Code, Codex, Copilot, OpenCode, …) hente møtekontekst på din lokale maskin. Når den er koblet til,",
|
||||||
|
"settings.mcp.banner_agent": "kan den agenten",
|
||||||
|
"settings.mcp.banner_2": "videresende det den leser til sin egen modellleverandørs sky — utenfor WhispAssists kontroll. WhispAssist selv sender aldri disse dataene noe sted; serveren lytter kun på denne enheten (",
|
||||||
|
"settings.mcp.banner_3": ") og hver lesing logges nedenfor.",
|
||||||
|
"settings.mcp.enable": "Aktiver MCP-serveren",
|
||||||
|
"settings.mcp.enable_hint": "Av som standard. Kun loopback, token-beskyttet — ingenting er tilgjengelig fra nettverket.",
|
||||||
|
"settings.mcp.transport": "Transport",
|
||||||
|
"settings.mcp.transport_http": "Strømmende HTTP",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (agenten starter en prosess)",
|
||||||
|
"settings.mcp.port": "Port",
|
||||||
|
"settings.mcp.transport_change_hint": "For å endre transport/port, slå av serveren først, og deretter på igjen.",
|
||||||
|
"settings.mcp.scope_title": "Omfang",
|
||||||
|
"settings.mcp.expose": "Eksponer",
|
||||||
|
"settings.mcp.scope_none": "Ingen — ingenting deles",
|
||||||
|
"settings.mcp.scope_selected": "Valgte — kun funksjonsbriefer du har merket som delte",
|
||||||
|
"settings.mcp.scope_all": "Alle — møter, transkripsjoner, oppgavepunkter og delte briefer",
|
||||||
|
"settings.mcp.expose_recordings": "Tillat også møter med et lagret opptak (av som standard — et innspilt møtes transkripsjon holdes tilbake selv i «Alle»-omfanget til dette er på)",
|
||||||
|
"settings.mcp.token_title": "Nytt autentiseringstoken — vises én gang, kopier det nå",
|
||||||
|
"settings.mcp.token_hint": "Dette vises ikke igjen. Det lagres i operativsystemets legitimasjonslager; hvis du mister det, slå av og på serveren igjen for å lage et nytt.",
|
||||||
|
"settings.mcp.copied": "Kopiert",
|
||||||
|
"settings.mcp.copy": "Kopier",
|
||||||
|
"settings.mcp.endpoint": "Endepunkt",
|
||||||
|
"settings.mcp.endpoint_hint": "Pek agentens MCP-klientkonfigurasjon mot {kind}, med tokenet ovenfor som bearer-legitimasjon.",
|
||||||
|
"settings.mcp.kind_command": "kommando",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "Tilgangslogg",
|
||||||
|
"settings.mcp.access_log_hint": "Hver verktøylesing en agent gjør, tillatt eller nektet (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "Ingen agent har lest noe ennå.",
|
||||||
|
"settings.mcp.log_meeting": "møte {id}",
|
||||||
|
"settings.mcp.refresh": "Oppdater",
|
||||||
|
|
||||||
|
"settings.privacy.title": "Personvern",
|
||||||
|
"settings.privacy.intro": "Hva WhispAssist faktisk har lov til å sende bort fra denne enheten akkurat nå. Med alt av (standard) forlater ingenting enheten i det hele tatt.",
|
||||||
|
"settings.privacy.llm_endpoint": "LLM-endepunkt",
|
||||||
|
"settings.privacy.off": "av",
|
||||||
|
"settings.privacy.local_only": "kun lokal",
|
||||||
|
"settings.privacy.leaves_device": "forlater denne enheten",
|
||||||
|
"settings.privacy.sync_label": "Synkronisering",
|
||||||
|
"settings.privacy.enabled": "aktivert",
|
||||||
|
"settings.privacy.mcp_label": "MCP-server",
|
||||||
|
"settings.privacy.mcp_on": "på · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "Innkommende kun på loopback — legger ingenting til utgangslisten ovenfor. En tilkoblet agent kan likevel videresende det den leser til sin egen modellleverandør; se MCP-server-fanen.",
|
||||||
|
"settings.privacy.egress_title": "Tillatelsesliste for utgående",
|
||||||
|
"settings.privacy.no_egress": "Ingen verter er tillatt — WA gjør ingen utgående overføring av innhold.",
|
||||||
|
"settings.privacy.sync_targets_title": "Synkroniseringsmål",
|
||||||
|
"settings.privacy.third_party": "tredjepart",
|
||||||
|
"settings.privacy.your_server": "din server",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "ingen TLS",
|
||||||
|
"settings.privacy.refresh": "Oppdater",
|
||||||
|
"settings.privacy.unavailable": "Personvern-selvsjekk utilgjengelig.",
|
||||||
|
"settings.privacy.vault_title": "Krypteringshvelv",
|
||||||
|
"settings.privacy.vault_intro": "Krypter notater, transkripsjoner og sammendrag i hvile med et passord. (Lydfiler krypteres ikke ennå.)",
|
||||||
|
"settings.privacy.vault_password": "Hvelvpassord",
|
||||||
|
"settings.privacy.enable_vault": "Aktiver hvelv",
|
||||||
|
"settings.privacy.vault_pw_hint": "Bruk minst 8 tegn. Hvis du glemmer det, kan kryptert innhold ikke gjenopprettes.",
|
||||||
|
"settings.privacy.vault_locked_1": "Hvelvet er ",
|
||||||
|
"settings.privacy.locked_word": "låst",
|
||||||
|
"settings.privacy.vault_locked_2": ". Lås opp for å lese krypterte møter.",
|
||||||
|
"settings.privacy.password": "Passord",
|
||||||
|
"settings.privacy.unlock": "Lås opp",
|
||||||
|
"settings.privacy.vault_unlocked_1": "Hvelvet er ",
|
||||||
|
"settings.privacy.unlocked_word": "opplåst",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". Nye notater, transkripsjoner og sammendrag krypteres i hvile.",
|
||||||
|
"settings.privacy.lock_now": "Lås nå",
|
||||||
|
"settings.privacy.change_password": "Endre passord",
|
||||||
|
"settings.privacy.current_password": "Nåværende passord",
|
||||||
|
"settings.privacy.new_password": "Nytt passord",
|
||||||
|
|
||||||
|
"settings.about.title": "Om",
|
||||||
|
"settings.about.tagline": "En helt lokal, åpen kildekode, Windows-innebygd møteassistent.",
|
||||||
|
"settings.about.build_commit": "Build-commit",
|
||||||
|
|
||||||
|
"consent.heading": "Før du tar opp",
|
||||||
|
"consent.body": "Å ta opp samtaler uten deltakernes samtykke kan være ulovlig i din region. Sjekk dine lokale opptakslover. Dette er en advarsel, ikke juridisk rådgivning.",
|
||||||
|
"consent.accept": "Jeg forstår — aktiver opptak",
|
||||||
|
"consent.cancel": "Avbryt",
|
||||||
|
|
||||||
|
"hosted.heading": "Før du bruker {provider}",
|
||||||
|
"hosted.body_1": "Å generere med",
|
||||||
|
"hosted.body_2": "sender dette møtets transkripsjon til deres servere — den forlater denne enheten og er underlagt deres personvernerklæring. WhispAssist har ingen kontroll over datahåndteringen når den først forlater enheten din. Dette er av som standard; du velger det nå.",
|
||||||
|
"hosted.accept": "Jeg forstår — fortsett",
|
||||||
|
"hosted.cancel": "Avbryt",
|
||||||
|
|
||||||
|
"theme.group": "Tema",
|
||||||
|
"theme.system": "Følg systemet",
|
||||||
|
"theme.light": "Lyst",
|
||||||
|
"theme.dark": "Mørkt",
|
||||||
|
|
||||||
|
"import.dialog_label": "Legg til et møte fra en fil eller URL",
|
||||||
|
"import.title": "Legg til et møte",
|
||||||
|
"import.close": "Lukk",
|
||||||
|
"import.close_title": "Lukk (Esc)",
|
||||||
|
"import.body": "Importer et eksisterende opptak — en lokal lyd-/videofil eller en lenke (YouTube, en strømmeside eller en direkte medie-URL). Det transkriberes og diariseres akkurat som et direkteopptak.",
|
||||||
|
"import.file_or_url": "Fil eller URL",
|
||||||
|
"import.source_placeholder": "Lim inn en URL, eller bla etter en fil…",
|
||||||
|
"import.choose_file": "Velg en lokal fil",
|
||||||
|
"import.browse": "Bla gjennom…",
|
||||||
|
"import.title_label": "Tittel",
|
||||||
|
"import.optional": "valgfritt",
|
||||||
|
"import.title_placeholder": "Standard er filnavnet",
|
||||||
|
"import.filter_av": "Lyd / video",
|
||||||
|
"import.requires_1": "Krever",
|
||||||
|
"import.requires_2": "installert og i din PATH (pluss",
|
||||||
|
"import.requires_3": "for URL-er). WhispAssist leverer dem ikke med.",
|
||||||
|
"import.importing": "Importerer… dette kan ta en stund",
|
||||||
|
"import.import": "Importer",
|
||||||
|
"import.cancel": "Avbryt",
|
||||||
|
|
||||||
|
"tagchip.filter": "Filtrer møter merket «{tag}»",
|
||||||
|
"tagchip.remove": "Fjern taggen {tag}",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "Inngangsnivå for system og mikrofon",
|
||||||
|
"levelmeter.system": "Inngangsnivå for system",
|
||||||
|
|
||||||
|
"app.tagline": "lokal · privat",
|
||||||
|
"app.note_template": "Notatmal",
|
||||||
|
"app.no_template": "Ingen mal",
|
||||||
|
"app.add_meeting": "Legg til møte",
|
||||||
|
"app.add_meeting_title": "Legg til et møte fra en fil eller URL",
|
||||||
|
"app.record": "Ta opp",
|
||||||
|
"app.record_title": "Start opptak (Ctrl+Shift+R)",
|
||||||
|
"app.stop": "Stopp",
|
||||||
|
"app.stop_title": "Stopp opptak (Ctrl+Shift+R)",
|
||||||
|
"app.cancel": "Avbryt",
|
||||||
|
"app.cancel_title": "Forkast dette opptaket og slett det",
|
||||||
|
"app.recording": "Tar opp…",
|
||||||
|
"app.backend_title": "Aktiv transkripsjonsbackend",
|
||||||
|
"app.retention_title": "Lagre lyd som .wav for dette møtet",
|
||||||
|
"app.saving": "lagrer",
|
||||||
|
"app.not_saved": "ikke lagret",
|
||||||
|
"app.reconnecting": "kobler til lydenhet på nytt…",
|
||||||
|
"app.settings": "Innstillinger",
|
||||||
|
"app.settings_title": "Innstillinger (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "Forkast dette opptaket? Lyden og transkripsjonen vil bli slettet.",
|
||||||
|
"app.announce_started": "Opptak startet",
|
||||||
|
"app.announce_paused": "Opptak satt på pause",
|
||||||
|
"app.announce_stopped": "Opptak stoppet",
|
||||||
|
"app.consent_dialog": "Samtykke til opptak",
|
||||||
|
"app.vault_dialog": "Lås opp krypteringshvelvet",
|
||||||
|
"app.vault_title": "Lås opp krypteringshvelvet",
|
||||||
|
"app.vault_desc": "Møtene dine er kryptert i hvile. Skriv inn passordet ditt for å lese dem.",
|
||||||
|
"app.vault_password": "Hvelvpassord",
|
||||||
|
"app.vault_incorrect": "Feil passord",
|
||||||
|
"app.unlock": "Lås opp",
|
||||||
|
"app.continue_locked": "Fortsett låst",
|
||||||
|
"app.show_meetings": "Vis møteliste",
|
||||||
|
"app.hide_meetings": "Skjul møteliste",
|
||||||
|
"app.show_summary": "Vis sammendragspanel",
|
||||||
|
"app.hide_summary": "Skjul sammendragspanel",
|
||||||
|
"app.resize_meetings": "Endre størrelse på møtelisten",
|
||||||
|
"app.resize_summary": "Endre størrelse på sammendragspanelet",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "Søk i møter…",
|
||||||
|
"meetings.search_aria": "Søk i møter",
|
||||||
|
"meetings.filter_tag_aria": "Filtrer etter tagg",
|
||||||
|
"meetings.all_tags": "Alle tagger",
|
||||||
|
"meetings.from_date_aria": "Fra dato",
|
||||||
|
"meetings.to_date_aria": "Til dato",
|
||||||
|
"meetings.bulk_format_aria": "Format for masseeksport",
|
||||||
|
"meetings.bulk_export": "Masseeksport",
|
||||||
|
"meetings.bulk_export_title": "Eksporter alle møter som samsvarer med tagg-/datofiltrene ovenfor",
|
||||||
|
"meetings.exporting": "Eksporterer…",
|
||||||
|
"meetings.exported_one": "Eksporterte {n} møte.",
|
||||||
|
"meetings.exported_many": "Eksporterte {n} møter.",
|
||||||
|
"meetings.searching": "Søker…",
|
||||||
|
"meetings.loading": "Laster inn møter…",
|
||||||
|
"meetings.no_matches": "Ingen treff.",
|
||||||
|
"meetings.empty": "Ingen møter ennå — klikk Ta opp ovenfor for å starte.",
|
||||||
|
"meetings.status.recording": "tar opp",
|
||||||
|
"meetings.status.transcribing": "transkriberer",
|
||||||
|
"meetings.status.recovering": "gjenoppretter",
|
||||||
|
"meetings.status.error": "feil",
|
||||||
|
"meetings.resume": "Gjenoppta transkripsjon",
|
||||||
|
"meetings.delete_aria": "Slett møte",
|
||||||
|
"meetings.delete_confirm": "Slette dette møtet? Dette fjerner opptaket, transkripsjonen og notatene.",
|
||||||
|
|
||||||
|
"transcript.heading": "Transkripsjon",
|
||||||
|
"transcript.title_aria": "Møtetittel",
|
||||||
|
"transcript.lang_title": "Transkripsjonsspråk",
|
||||||
|
"transcript.lang_auto": "oppdager…",
|
||||||
|
"transcript.show": "Vis transkripsjon",
|
||||||
|
"transcript.hide": "Skjul transkripsjon",
|
||||||
|
"transcript.empty": "Ingen transkripsjon for dette møtet.",
|
||||||
|
"transcript.will_appear": "Transkripsjonen vil vises her mens du tar opp.",
|
||||||
|
"transcript.play_from_here": "Spill av herfra",
|
||||||
|
"transcript.resize": "Endre størrelse på transkripsjon og notater",
|
||||||
|
"transcript.reprocess_placeholder": "Transkriber på nytt med…",
|
||||||
|
"transcript.reprocess_lang_aria": "Språk for ombehandling",
|
||||||
|
"transcript.reprocess_keep_lang": "Behold nåværende språk",
|
||||||
|
"transcript.reprocess_go": "Kjør",
|
||||||
|
"transcript.retranscribing": "Transkriberer på nytt…",
|
||||||
|
"transcript.has_note": "Har et notat",
|
||||||
|
"transcript.note_placeholder": "Legg til et notat for dette øyeblikket…",
|
||||||
|
"transcript.note_aria": "Notat for denne transkripsjonslinjen",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Word-dokument",
|
||||||
|
"transcript.filter_obsidian": "Obsidian-notat",
|
||||||
|
|
||||||
|
"notes.heading": "Notater",
|
||||||
|
"notes.show": "Vis notater",
|
||||||
|
"notes.hide": "Skjul notater",
|
||||||
|
"notes.placeholder": "Notater…",
|
||||||
|
"notes.live_placeholder": "Skriv notater mens du snakker…",
|
||||||
|
"notes.toolbar_aria": "Notatformatering",
|
||||||
|
"notes.bold": "Fet",
|
||||||
|
"notes.italic": "Kursiv",
|
||||||
|
"notes.h1": "Overskrift 1",
|
||||||
|
"notes.h2": "Overskrift 2",
|
||||||
|
"notes.bullet": "Punktliste",
|
||||||
|
"notes.checkbox_title": "Avkrysningsboks",
|
||||||
|
"notes.checkbox_aria": "Listeelement med avkrysningsboks",
|
||||||
|
"notes.edit_raw": "Rediger rå markdown",
|
||||||
|
"notes.render": "Gjengi markdown",
|
||||||
|
"notes.editor": "Redigering",
|
||||||
|
"notes.preview": "Forhåndsvisning",
|
||||||
|
"notes.export_md_title": "Eksporter notater som .md",
|
||||||
|
"notes.export_pdf_title": "Eksporter notater som .pdf",
|
||||||
|
"notes.export_docx_title": "Eksporter notater som .docx",
|
||||||
|
"notes.export_bundle_title": "Eksporter lyd + transkripsjon + notater til en mappe",
|
||||||
|
"notes.export_obsidian_title": "Eksporter ett Obsidian-notat (notater + sammendrag + transkripsjon, ingen lyd)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "Opptak",
|
||||||
|
"summary.loading_recording": "Laster inn opptak…",
|
||||||
|
"summary.sync_heading": "Synkronisering",
|
||||||
|
"summary.uploading": "Laster opp…",
|
||||||
|
"summary.upload_now": "Last opp nå",
|
||||||
|
"summary.no_uploads": "Ingen opplastinger for dette møtet ennå.",
|
||||||
|
"summary.retry": "prøv igjen",
|
||||||
|
"summary.tags_heading": "Tagger",
|
||||||
|
"summary.tags_select": "Velg et møte for å tagge det.",
|
||||||
|
"summary.tag_add_placeholder": "Legg til tagg…",
|
||||||
|
"summary.tag_first_placeholder": "prosjekt, kunde, tema…",
|
||||||
|
"summary.generating": "Genererer…",
|
||||||
|
"summary.generate_tags": "Generer tagger",
|
||||||
|
"summary.saving": "Lagrer…",
|
||||||
|
"summary.save_tags": "Lagre tagger",
|
||||||
|
"summary.summary_heading": "Sammendrag",
|
||||||
|
"summary.ai_provider": "KI-leverandør",
|
||||||
|
"summary.provider_off": "Av",
|
||||||
|
"summary.provider_custom": "Egendefinert",
|
||||||
|
"summary.local": "lokal",
|
||||||
|
"summary.leaves_device": "forlater denne enheten",
|
||||||
|
"summary.this_hosted_provider": "denne hostede leverandøren",
|
||||||
|
"summary.select_generate": "Velg et møte for å generere et sammendrag.",
|
||||||
|
"summary.decisions_heading": "Beslutninger",
|
||||||
|
"summary.regenerate": "Generer på nytt",
|
||||||
|
"summary.generated_hint": "Generert lokalt etter møtet (krever en lokal LLM-leverandør).",
|
||||||
|
"summary.no_provider": "Ingen KI-leverandør konfigurert — aktiver en i Innstillinger.",
|
||||||
|
"summary.generate_summary": "Generer sammendrag",
|
||||||
|
"summary.briefs_heading": "Funksjonsbriefer",
|
||||||
|
"summary.briefs_select": "Velg et møte for å opprette eller vise funksjonsbriefer.",
|
||||||
|
"summary.briefs_desc": "Destillerer dette møtet til en agentklar spesifikasjon (krever en lokal LLM-leverandør) — gi den til en kodeagent, eller server den over MCP-serveren når den er på.",
|
||||||
|
"summary.brief_repo_placeholder": "Målrepo (valgfritt), f.eks. acme/reporting-web",
|
||||||
|
"summary.distilling": "Destillerer…",
|
||||||
|
"summary.create_brief": "Opprett funksjonsbrief",
|
||||||
|
"summary.brief_mcp_title": "Tilgjengelig når MCP-serveren er på",
|
||||||
|
"summary.copied": "Kopiert",
|
||||||
|
"summary.copy_md": "Kopier som Markdown",
|
||||||
|
"summary.copy_json": "Kopier som JSON",
|
||||||
|
"summary.brief_problem": "Problem",
|
||||||
|
"summary.brief_outcome": "Ønsket resultat",
|
||||||
|
"summary.brief_criteria": "Akseptkriterier",
|
||||||
|
"summary.brief_none": "Ingenting fanget.",
|
||||||
|
"summary.brief_context": "Kontekst",
|
||||||
|
"summary.action_items_heading": "Oppgavepunkter",
|
||||||
|
"summary.ai_select": "Velg et møte for å se oppgavepunktene.",
|
||||||
|
"summary.ai_empty": "Ingen ennå — legg til ett nedenfor, eller generer et sammendrag for å hente dem automatisk.",
|
||||||
|
"summary.confirmed": "Bekreftet",
|
||||||
|
"summary.ai_text_placeholder": "Oppgavepunkt…",
|
||||||
|
"summary.ai_text_aria": "Tekst for oppgavepunkt",
|
||||||
|
"summary.owner": "Ansvarlig",
|
||||||
|
"summary.due_date": "Forfallsdato",
|
||||||
|
"summary.reminder_title": "Planlegg en lokal påminnelse for denne forfallsdatoen",
|
||||||
|
"summary.ai_delete_title": "Slett dette oppgavepunktet",
|
||||||
|
"summary.ai_delete_aria": "Slett oppgavepunkt",
|
||||||
|
"summary.add_action_item": "Legg til oppgavepunkt",
|
||||||
|
"summary.save_action_items": "Lagre oppgavepunkter",
|
||||||
|
"summary.calendar_heading": "Kalenderhendelse",
|
||||||
|
"summary.cal_select": "Velg et møte for å koble det til en kalenderhendelse.",
|
||||||
|
"summary.event_search_placeholder": "Søk hendelsestittel…",
|
||||||
|
"summary.linked_event": "Koblet hendelse",
|
||||||
|
"summary.change_event": "Endre hendelse…",
|
||||||
|
"summary.link_event": "Koble til en hendelse…",
|
||||||
|
"summary.untitled_event": "(uten tittel)",
|
||||||
|
"summary.no_events_1": "Ingen hendelser importert ennå — importer en",
|
||||||
|
"summary.no_events_2": "fra Innstillinger → Kalender.",
|
||||||
|
"summary.no_events_match": "Ingen hendelser samsvarer med dette søket/datoen — prøv å fjerne ett.",
|
||||||
|
"summary.organizer_label": "Arrangør: {name}",
|
||||||
|
"summary.participants_heading": "Deltakere",
|
||||||
|
"summary.participants_hint": "Fylles ut fra den koblede kalenderhendelsen.",
|
||||||
|
"summary.speakers_heading": "Talere",
|
||||||
|
"summary.speakers_select": "Velg et møte for å navngi talerne.",
|
||||||
|
"summary.no_speakers": "Ingen talere oppdaget ennå.",
|
||||||
|
"summary.speaker_name_placeholder": "Talerens navn",
|
||||||
|
"summary.save": "Lagre",
|
||||||
|
"summary.cancel": "Avbryt",
|
||||||
|
"summary.name_speaker": "Navngi denne taleren…",
|
||||||
|
"summary.add_new_name": "+ Legg til nytt navn…",
|
||||||
|
|
||||||
|
"settings.recording.title": "Opptak",
|
||||||
|
"settings.recording.record_default": "Ta opp møter som standard",
|
||||||
|
"settings.recording.save_audio_as": "lagre lyd som",
|
||||||
|
"settings.recording.default_hint": "Av som standard. Når det er av, brukes lyd kun til å produsere transkripsjonen og slettes når møtet fullføres. Du kan også veksle opptak per møte.",
|
||||||
|
"settings.recording.consent_label": "Samtykke: {status}.",
|
||||||
|
"settings.recording.consent_ack": "bekreftet",
|
||||||
|
"settings.recording.consent_not": "ennå ikke bekreftet",
|
||||||
|
"settings.recording.auto_label": "Start opptak automatisk når en kalenderhendelse begynner",
|
||||||
|
"settings.recording.auto_hint": "Kun mens WhispAssist er åpen. Når starttiden for en importert kalenderhendelse inntreffer, begynner et opptak automatisk (med din standardinnstilling for oppbevaring ovenfor). Ingenting kjører i bakgrunnen — tidtakeren er kun aktivert mens appen kjører. Importer hendelser under Innstillinger → Kalender."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "Gravação",
|
||||||
|
"nav.hardware": "Hardware",
|
||||||
|
"nav.storage": "Armazenamento",
|
||||||
|
"nav.calendar": "Calendário",
|
||||||
|
"nav.sync": "Sincronização",
|
||||||
|
"nav.ai": "IA",
|
||||||
|
"nav.mcp": "Servidor MCP",
|
||||||
|
"nav.privacy": "Privacidade",
|
||||||
|
"nav.about": "Sobre",
|
||||||
|
"nav.language": "Idioma",
|
||||||
|
|
||||||
|
"settings.language.title": "Idioma",
|
||||||
|
"settings.language.display": "Idioma da interface",
|
||||||
|
"settings.language.display_hint": "O idioma da interface do aplicativo. Adicionar um idioma é tão simples quanto colocar um único arquivo de tradução.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "Idioma de transcrição",
|
||||||
|
"settings.transcription.label": "Idioma",
|
||||||
|
"settings.transcription.auto": "Detecção automática",
|
||||||
|
"settings.transcription.applies_hint": "Aplica-se à próxima gravação. O idioma efetivamente utilizado é mostrado depois em cada reunião.",
|
||||||
|
"settings.transcription.english_only": "Apenas inglês",
|
||||||
|
"settings.transcription.model_english_only": "\"{model}\" é apenas em inglês.",
|
||||||
|
"settings.transcription.no_model": "Nenhum modelo selecionado.",
|
||||||
|
"settings.transcription.switch_multilingual": "Mude para um modelo multilíngue acima para escolher um idioma.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "Hardware",
|
||||||
|
"settings.hardware.active_backend": "Backend ativo",
|
||||||
|
"settings.hardware.model_meta": "· modelo {size}",
|
||||||
|
"settings.hardware.preferred_backend": "Backend preferido",
|
||||||
|
"settings.hardware.auto_backend": "Automático (o melhor disponível)",
|
||||||
|
"settings.hardware.not_available": "não disponível",
|
||||||
|
"settings.hardware.fallback_hint": "Recorre automaticamente (NPU → NVIDIA → AMD → Intel → CPU) se o backend escolhido não carregar.",
|
||||||
|
"settings.hardware.audio_devices": "Dispositivos de áudio",
|
||||||
|
"settings.hardware.recording_device": "Dispositivo de gravação",
|
||||||
|
"settings.hardware.default_system_audio": "Áudio do sistema padrão",
|
||||||
|
"settings.hardware.recording_device_hint": "O WhispAssist grava o que este dispositivo reproduz (loopback) — o outro lado da chamada. Escolha uma saída específica se não quiser que ele siga o padrão do sistema do Windows.",
|
||||||
|
"settings.hardware.microphone": "Microfone",
|
||||||
|
"settings.hardware.mic_off": "Desligado — não capturar meu microfone",
|
||||||
|
"settings.hardware.default_mic": "Microfone padrão",
|
||||||
|
"settings.hardware.mic_hint": "Adiciona a sua própria voz à transcrição ao vivo para que os dois lados da reunião sejam capturados. Permanece no seu dispositivo — nada é enviado. Escolha \"Desligado\" para transcrever apenas o áudio do sistema acima.",
|
||||||
|
"settings.hardware.npu_detected": "Acelerador NPU detectado",
|
||||||
|
"settings.hardware.ready": "Pronto",
|
||||||
|
"settings.hardware.downloading": "Baixando…",
|
||||||
|
"settings.hardware.package_needed": "Pacote necessário",
|
||||||
|
"settings.hardware.npu_hint": "Sua NPU precisa de um pacote de aceleração único (modelo Whisper ONNX{extra}). Ele é baixado em segundo plano; você também pode iniciá-lo agora.",
|
||||||
|
"settings.hardware.plus_openvino": " + ambiente de execução OpenVINO",
|
||||||
|
"settings.hardware.download_npu": "Baixar pacote NPU",
|
||||||
|
"settings.hardware.dml_title": "Aceleração de GPU (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "Sua GPU pode ser acelerada sem Vulkan por meio do DirectML — um pacote único (modelo Whisper ONNX{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + ambiente de execução DirectML",
|
||||||
|
"settings.hardware.download_dml": "Baixar pacote DirectML",
|
||||||
|
"settings.hardware.detection_unavailable": "Detecção de hardware indisponível.",
|
||||||
|
"settings.hardware.low_overhead": "Predefinição de baixo consumo",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + menor modelo, para uso na bateria/em segundo plano",
|
||||||
|
"settings.hardware.models": "Modelos",
|
||||||
|
"settings.hardware.downloading_model": "Baixando {name}",
|
||||||
|
"settings.hardware.active_badge": "Ativo",
|
||||||
|
"settings.hardware.installed": "Instalado",
|
||||||
|
"settings.hardware.use": "Usar",
|
||||||
|
"settings.hardware.remove": "Remover",
|
||||||
|
"settings.hardware.cant_remove_active": "Não é possível remover o modelo ativo",
|
||||||
|
"settings.hardware.download": "Baixar",
|
||||||
|
"settings.hardware.diarization": "Diarização de locutores",
|
||||||
|
"settings.hardware.diarization_hint": "Instale os dois modelos para que as gravações concluídas sejam separadas por locutor (Locutor 1, Locutor 2, …) em vez de uma única transcrição contínua. Funciona totalmente offline, uma vez por reunião após terminar. Sem eles, cada linha é atribuída a um único locutor.",
|
||||||
|
|
||||||
|
"settings.storage.title": "Armazenamento",
|
||||||
|
"settings.storage.location_label": "Local",
|
||||||
|
"settings.storage.location_hint": "Alterar o local de armazenamento ainda não é suportado — é aqui que ficam atualmente as reuniões, os modelos e o banco de dados.",
|
||||||
|
"settings.storage.retention_title": "Retenção",
|
||||||
|
"settings.storage.retention_hint": "Exclui automaticamente as reuniões mais antigas acima de um limite. Verificado uma vez na inicialização; vazio significa sem limite. Nunca afeta uma reunião que esteja sendo gravada.",
|
||||||
|
"settings.storage.max_age": "Idade máxima (dias)",
|
||||||
|
"settings.storage.max_size": "Tamanho máximo (GB)",
|
||||||
|
"settings.storage.no_limit": "sem limite",
|
||||||
|
"settings.storage.export_import_title": "Exportar e importar",
|
||||||
|
"settings.storage.export_hint_1": "Mova gravações entre computadores como pastas de pacote portáteis (áudio, transcrição, notas, resumo e um",
|
||||||
|
"settings.storage.export_hint_2": "manifesto). Exporte para qualquer pasta — uma unidade sincronizada, um pendrive ou o ponto de montagem local de um destino de sincronização — e depois importe-a na outra máquina. As reuniões importadas recebem um novo id, então reimportar nunca sobrescreve nada.",
|
||||||
|
"settings.storage.export_dialog_title": "Exportar todas as reuniões para…",
|
||||||
|
"settings.storage.import_dialog_title": "Importar um pacote (ou uma pasta de pacotes)…",
|
||||||
|
"settings.storage.exporting": "Exportando…",
|
||||||
|
"settings.storage.export_all": "Exportar todas as reuniões…",
|
||||||
|
"settings.storage.importing": "Importando…",
|
||||||
|
"settings.storage.import_all": "Importar reuniões…",
|
||||||
|
"settings.storage.exported_one": "{n} reunião exportada.",
|
||||||
|
"settings.storage.exported_many": "{n} reuniões exportadas.",
|
||||||
|
"settings.storage.imported_one": "{n} reunião importada.",
|
||||||
|
"settings.storage.imported_many": "{n} reuniões importadas.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "Calendário e Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "Importe eventos e participantes de um backup local do Outlook",
|
||||||
|
"settings.calendar.intro_2": "— somente leitura, nada é reescrito no arquivo. Nada sai deste dispositivo.",
|
||||||
|
"settings.calendar.pst_file": "Arquivo .pst",
|
||||||
|
"settings.calendar.no_file": "Nenhum arquivo selecionado",
|
||||||
|
"settings.calendar.browse": "Procurar…",
|
||||||
|
"settings.calendar.password": "Senha",
|
||||||
|
"settings.calendar.password_note": "raramente necessária",
|
||||||
|
"settings.calendar.optional": "opcional",
|
||||||
|
"settings.calendar.import_range": "Intervalo de importação",
|
||||||
|
"settings.calendar.range_30": "Últimos 30 dias",
|
||||||
|
"settings.calendar.range_90": "Últimos 90 dias",
|
||||||
|
"settings.calendar.range_180": "Últimos 6 meses",
|
||||||
|
"settings.calendar.range_365": "Último ano",
|
||||||
|
"settings.calendar.range_all": "Todo o período",
|
||||||
|
"settings.calendar.range_hint": "Uma caixa de correio de longa duração pode conter anos de entradas recorrentes/de feriados — reduzir o intervalo mantém o calendário importado no que é realmente relevante. Aplica-se tanto a este botão de importar quanto à reimportação automática na inicialização.",
|
||||||
|
"settings.calendar.importing": "Importando…",
|
||||||
|
"settings.calendar.import": "Importar",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} eventos",
|
||||||
|
"settings.calendar.auto_reimport": "Reimportar este arquivo automaticamente na inicialização",
|
||||||
|
"settings.calendar.auto_reimport_hint": "Executa uma vez na inicialização, não em um temporizador — a reimportação pode ser repetida com segurança (os eventos existentes são correspondidos e atualizados, não duplicados).",
|
||||||
|
"settings.calendar.import_failed": "Falha na importação: {error} — o próprio arquivo não é tocado; verifique o caminho e tente novamente.",
|
||||||
|
"settings.calendar.cleanup_title": "Limpar",
|
||||||
|
"settings.calendar.cleanup_hint": "Os eventos já vinculados a uma reunião gravada são sempre mantidos, independentemente da opção escolhida abaixo.",
|
||||||
|
"settings.calendar.cleanup_30": "Mais antigos que 30 dias",
|
||||||
|
"settings.calendar.cleanup_90": "Mais antigos que 90 dias",
|
||||||
|
"settings.calendar.cleanup_180": "Mais antigos que 6 meses",
|
||||||
|
"settings.calendar.cleanup_365": "Mais antigos que 1 ano",
|
||||||
|
"settings.calendar.cleanup_all": "Excluir tudo",
|
||||||
|
"settings.calendar.cleaning": "Limpando…",
|
||||||
|
"settings.calendar.cleanup_btn": "Limpar calendário",
|
||||||
|
"settings.calendar.cleanup_result": "{deleted}{extra} excluídos.",
|
||||||
|
"settings.calendar.cleanup_kept": ", {n} mantidos (vinculados a uma reunião)",
|
||||||
|
"settings.calendar.cleanup_failed": "Falha na limpeza: {error}",
|
||||||
|
"settings.calendar.imported_events": "Eventos importados",
|
||||||
|
"settings.calendar.no_events": "Ainda não há eventos importados.",
|
||||||
|
"settings.calendar.search_title": "Pesquisar título",
|
||||||
|
"settings.calendar.search_placeholder": "Nome da reunião…",
|
||||||
|
"settings.calendar.date_label": "Data",
|
||||||
|
"settings.calendar.events_count": "{shown} de {total} eventos",
|
||||||
|
"settings.calendar.untitled": "(sem título)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "Configurações",
|
||||||
|
"settings.close": "Fechar",
|
||||||
|
"settings.close_title": "Fechar (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "Modo de visualização — o backend de sincronização ainda não está implementado (Fase 9). As alterações são mantidas apenas na interface.",
|
||||||
|
"settings.sync.title": "Sincronização e upload",
|
||||||
|
"settings.sync.enable_label": "Ativar o upload de artefatos de reuniões para os destinos configurados",
|
||||||
|
"settings.sync.enable_hint": "Desativado por padrão. Nada é enviado a menos que isto esteja ativado e um destino esteja ativado. Os destinos auto-hospedados mantêm os dados no seu próprio servidor; as nuvens de terceiros são claramente identificadas.",
|
||||||
|
"settings.sync.targets_title": "Destinos",
|
||||||
|
"settings.sync.no_targets": "Ainda não há destinos. Adicione um abaixo.",
|
||||||
|
"settings.sync.third_party": "terceiros",
|
||||||
|
"settings.sync.your_server": "seu servidor",
|
||||||
|
"settings.sync.edit": "Editar",
|
||||||
|
"settings.sync.remove": "Remover",
|
||||||
|
"settings.sync.edit_target": "Editar destino",
|
||||||
|
"settings.sync.add_target": "Adicionar um destino",
|
||||||
|
"settings.sync.webdav_hint": "O WebDAV cobre Nextcloud, ownCloud, Cloudreve, Seafile e Synology. (Seafile: ative o SeafDAV no servidor.)",
|
||||||
|
"settings.sync.name": "Nome",
|
||||||
|
"settings.sync.name_placeholder": "Nextcloud de casa",
|
||||||
|
"settings.sync.provider": "Provedor",
|
||||||
|
"settings.sync.server_url": "URL do servidor",
|
||||||
|
"settings.sync.server_url_note_1": "Apenas a URL do seu servidor — o WhispAssist adiciona",
|
||||||
|
"settings.sync.server_url_note_2": "automaticamente.",
|
||||||
|
"settings.sync.remote_folder": "Pasta remota",
|
||||||
|
"settings.sync.username": "Nome de usuário",
|
||||||
|
"settings.sync.app_password": "Senha do aplicativo",
|
||||||
|
"settings.sync.pw_keep": "deixe em branco para manter a senha atual",
|
||||||
|
"settings.sync.pw_store": "armazenada no gerenciador de credenciais do sistema operacional",
|
||||||
|
"settings.sync.upload_legend": "Enviar",
|
||||||
|
"settings.sync.artifact_transcript": "transcrição",
|
||||||
|
"settings.sync.artifact_notes": "notas",
|
||||||
|
"settings.sync.artifact_summary": "resumo",
|
||||||
|
"settings.sync.artifact_recording": "gravação (.wav, se retida)",
|
||||||
|
"settings.sync.allow_plaintext": "Permitir http em texto simples para um endereço LAN (não recomendado)",
|
||||||
|
"settings.sync.encrypt_before": "Criptografar antes do envio (o destino armazena apenas texto cifrado)",
|
||||||
|
"settings.sync.requires_vault": "— requer um cofre desbloqueado",
|
||||||
|
"settings.sync.test_connection": "Testar conexão",
|
||||||
|
"settings.sync.save_changes": "Salvar alterações",
|
||||||
|
"settings.sync.add_target_btn": "Adicionar destino",
|
||||||
|
"settings.sync.cancel": "Cancelar",
|
||||||
|
"settings.sync.third_party_banner": "{kind} é uma nuvem de terceiros — o envio manda seus dados para fora do seu dispositivo para {kind}.",
|
||||||
|
"settings.sync.link_account": "Vincular conta {kind}…",
|
||||||
|
"settings.sync.oauth_hint": "Abre um login OAuth (redirecionamento loopback); o token é armazenado no gerenciador de credenciais do sistema operacional.",
|
||||||
|
"settings.sync.footnote": "As credenciais nunca são gravadas nas configurações nem no banco de dados — apenas no gerenciador de credenciais do sistema operacional. O TLS é obrigatório para destinos fora da LAN.",
|
||||||
|
|
||||||
|
"settings.ai.title": "Provedor de resumos de IA",
|
||||||
|
"settings.ai.intro_1": "Os resumos são executados em um LLM local por padrão. Aponte isto para o Ollama neste PC ou em outra máquina da sua LAN (ex.",
|
||||||
|
"settings.ai.intro_2": "— ambos contam como local, então nada sai da sua rede. Os provedores hospedados (Anthropic) são opcionais, desativados por padrão, e enviam a transcrição para um terceiro assim que você ativa um.",
|
||||||
|
"settings.ai.provider": "Provedor",
|
||||||
|
"settings.ai.off": "Desativado",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (local / LAN)",
|
||||||
|
"settings.ai.provider_custom": "Personalizado (compatível com OpenAI)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — hospedado, sai deste dispositivo",
|
||||||
|
"settings.ai.endpoint": "Endpoint",
|
||||||
|
"settings.ai.model": "Modelo",
|
||||||
|
"settings.ai.api_key": "Chave de API",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (já definida — deixe em branco para mantê-la)",
|
||||||
|
"settings.ai.show_api_key": "Mostrar chave de API",
|
||||||
|
"settings.ai.hide_api_key": "Ocultar chave de API",
|
||||||
|
"settings.ai.anthropic_banner": "A Anthropic é um serviço hospedado de terceiros — a transcrição desta reunião sai do seu dispositivo quando você gera um resumo.",
|
||||||
|
"settings.ai.endpoint_banner": "Este endpoint não está na sua máquina nem na sua LAN — sua transcrição sairia da sua rede.",
|
||||||
|
"settings.ai.saving": "Salvando…",
|
||||||
|
"settings.ai.save": "Salvar",
|
||||||
|
"settings.ai.test_connection": "Testar conexão",
|
||||||
|
"settings.ai.this_hosted_provider": "este provedor hospedado",
|
||||||
|
"settings.ai.reachable": "Acessível",
|
||||||
|
"settings.ai.unreachable": "Inacessível",
|
||||||
|
"settings.ai.model_count_one": "· {n} modelo",
|
||||||
|
"settings.ai.model_count_many": "· {n} modelos",
|
||||||
|
"settings.ai.on_lan": "· na sua máquina / LAN",
|
||||||
|
"settings.ai.leaves_network": "· sai da sua rede",
|
||||||
|
"settings.ai.advanced_title": "Configuração avançada do Ollama",
|
||||||
|
"settings.ai.changed": "{n} alterados",
|
||||||
|
"settings.ai.reset_all": "Redefinir tudo para os padrões",
|
||||||
|
"settings.ai.system_prompt": "Prompt do sistema",
|
||||||
|
"settings.ai.system_prompt_placeholder": "ex. Você é um resumidor de reuniões conciso.",
|
||||||
|
"settings.ai.system_prompt_hint": "Adicionado antes do formato de saída exigido pelo WhispAssist, para que suas instruções não possam quebrar a análise do resumo/tarefas.",
|
||||||
|
"settings.ai.think": "Raciocinar",
|
||||||
|
"settings.ai.think_help": "Esforço de raciocínio (apenas modelos de raciocínio).",
|
||||||
|
"settings.ai.keep_alive": "Manter ativo",
|
||||||
|
"settings.ai.keep_alive_help": "Por quanto tempo o modelo permanece na RAM. ex. 5m, 1h, 0 (descarregar), -1 (para sempre).",
|
||||||
|
"settings.ai.runtime_hardware": "Execução e hardware",
|
||||||
|
"settings.ai.rarely_needed": "— raramente necessário",
|
||||||
|
"settings.ai.save_advanced": "Salvar avançado",
|
||||||
|
"settings.ai.saved": "Salvo",
|
||||||
|
"settings.ai.turn_off": "Desativar",
|
||||||
|
"settings.ai.default_value": "Padrão {value}.",
|
||||||
|
"settings.ai.reset_default": "Redefinir para o padrão",
|
||||||
|
"settings.ai.reset_field": "Redefinir {name}",
|
||||||
|
|
||||||
|
"settings.mcp.title": "Servidor MCP",
|
||||||
|
"settings.mcp.banner_1": "Isto permite que o seu próprio agente de programação (Claude Code, Codex, Copilot, OpenCode, …) obtenha o contexto da reunião na sua máquina local. Uma vez conectado,",
|
||||||
|
"settings.mcp.banner_agent": "esse agente",
|
||||||
|
"settings.mcp.banner_2": "pode encaminhar o que lê para a nuvem do seu próprio provedor de modelo — fora do controle do WhispAssist. O próprio WhispAssist nunca envia esses dados para lugar nenhum; o servidor apenas escuta neste dispositivo (",
|
||||||
|
"settings.mcp.banner_3": ") e cada leitura é registrada abaixo.",
|
||||||
|
"settings.mcp.enable": "Ativar o servidor MCP",
|
||||||
|
"settings.mcp.enable_hint": "Desativado por padrão. Apenas loopback, protegido por token — nada é acessível a partir da rede.",
|
||||||
|
"settings.mcp.transport": "Transporte",
|
||||||
|
"settings.mcp.transport_http": "HTTP em streaming",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (o agente inicia um processo)",
|
||||||
|
"settings.mcp.port": "Porta",
|
||||||
|
"settings.mcp.transport_change_hint": "Para mudar o transporte/porta, desligue primeiro o servidor e ligue-o novamente.",
|
||||||
|
"settings.mcp.scope_title": "Escopo",
|
||||||
|
"settings.mcp.expose": "Expor",
|
||||||
|
"settings.mcp.scope_none": "Nenhum — nada é compartilhado",
|
||||||
|
"settings.mcp.scope_selected": "Selecionados — apenas os resumos de recursos que você marcou como compartilhados",
|
||||||
|
"settings.mcp.scope_all": "Todos — reuniões, transcrições, tarefas e resumos compartilhados",
|
||||||
|
"settings.mcp.expose_recordings": "Permitir também reuniões com uma gravação salva (desativado por padrão — a transcrição de uma reunião gravada é retida mesmo no escopo \"Todos\" até que isto seja ativado)",
|
||||||
|
"settings.mcp.token_title": "Novo token de autenticação — mostrado uma vez, copie-o agora",
|
||||||
|
"settings.mcp.token_hint": "Isto não será mostrado novamente. É armazenado no gerenciador de credenciais do sistema operacional; se você o perder, desligue e ligue o servidor novamente para gerar um novo.",
|
||||||
|
"settings.mcp.copied": "Copiado",
|
||||||
|
"settings.mcp.copy": "Copiar",
|
||||||
|
"settings.mcp.endpoint": "Endpoint",
|
||||||
|
"settings.mcp.endpoint_hint": "Aponte a configuração do cliente MCP do seu agente para {kind}, com o token acima como credencial bearer.",
|
||||||
|
"settings.mcp.kind_command": "comando",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "Registro de acessos",
|
||||||
|
"settings.mcp.access_log_hint": "Cada leitura de ferramenta que um agente faz, permitida ou negada (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "Nenhum agente leu nada ainda.",
|
||||||
|
"settings.mcp.log_meeting": "reunião {id}",
|
||||||
|
"settings.mcp.refresh": "Atualizar",
|
||||||
|
|
||||||
|
"settings.privacy.title": "Privacidade",
|
||||||
|
"settings.privacy.intro": "O que o WhispAssist está realmente autorizado a enviar para fora deste dispositivo neste momento. Com tudo desativado (o padrão), nada sai do dispositivo.",
|
||||||
|
"settings.privacy.llm_endpoint": "Endpoint do LLM",
|
||||||
|
"settings.privacy.off": "desativado",
|
||||||
|
"settings.privacy.local_only": "somente local",
|
||||||
|
"settings.privacy.leaves_device": "sai deste dispositivo",
|
||||||
|
"settings.privacy.sync_label": "Sincronização",
|
||||||
|
"settings.privacy.enabled": "ativada",
|
||||||
|
"settings.privacy.mcp_label": "Servidor MCP",
|
||||||
|
"settings.privacy.mcp_on": "ativado · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "Entrada apenas em loopback — não acrescenta nada à lista de saída acima. Um agente conectado pode ainda encaminhar o que lê para o seu próprio provedor de modelo; consulte a aba do servidor MCP.",
|
||||||
|
"settings.privacy.egress_title": "Lista de permissões de saída",
|
||||||
|
"settings.privacy.no_egress": "Nenhum host está na lista de permissões — o WA não faz nenhuma saída de conteúdo.",
|
||||||
|
"settings.privacy.sync_targets_title": "Destinos de sincronização",
|
||||||
|
"settings.privacy.third_party": "terceiros",
|
||||||
|
"settings.privacy.your_server": "seu servidor",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "sem TLS",
|
||||||
|
"settings.privacy.refresh": "Atualizar",
|
||||||
|
"settings.privacy.unavailable": "Autoverificação de privacidade indisponível.",
|
||||||
|
"settings.privacy.vault_title": "Cofre de criptografia",
|
||||||
|
"settings.privacy.vault_intro": "Criptografe notas, transcrições e resumos em repouso com uma senha. (Os arquivos de áudio ainda não são criptografados.)",
|
||||||
|
"settings.privacy.vault_password": "Senha do cofre",
|
||||||
|
"settings.privacy.enable_vault": "Ativar cofre",
|
||||||
|
"settings.privacy.vault_pw_hint": "Use pelo menos 8 caracteres. Se você a esquecer, o conteúdo criptografado não poderá ser recuperado.",
|
||||||
|
"settings.privacy.vault_locked_1": "O cofre está ",
|
||||||
|
"settings.privacy.locked_word": "bloqueado",
|
||||||
|
"settings.privacy.vault_locked_2": ". Desbloqueie para ler as reuniões criptografadas.",
|
||||||
|
"settings.privacy.password": "Senha",
|
||||||
|
"settings.privacy.unlock": "Desbloquear",
|
||||||
|
"settings.privacy.vault_unlocked_1": "O cofre está ",
|
||||||
|
"settings.privacy.unlocked_word": "desbloqueado",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". As novas notas, transcrições e resumos são criptografados em repouso.",
|
||||||
|
"settings.privacy.lock_now": "Bloquear agora",
|
||||||
|
"settings.privacy.change_password": "Alterar senha",
|
||||||
|
"settings.privacy.current_password": "Senha atual",
|
||||||
|
"settings.privacy.new_password": "Nova senha",
|
||||||
|
|
||||||
|
"settings.about.title": "Sobre",
|
||||||
|
"settings.about.tagline": "Um assistente de reuniões totalmente local, de código aberto e nativo do Windows.",
|
||||||
|
"settings.about.build_commit": "Commit de compilação",
|
||||||
|
|
||||||
|
"consent.heading": "Antes de gravar",
|
||||||
|
"consent.body": "Gravar conversas sem o consentimento dos participantes pode ser ilegal na sua região. Verifique as leis locais sobre gravação. Isto é um aviso, não aconselhamento jurídico.",
|
||||||
|
"consent.accept": "Entendo — ativar a gravação",
|
||||||
|
"consent.cancel": "Cancelar",
|
||||||
|
|
||||||
|
"hosted.heading": "Antes de usar {provider}",
|
||||||
|
"hosted.body_1": "Gerar com",
|
||||||
|
"hosted.body_2": "envia a transcrição desta reunião para os servidores deles — ela sai deste dispositivo e fica sujeita à política de privacidade deles. O WhispAssist não tem controle sobre o tratamento dos dados depois que eles saem do seu dispositivo. Isto está desativado por padrão; você está escolhendo agora.",
|
||||||
|
"hosted.accept": "Entendo — continuar",
|
||||||
|
"hosted.cancel": "Cancelar",
|
||||||
|
|
||||||
|
"theme.group": "Tema",
|
||||||
|
"theme.system": "Acompanhar o sistema",
|
||||||
|
"theme.light": "Claro",
|
||||||
|
"theme.dark": "Escuro",
|
||||||
|
|
||||||
|
"import.dialog_label": "Adicionar uma reunião a partir de um arquivo ou URL",
|
||||||
|
"import.title": "Adicionar uma reunião",
|
||||||
|
"import.close": "Fechar",
|
||||||
|
"import.close_title": "Fechar (Esc)",
|
||||||
|
"import.body": "Importe uma gravação existente — um arquivo de áudio/vídeo local ou um link (YouTube, uma página de streaming ou uma URL de mídia direta). Ela é transcrita e diarizada como uma gravação ao vivo.",
|
||||||
|
"import.file_or_url": "Arquivo ou URL",
|
||||||
|
"import.source_placeholder": "Cole uma URL ou procure um arquivo…",
|
||||||
|
"import.choose_file": "Escolher um arquivo local",
|
||||||
|
"import.browse": "Procurar…",
|
||||||
|
"import.title_label": "Título",
|
||||||
|
"import.optional": "opcional",
|
||||||
|
"import.title_placeholder": "Por padrão, o nome do arquivo",
|
||||||
|
"import.filter_av": "Áudio / vídeo",
|
||||||
|
"import.requires_1": "Requer",
|
||||||
|
"import.requires_2": "instalado e no seu PATH (mais",
|
||||||
|
"import.requires_3": "para URLs). O WhispAssist não os inclui.",
|
||||||
|
"import.importing": "Importando… isso pode demorar um pouco",
|
||||||
|
"import.import": "Importar",
|
||||||
|
"import.cancel": "Cancelar",
|
||||||
|
|
||||||
|
"tagchip.filter": "Filtrar reuniões marcadas com \"{tag}\"",
|
||||||
|
"tagchip.remove": "Remover marcador {tag}",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "Nível de entrada do sistema e do microfone",
|
||||||
|
"levelmeter.system": "Nível de entrada do sistema",
|
||||||
|
|
||||||
|
"app.tagline": "local · privado",
|
||||||
|
"app.note_template": "Modelo de notas",
|
||||||
|
"app.no_template": "Sem modelo",
|
||||||
|
"app.add_meeting": "Adicionar reunião",
|
||||||
|
"app.add_meeting_title": "Adicionar uma reunião a partir de um arquivo ou URL",
|
||||||
|
"app.record": "Gravar",
|
||||||
|
"app.record_title": "Iniciar gravação (Ctrl+Shift+R)",
|
||||||
|
"app.stop": "Parar",
|
||||||
|
"app.stop_title": "Parar gravação (Ctrl+Shift+R)",
|
||||||
|
"app.cancel": "Cancelar",
|
||||||
|
"app.cancel_title": "Descartar esta gravação e excluí-la",
|
||||||
|
"app.recording": "Gravando…",
|
||||||
|
"app.backend_title": "Backend de transcrição ativo",
|
||||||
|
"app.retention_title": "Salvar o áudio como .wav para esta reunião",
|
||||||
|
"app.saving": "salvando",
|
||||||
|
"app.not_saved": "não salvo",
|
||||||
|
"app.reconnecting": "reconectando o dispositivo de áudio…",
|
||||||
|
"app.settings": "Configurações",
|
||||||
|
"app.settings_title": "Configurações (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "Descartar esta gravação? Seu áudio e transcrição serão excluídos.",
|
||||||
|
"app.announce_started": "Gravação iniciada",
|
||||||
|
"app.announce_paused": "Gravação pausada",
|
||||||
|
"app.announce_stopped": "Gravação parada",
|
||||||
|
"app.consent_dialog": "Consentimento de gravação",
|
||||||
|
"app.vault_dialog": "Desbloquear o cofre de criptografia",
|
||||||
|
"app.vault_title": "Desbloquear o cofre de criptografia",
|
||||||
|
"app.vault_desc": "Suas reuniões estão criptografadas em repouso. Digite sua senha para lê-las.",
|
||||||
|
"app.vault_password": "Senha do cofre",
|
||||||
|
"app.vault_incorrect": "Senha incorreta",
|
||||||
|
"app.unlock": "Desbloquear",
|
||||||
|
"app.continue_locked": "Continuar bloqueado",
|
||||||
|
"app.show_meetings": "Mostrar lista de reuniões",
|
||||||
|
"app.hide_meetings": "Ocultar lista de reuniões",
|
||||||
|
"app.show_summary": "Mostrar painel de resumo",
|
||||||
|
"app.hide_summary": "Ocultar painel de resumo",
|
||||||
|
"app.resize_meetings": "Redimensionar lista de reuniões",
|
||||||
|
"app.resize_summary": "Redimensionar painel de resumo",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "Pesquisar reuniões…",
|
||||||
|
"meetings.search_aria": "Pesquisar reuniões",
|
||||||
|
"meetings.filter_tag_aria": "Filtrar por marcador",
|
||||||
|
"meetings.all_tags": "Todos os marcadores",
|
||||||
|
"meetings.from_date_aria": "Da data",
|
||||||
|
"meetings.to_date_aria": "Até a data",
|
||||||
|
"meetings.bulk_format_aria": "Formato de exportação em massa",
|
||||||
|
"meetings.bulk_export": "Exportação em massa",
|
||||||
|
"meetings.bulk_export_title": "Exportar todas as reuniões que correspondam aos filtros de marcador/data acima",
|
||||||
|
"meetings.exporting": "Exportando…",
|
||||||
|
"meetings.exported_one": "{n} reunião exportada.",
|
||||||
|
"meetings.exported_many": "{n} reuniões exportadas.",
|
||||||
|
"meetings.searching": "Pesquisando…",
|
||||||
|
"meetings.loading": "Carregando reuniões…",
|
||||||
|
"meetings.no_matches": "Nenhuma correspondência.",
|
||||||
|
"meetings.empty": "Ainda não há reuniões — clique em Gravar acima para começar.",
|
||||||
|
"meetings.status.recording": "gravando",
|
||||||
|
"meetings.status.transcribing": "transcrevendo",
|
||||||
|
"meetings.status.recovering": "recuperando",
|
||||||
|
"meetings.status.error": "erro",
|
||||||
|
"meetings.resume": "Retomar transcrição",
|
||||||
|
"meetings.delete_aria": "Excluir reunião",
|
||||||
|
"meetings.delete_confirm": "Excluir esta reunião? Isso remove sua gravação, transcrição e notas.",
|
||||||
|
|
||||||
|
"transcript.heading": "Transcrição",
|
||||||
|
"transcript.title_aria": "Título da reunião",
|
||||||
|
"transcript.lang_title": "Idioma de transcrição",
|
||||||
|
"transcript.lang_auto": "detectando…",
|
||||||
|
"transcript.show": "Mostrar transcrição",
|
||||||
|
"transcript.hide": "Ocultar transcrição",
|
||||||
|
"transcript.empty": "Não há transcrição para esta reunião.",
|
||||||
|
"transcript.will_appear": "A transcrição aparecerá aqui à medida que você grava.",
|
||||||
|
"transcript.play_from_here": "Reproduzir a partir daqui",
|
||||||
|
"transcript.resize": "Redimensionar transcrição e notas",
|
||||||
|
"transcript.reprocess_placeholder": "Transcrever novamente com…",
|
||||||
|
"transcript.reprocess_lang_aria": "Idioma de reprocessamento",
|
||||||
|
"transcript.reprocess_keep_lang": "Manter o idioma atual",
|
||||||
|
"transcript.reprocess_go": "Ir",
|
||||||
|
"transcript.retranscribing": "Transcrevendo novamente…",
|
||||||
|
"transcript.has_note": "Tem uma nota",
|
||||||
|
"transcript.note_placeholder": "Adicionar uma nota para este momento…",
|
||||||
|
"transcript.note_aria": "Nota para esta linha da transcrição",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Documento do Word",
|
||||||
|
"transcript.filter_obsidian": "Nota do Obsidian",
|
||||||
|
|
||||||
|
"notes.heading": "Notas",
|
||||||
|
"notes.show": "Mostrar notas",
|
||||||
|
"notes.hide": "Ocultar notas",
|
||||||
|
"notes.placeholder": "Notas…",
|
||||||
|
"notes.live_placeholder": "Digite notas enquanto fala…",
|
||||||
|
"notes.toolbar_aria": "Formatação de notas",
|
||||||
|
"notes.bold": "Negrito",
|
||||||
|
"notes.italic": "Itálico",
|
||||||
|
"notes.h1": "Título 1",
|
||||||
|
"notes.h2": "Título 2",
|
||||||
|
"notes.bullet": "Lista com marcadores",
|
||||||
|
"notes.checkbox_title": "Caixa de seleção",
|
||||||
|
"notes.checkbox_aria": "Item de lista com caixa de seleção",
|
||||||
|
"notes.edit_raw": "Editar o markdown bruto",
|
||||||
|
"notes.render": "Renderizar o markdown",
|
||||||
|
"notes.editor": "Editor",
|
||||||
|
"notes.preview": "Visualização",
|
||||||
|
"notes.export_md_title": "Exportar notas como .md",
|
||||||
|
"notes.export_pdf_title": "Exportar notas como .pdf",
|
||||||
|
"notes.export_docx_title": "Exportar notas como .docx",
|
||||||
|
"notes.export_bundle_title": "Exportar áudio + transcrição + notas para uma pasta",
|
||||||
|
"notes.export_obsidian_title": "Exportar uma nota do Obsidian (notas + resumo + transcrição, sem áudio)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "Gravação",
|
||||||
|
"summary.loading_recording": "Carregando gravação…",
|
||||||
|
"summary.sync_heading": "Sincronização",
|
||||||
|
"summary.uploading": "Enviando…",
|
||||||
|
"summary.upload_now": "Enviar agora",
|
||||||
|
"summary.no_uploads": "Ainda não há envios para esta reunião.",
|
||||||
|
"summary.retry": "tentar novamente",
|
||||||
|
"summary.tags_heading": "Marcadores",
|
||||||
|
"summary.tags_select": "Selecione uma reunião para marcá-la.",
|
||||||
|
"summary.tag_add_placeholder": "Adicionar marcador…",
|
||||||
|
"summary.tag_first_placeholder": "projeto, cliente, tema…",
|
||||||
|
"summary.generating": "Gerando…",
|
||||||
|
"summary.generate_tags": "Gerar marcadores",
|
||||||
|
"summary.saving": "Salvando…",
|
||||||
|
"summary.save_tags": "Salvar marcadores",
|
||||||
|
"summary.summary_heading": "Resumo",
|
||||||
|
"summary.ai_provider": "Provedor de IA",
|
||||||
|
"summary.provider_off": "Desativado",
|
||||||
|
"summary.provider_custom": "Personalizado",
|
||||||
|
"summary.local": "local",
|
||||||
|
"summary.leaves_device": "sai deste dispositivo",
|
||||||
|
"summary.this_hosted_provider": "este provedor hospedado",
|
||||||
|
"summary.select_generate": "Selecione uma reunião para gerar um resumo.",
|
||||||
|
"summary.decisions_heading": "Decisões",
|
||||||
|
"summary.regenerate": "Regenerar",
|
||||||
|
"summary.generated_hint": "Gerado localmente após a reunião (requer um provedor de LLM local).",
|
||||||
|
"summary.no_provider": "Nenhum provedor de IA configurado — ative um nas Configurações.",
|
||||||
|
"summary.generate_summary": "Gerar resumo",
|
||||||
|
"summary.briefs_heading": "Resumos de recursos",
|
||||||
|
"summary.briefs_select": "Selecione uma reunião para criar ou ver resumos de recursos.",
|
||||||
|
"summary.briefs_desc": "Destila esta reunião em uma especificação pronta para agentes (requer um provedor de LLM local) — entregue-a a um agente de programação, ou disponibilize-a pelo servidor MCP quando ele estiver ativado.",
|
||||||
|
"summary.brief_repo_placeholder": "Repositório de destino (opcional), ex. acme/reporting-web",
|
||||||
|
"summary.distilling": "Destilando…",
|
||||||
|
"summary.create_brief": "Criar resumo de recurso",
|
||||||
|
"summary.brief_mcp_title": "Disponível quando o servidor MCP está ativado",
|
||||||
|
"summary.copied": "Copiado",
|
||||||
|
"summary.copy_md": "Copiar como Markdown",
|
||||||
|
"summary.copy_json": "Copiar como JSON",
|
||||||
|
"summary.brief_problem": "Problema",
|
||||||
|
"summary.brief_outcome": "Resultado desejado",
|
||||||
|
"summary.brief_criteria": "Critérios de aceitação",
|
||||||
|
"summary.brief_none": "Nada capturado.",
|
||||||
|
"summary.brief_context": "Contexto",
|
||||||
|
"summary.action_items_heading": "Tarefas",
|
||||||
|
"summary.ai_select": "Selecione uma reunião para ver suas tarefas.",
|
||||||
|
"summary.ai_empty": "Ainda nenhuma — adicione uma abaixo, ou gere um resumo para extraí-las automaticamente.",
|
||||||
|
"summary.confirmed": "Confirmada",
|
||||||
|
"summary.ai_text_placeholder": "Tarefa…",
|
||||||
|
"summary.ai_text_aria": "Texto da tarefa",
|
||||||
|
"summary.owner": "Responsável",
|
||||||
|
"summary.due_date": "Data de vencimento",
|
||||||
|
"summary.reminder_title": "Agendar um lembrete local para esta data de vencimento",
|
||||||
|
"summary.ai_delete_title": "Excluir esta tarefa",
|
||||||
|
"summary.ai_delete_aria": "Excluir tarefa",
|
||||||
|
"summary.add_action_item": "Adicionar tarefa",
|
||||||
|
"summary.save_action_items": "Salvar tarefas",
|
||||||
|
"summary.calendar_heading": "Evento do calendário",
|
||||||
|
"summary.cal_select": "Selecione uma reunião para vinculá-la a um evento do calendário.",
|
||||||
|
"summary.event_search_placeholder": "Pesquisar título do evento…",
|
||||||
|
"summary.linked_event": "Evento vinculado",
|
||||||
|
"summary.change_event": "Alterar evento…",
|
||||||
|
"summary.link_event": "Vincular um evento…",
|
||||||
|
"summary.untitled_event": "(sem título)",
|
||||||
|
"summary.no_events_1": "Ainda não há eventos importados — importe um",
|
||||||
|
"summary.no_events_2": "em Configurações → Calendário.",
|
||||||
|
"summary.no_events_match": "Nenhum evento corresponde a esta pesquisa/data — tente limpar uma.",
|
||||||
|
"summary.organizer_label": "Organizador: {name}",
|
||||||
|
"summary.participants_heading": "Participantes",
|
||||||
|
"summary.participants_hint": "Preenchido a partir do evento do calendário vinculado.",
|
||||||
|
"summary.speakers_heading": "Locutores",
|
||||||
|
"summary.speakers_select": "Selecione uma reunião para nomear seus locutores.",
|
||||||
|
"summary.no_speakers": "Ainda não foram detectados locutores.",
|
||||||
|
"summary.speaker_name_placeholder": "Nome do locutor",
|
||||||
|
"summary.save": "Salvar",
|
||||||
|
"summary.cancel": "Cancelar",
|
||||||
|
"summary.name_speaker": "Nomear este locutor…",
|
||||||
|
"summary.add_new_name": "+ Adicionar novo nome…",
|
||||||
|
|
||||||
|
"settings.recording.title": "Gravação",
|
||||||
|
"settings.recording.record_default": "Gravar reuniões por padrão",
|
||||||
|
"settings.recording.save_audio_as": "salvar áudio como",
|
||||||
|
"settings.recording.default_hint": "Desativado por padrão. Quando desativado, o áudio é usado apenas para produzir a transcrição e é excluído quando a reunião é finalizada. Você também pode alternar a gravação por reunião.",
|
||||||
|
"settings.recording.consent_label": "Consentimento: {status}.",
|
||||||
|
"settings.recording.consent_ack": "confirmado",
|
||||||
|
"settings.recording.consent_not": "ainda não confirmado",
|
||||||
|
"settings.recording.auto_label": "Iniciar a gravação automaticamente quando um evento do calendário começar",
|
||||||
|
"settings.recording.auto_hint": "Apenas enquanto o WhispAssist estiver aberto. Quando chega a hora de início de um evento do calendário importado, uma gravação começa automaticamente (usando sua configuração de retenção padrão acima). Nada é executado em segundo plano — o temporizador só é armado enquanto o aplicativo está em execução. Importe eventos em Configurações → Calendário."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "Gravação",
|
||||||
|
"nav.hardware": "Hardware",
|
||||||
|
"nav.storage": "Armazenamento",
|
||||||
|
"nav.calendar": "Calendário",
|
||||||
|
"nav.sync": "Sincronização",
|
||||||
|
"nav.ai": "IA",
|
||||||
|
"nav.mcp": "Servidor MCP",
|
||||||
|
"nav.privacy": "Privacidade",
|
||||||
|
"nav.about": "Acerca de",
|
||||||
|
"nav.language": "Idioma",
|
||||||
|
|
||||||
|
"settings.language.title": "Idioma",
|
||||||
|
"settings.language.display": "Idioma da interface",
|
||||||
|
"settings.language.display_hint": "O idioma da interface da aplicação. Adicionar um idioma é tão simples como colocar um único ficheiro de tradução.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "Idioma de transcrição",
|
||||||
|
"settings.transcription.label": "Idioma",
|
||||||
|
"settings.transcription.auto": "Deteção automática",
|
||||||
|
"settings.transcription.applies_hint": "Aplica-se à próxima gravação. O idioma efetivamente utilizado é mostrado depois em cada reunião.",
|
||||||
|
"settings.transcription.english_only": "Apenas inglês",
|
||||||
|
"settings.transcription.model_english_only": "«{model}» é apenas em inglês.",
|
||||||
|
"settings.transcription.no_model": "Nenhum modelo selecionado.",
|
||||||
|
"settings.transcription.switch_multilingual": "Mude para um modelo multilingue acima para escolher um idioma.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "Hardware",
|
||||||
|
"settings.hardware.active_backend": "Backend ativo",
|
||||||
|
"settings.hardware.model_meta": "· modelo {size}",
|
||||||
|
"settings.hardware.preferred_backend": "Backend preferido",
|
||||||
|
"settings.hardware.auto_backend": "Automático (o melhor disponível)",
|
||||||
|
"settings.hardware.not_available": "não disponível",
|
||||||
|
"settings.hardware.fallback_hint": "Recorre automaticamente (NPU → NVIDIA → AMD → Intel → CPU) se o backend escolhido não carregar.",
|
||||||
|
"settings.hardware.audio_devices": "Dispositivos de áudio",
|
||||||
|
"settings.hardware.recording_device": "Dispositivo de gravação",
|
||||||
|
"settings.hardware.default_system_audio": "Áudio do sistema predefinido",
|
||||||
|
"settings.hardware.recording_device_hint": "O WhispAssist grava o que este dispositivo reproduz (loopback) — o outro lado da chamada. Escolha uma saída específica se não quiser que siga a predefinição do sistema do Windows.",
|
||||||
|
"settings.hardware.microphone": "Microfone",
|
||||||
|
"settings.hardware.mic_off": "Desativado — não capturar o meu microfone",
|
||||||
|
"settings.hardware.default_mic": "Microfone predefinido",
|
||||||
|
"settings.hardware.mic_hint": "Adiciona a sua própria voz à transcrição em direto para que ambos os lados da reunião sejam captados. Permanece no seu dispositivo — nada é carregado. Escolha «Desativado» para transcrever apenas o áudio do sistema acima.",
|
||||||
|
"settings.hardware.npu_detected": "Acelerador NPU detetado",
|
||||||
|
"settings.hardware.ready": "Pronto",
|
||||||
|
"settings.hardware.downloading": "A transferir…",
|
||||||
|
"settings.hardware.package_needed": "Pacote necessário",
|
||||||
|
"settings.hardware.npu_hint": "A sua NPU precisa de um pacote de aceleração único (modelo Whisper ONNX{extra}). É transferido em segundo plano; também pode iniciá-lo agora.",
|
||||||
|
"settings.hardware.plus_openvino": " + ambiente de execução OpenVINO",
|
||||||
|
"settings.hardware.download_npu": "Transferir pacote NPU",
|
||||||
|
"settings.hardware.dml_title": "Aceleração de GPU (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "A sua GPU pode ser acelerada sem Vulkan através do DirectML — um pacote único (modelo Whisper ONNX{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + ambiente de execução DirectML",
|
||||||
|
"settings.hardware.download_dml": "Transferir pacote DirectML",
|
||||||
|
"settings.hardware.detection_unavailable": "Deteção de hardware indisponível.",
|
||||||
|
"settings.hardware.low_overhead": "Predefinição de baixo consumo",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + modelo mais pequeno, para uso com bateria/em segundo plano",
|
||||||
|
"settings.hardware.models": "Modelos",
|
||||||
|
"settings.hardware.downloading_model": "A transferir {name}",
|
||||||
|
"settings.hardware.active_badge": "Ativo",
|
||||||
|
"settings.hardware.installed": "Instalado",
|
||||||
|
"settings.hardware.use": "Usar",
|
||||||
|
"settings.hardware.remove": "Remover",
|
||||||
|
"settings.hardware.cant_remove_active": "Não é possível remover o modelo ativo",
|
||||||
|
"settings.hardware.download": "Transferir",
|
||||||
|
"settings.hardware.diarization": "Diarização de oradores",
|
||||||
|
"settings.hardware.diarization_hint": "Instale ambos os modelos para que as gravações concluídas sejam separadas por orador (Orador 1, Orador 2, …) em vez de uma única transcrição contínua. Funciona totalmente offline, uma vez por reunião após terminar. Sem eles, cada linha é atribuída a um único orador.",
|
||||||
|
|
||||||
|
"settings.storage.title": "Armazenamento",
|
||||||
|
"settings.storage.location_label": "Localização",
|
||||||
|
"settings.storage.location_hint": "Alterar a localização de armazenamento ainda não é suportado — é aqui que residem atualmente as reuniões, os modelos e a base de dados.",
|
||||||
|
"settings.storage.retention_title": "Retenção",
|
||||||
|
"settings.storage.retention_hint": "Elimina automaticamente as reuniões mais antigas acima de um limite. Verificado uma vez no arranque; vazio significa sem limite. Nunca afeta uma reunião que esteja a ser gravada.",
|
||||||
|
"settings.storage.max_age": "Idade máxima (dias)",
|
||||||
|
"settings.storage.max_size": "Tamanho máximo (GB)",
|
||||||
|
"settings.storage.no_limit": "sem limite",
|
||||||
|
"settings.storage.export_import_title": "Exportar e importar",
|
||||||
|
"settings.storage.export_hint_1": "Mova gravações entre computadores como pastas de pacote portáteis (áudio, transcrição, notas, resumo e um",
|
||||||
|
"settings.storage.export_hint_2": "manifesto). Exporte para qualquer pasta — uma unidade sincronizada, uma pen USB ou o ponto de montagem local de um destino de sincronização — e depois importe-o na outra máquina. As reuniões importadas recebem um novo id, pelo que reimportar nunca substitui nada.",
|
||||||
|
"settings.storage.export_dialog_title": "Exportar todas as reuniões para…",
|
||||||
|
"settings.storage.import_dialog_title": "Importar um pacote (ou uma pasta de pacotes)…",
|
||||||
|
"settings.storage.exporting": "A exportar…",
|
||||||
|
"settings.storage.export_all": "Exportar todas as reuniões…",
|
||||||
|
"settings.storage.importing": "A importar…",
|
||||||
|
"settings.storage.import_all": "Importar reuniões…",
|
||||||
|
"settings.storage.exported_one": "{n} reunião exportada.",
|
||||||
|
"settings.storage.exported_many": "{n} reuniões exportadas.",
|
||||||
|
"settings.storage.imported_one": "{n} reunião importada.",
|
||||||
|
"settings.storage.imported_many": "{n} reuniões importadas.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "Calendário e Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "Importe eventos e participantes de uma cópia de segurança local do Outlook",
|
||||||
|
"settings.calendar.intro_2": "— apenas leitura, nada é reescrito no ficheiro. Nada sai deste dispositivo.",
|
||||||
|
"settings.calendar.pst_file": "Ficheiro .pst",
|
||||||
|
"settings.calendar.no_file": "Nenhum ficheiro selecionado",
|
||||||
|
"settings.calendar.browse": "Procurar…",
|
||||||
|
"settings.calendar.password": "Palavra-passe",
|
||||||
|
"settings.calendar.password_note": "raramente necessária",
|
||||||
|
"settings.calendar.optional": "opcional",
|
||||||
|
"settings.calendar.import_range": "Intervalo de importação",
|
||||||
|
"settings.calendar.range_30": "Últimos 30 dias",
|
||||||
|
"settings.calendar.range_90": "Últimos 90 dias",
|
||||||
|
"settings.calendar.range_180": "Últimos 6 meses",
|
||||||
|
"settings.calendar.range_365": "Último ano",
|
||||||
|
"settings.calendar.range_all": "Todo o período",
|
||||||
|
"settings.calendar.range_hint": "Uma caixa de correio de longa duração pode conter anos de entradas recorrentes/de feriados — reduzir o intervalo mantém o calendário importado no que é realmente relevante. Aplica-se tanto a este botão de importar como à reimportação automática no arranque.",
|
||||||
|
"settings.calendar.importing": "A importar…",
|
||||||
|
"settings.calendar.import": "Importar",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} eventos",
|
||||||
|
"settings.calendar.auto_reimport": "Reimportar este ficheiro automaticamente no arranque",
|
||||||
|
"settings.calendar.auto_reimport_hint": "Executa uma vez no arranque, não num temporizador — a reimportação pode ser repetida em segurança (os eventos existentes são correspondidos e atualizados, não duplicados).",
|
||||||
|
"settings.calendar.import_failed": "Falha na importação: {error} — o próprio ficheiro não é tocado; verifique o caminho e tente novamente.",
|
||||||
|
"settings.calendar.cleanup_title": "Limpar",
|
||||||
|
"settings.calendar.cleanup_hint": "Os eventos já associados a uma reunião gravada são sempre mantidos, independentemente da opção escolhida abaixo.",
|
||||||
|
"settings.calendar.cleanup_30": "Mais antigos que 30 dias",
|
||||||
|
"settings.calendar.cleanup_90": "Mais antigos que 90 dias",
|
||||||
|
"settings.calendar.cleanup_180": "Mais antigos que 6 meses",
|
||||||
|
"settings.calendar.cleanup_365": "Mais antigos que 1 ano",
|
||||||
|
"settings.calendar.cleanup_all": "Eliminar tudo",
|
||||||
|
"settings.calendar.cleaning": "A limpar…",
|
||||||
|
"settings.calendar.cleanup_btn": "Limpar calendário",
|
||||||
|
"settings.calendar.cleanup_result": "{deleted}{extra} eliminados.",
|
||||||
|
"settings.calendar.cleanup_kept": ", {n} mantidos (associados a uma reunião)",
|
||||||
|
"settings.calendar.cleanup_failed": "Falha na limpeza: {error}",
|
||||||
|
"settings.calendar.imported_events": "Eventos importados",
|
||||||
|
"settings.calendar.no_events": "Ainda não foram importados eventos.",
|
||||||
|
"settings.calendar.search_title": "Procurar título",
|
||||||
|
"settings.calendar.search_placeholder": "Nome da reunião…",
|
||||||
|
"settings.calendar.date_label": "Data",
|
||||||
|
"settings.calendar.events_count": "{shown} de {total} eventos",
|
||||||
|
"settings.calendar.untitled": "(sem título)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "Definições",
|
||||||
|
"settings.close": "Fechar",
|
||||||
|
"settings.close_title": "Fechar (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "Modo de pré-visualização — o backend de sincronização ainda não está implementado (Fase 9). As alterações são mantidas apenas na interface.",
|
||||||
|
"settings.sync.title": "Sincronização e carregamento",
|
||||||
|
"settings.sync.enable_label": "Ativar o carregamento de artefactos de reuniões para os destinos configurados",
|
||||||
|
"settings.sync.enable_hint": "Desativado por predefinição. Nada é carregado a menos que isto esteja ativado e um destino esteja ativado. Os destinos auto-alojados mantêm os dados no seu próprio servidor; as nuvens de terceiros estão claramente identificadas.",
|
||||||
|
"settings.sync.targets_title": "Destinos",
|
||||||
|
"settings.sync.no_targets": "Ainda não há destinos. Adicione um abaixo.",
|
||||||
|
"settings.sync.third_party": "terceiros",
|
||||||
|
"settings.sync.your_server": "o seu servidor",
|
||||||
|
"settings.sync.edit": "Editar",
|
||||||
|
"settings.sync.remove": "Remover",
|
||||||
|
"settings.sync.edit_target": "Editar destino",
|
||||||
|
"settings.sync.add_target": "Adicionar um destino",
|
||||||
|
"settings.sync.webdav_hint": "O WebDAV cobre Nextcloud, ownCloud, Cloudreve, Seafile e Synology. (Seafile: ative o SeafDAV no servidor.)",
|
||||||
|
"settings.sync.name": "Nome",
|
||||||
|
"settings.sync.name_placeholder": "Nextcloud de casa",
|
||||||
|
"settings.sync.provider": "Fornecedor",
|
||||||
|
"settings.sync.server_url": "URL do servidor",
|
||||||
|
"settings.sync.server_url_note_1": "Apenas o URL do seu servidor — o WhispAssist adiciona",
|
||||||
|
"settings.sync.server_url_note_2": "automaticamente.",
|
||||||
|
"settings.sync.remote_folder": "Pasta remota",
|
||||||
|
"settings.sync.username": "Nome de utilizador",
|
||||||
|
"settings.sync.app_password": "Palavra-passe da aplicação",
|
||||||
|
"settings.sync.pw_keep": "deixe em branco para manter a palavra-passe atual",
|
||||||
|
"settings.sync.pw_store": "armazenada no gestor de credenciais do sistema operativo",
|
||||||
|
"settings.sync.upload_legend": "Carregar",
|
||||||
|
"settings.sync.artifact_transcript": "transcrição",
|
||||||
|
"settings.sync.artifact_notes": "notas",
|
||||||
|
"settings.sync.artifact_summary": "resumo",
|
||||||
|
"settings.sync.artifact_recording": "gravação (.wav, se retida)",
|
||||||
|
"settings.sync.allow_plaintext": "Permitir http em texto simples para um endereço LAN (não recomendado)",
|
||||||
|
"settings.sync.encrypt_before": "Cifrar antes de carregar (o destino armazena apenas texto cifrado)",
|
||||||
|
"settings.sync.requires_vault": "— requer um cofre desbloqueado",
|
||||||
|
"settings.sync.test_connection": "Testar ligação",
|
||||||
|
"settings.sync.save_changes": "Guardar alterações",
|
||||||
|
"settings.sync.add_target_btn": "Adicionar destino",
|
||||||
|
"settings.sync.cancel": "Cancelar",
|
||||||
|
"settings.sync.third_party_banner": "{kind} é uma nuvem de terceiros — carregar envia os seus dados para fora do seu dispositivo para {kind}.",
|
||||||
|
"settings.sync.link_account": "Associar conta {kind}…",
|
||||||
|
"settings.sync.oauth_hint": "Abre um início de sessão OAuth (redirecionamento loopback); o token é armazenado no gestor de credenciais do sistema operativo.",
|
||||||
|
"settings.sync.footnote": "As credenciais nunca são escritas nas definições nem na base de dados — apenas no gestor de credenciais do sistema operativo. O TLS é obrigatório para destinos fora da LAN.",
|
||||||
|
|
||||||
|
"settings.ai.title": "Fornecedor de resumos de IA",
|
||||||
|
"settings.ai.intro_1": "Os resumos são executados num LLM local por predefinição. Aponte isto para o Ollama neste PC ou noutra máquina da sua LAN (por ex.",
|
||||||
|
"settings.ai.intro_2": "— ambos contam como local, por isso nada sai da sua rede. Os fornecedores alojados (Anthropic) são opcionais, desativados por predefinição, e enviam a transcrição para um terceiro assim que ativar um.",
|
||||||
|
"settings.ai.provider": "Fornecedor",
|
||||||
|
"settings.ai.off": "Desativado",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (local / LAN)",
|
||||||
|
"settings.ai.provider_custom": "Personalizado (compatível com OpenAI)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — alojado, sai deste dispositivo",
|
||||||
|
"settings.ai.endpoint": "Ponto de extremidade",
|
||||||
|
"settings.ai.model": "Modelo",
|
||||||
|
"settings.ai.api_key": "Chave API",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (já definida — deixe em branco para a manter)",
|
||||||
|
"settings.ai.show_api_key": "Mostrar chave API",
|
||||||
|
"settings.ai.hide_api_key": "Ocultar chave API",
|
||||||
|
"settings.ai.anthropic_banner": "A Anthropic é um serviço alojado de terceiros — a transcrição desta reunião sai do seu dispositivo quando gera um resumo.",
|
||||||
|
"settings.ai.endpoint_banner": "Este ponto de extremidade não está na sua máquina nem na sua LAN — a sua transcrição sairia da sua rede.",
|
||||||
|
"settings.ai.saving": "A guardar…",
|
||||||
|
"settings.ai.save": "Guardar",
|
||||||
|
"settings.ai.test_connection": "Testar ligação",
|
||||||
|
"settings.ai.this_hosted_provider": "este fornecedor alojado",
|
||||||
|
"settings.ai.reachable": "Acessível",
|
||||||
|
"settings.ai.unreachable": "Inacessível",
|
||||||
|
"settings.ai.model_count_one": "· {n} modelo",
|
||||||
|
"settings.ai.model_count_many": "· {n} modelos",
|
||||||
|
"settings.ai.on_lan": "· na sua máquina / LAN",
|
||||||
|
"settings.ai.leaves_network": "· sai da sua rede",
|
||||||
|
"settings.ai.advanced_title": "Configuração avançada do Ollama",
|
||||||
|
"settings.ai.changed": "{n} alterados",
|
||||||
|
"settings.ai.reset_all": "Repor tudo para as predefinições",
|
||||||
|
"settings.ai.system_prompt": "Prompt do sistema",
|
||||||
|
"settings.ai.system_prompt_placeholder": "por ex. É um resumidor de reuniões conciso.",
|
||||||
|
"settings.ai.system_prompt_hint": "Adicionado antes do formato de saída exigido pelo WhispAssist, para que as suas instruções não possam quebrar a análise do resumo/tarefas.",
|
||||||
|
"settings.ai.think": "Raciocinar",
|
||||||
|
"settings.ai.think_help": "Esforço de raciocínio (apenas modelos de raciocínio).",
|
||||||
|
"settings.ai.keep_alive": "Manter ativo",
|
||||||
|
"settings.ai.keep_alive_help": "Quanto tempo o modelo permanece na RAM. por ex. 5m, 1h, 0 (descarregar), -1 (para sempre).",
|
||||||
|
"settings.ai.runtime_hardware": "Execução e hardware",
|
||||||
|
"settings.ai.rarely_needed": "— raramente necessário",
|
||||||
|
"settings.ai.save_advanced": "Guardar avançado",
|
||||||
|
"settings.ai.saved": "Guardado",
|
||||||
|
"settings.ai.turn_off": "Desativar",
|
||||||
|
"settings.ai.default_value": "Predefinição {value}.",
|
||||||
|
"settings.ai.reset_default": "Repor para a predefinição",
|
||||||
|
"settings.ai.reset_field": "Repor {name}",
|
||||||
|
|
||||||
|
"settings.mcp.title": "Servidor MCP",
|
||||||
|
"settings.mcp.banner_1": "Isto permite que o seu próprio agente de programação (Claude Code, Codex, Copilot, OpenCode, …) obtenha o contexto da reunião na sua máquina local. Uma vez ligado,",
|
||||||
|
"settings.mcp.banner_agent": "esse agente",
|
||||||
|
"settings.mcp.banner_2": "pode reencaminhar o que lê para a nuvem do seu próprio fornecedor de modelo — fora do controlo do WhispAssist. O próprio WhispAssist nunca envia estes dados para lado nenhum; o servidor apenas escuta neste dispositivo (",
|
||||||
|
"settings.mcp.banner_3": ") e cada leitura é registada abaixo.",
|
||||||
|
"settings.mcp.enable": "Ativar o servidor MCP",
|
||||||
|
"settings.mcp.enable_hint": "Desativado por predefinição. Apenas loopback, protegido por token — nada é acessível a partir da rede.",
|
||||||
|
"settings.mcp.transport": "Transporte",
|
||||||
|
"settings.mcp.transport_http": "HTTP em streaming",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (o agente inicia um processo)",
|
||||||
|
"settings.mcp.port": "Porta",
|
||||||
|
"settings.mcp.transport_change_hint": "Para mudar o transporte/porta, desligue primeiro o servidor e volte a ligá-lo.",
|
||||||
|
"settings.mcp.scope_title": "Âmbito",
|
||||||
|
"settings.mcp.expose": "Expor",
|
||||||
|
"settings.mcp.scope_none": "Nenhum — nada é partilhado",
|
||||||
|
"settings.mcp.scope_selected": "Selecionados — apenas os resumos de funcionalidades que marcou como partilhados",
|
||||||
|
"settings.mcp.scope_all": "Todos — reuniões, transcrições, tarefas e resumos partilhados",
|
||||||
|
"settings.mcp.expose_recordings": "Permitir também reuniões com uma gravação guardada (desativado por predefinição — a transcrição de uma reunião gravada é retida mesmo no âmbito «Todos» até isto ser ativado)",
|
||||||
|
"settings.mcp.token_title": "Novo token de autenticação — mostrado uma vez, copie-o agora",
|
||||||
|
"settings.mcp.token_hint": "Isto não será mostrado novamente. É armazenado no gestor de credenciais do sistema operativo; se o perder, desligue e volte a ligar o servidor para gerar um novo.",
|
||||||
|
"settings.mcp.copied": "Copiado",
|
||||||
|
"settings.mcp.copy": "Copiar",
|
||||||
|
"settings.mcp.endpoint": "Ponto de extremidade",
|
||||||
|
"settings.mcp.endpoint_hint": "Aponte a configuração do cliente MCP do seu agente para {kind}, com o token acima como credencial bearer.",
|
||||||
|
"settings.mcp.kind_command": "comando",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "Registo de acessos",
|
||||||
|
"settings.mcp.access_log_hint": "Cada leitura de ferramenta que um agente faz, permitida ou negada (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "Nenhum agente leu nada ainda.",
|
||||||
|
"settings.mcp.log_meeting": "reunião {id}",
|
||||||
|
"settings.mcp.refresh": "Atualizar",
|
||||||
|
|
||||||
|
"settings.privacy.title": "Privacidade",
|
||||||
|
"settings.privacy.intro": "O que o WhispAssist está realmente autorizado a enviar para fora deste dispositivo neste momento. Com tudo desativado (a predefinição), nada sai do dispositivo.",
|
||||||
|
"settings.privacy.llm_endpoint": "Ponto de extremidade LLM",
|
||||||
|
"settings.privacy.off": "desativado",
|
||||||
|
"settings.privacy.local_only": "apenas local",
|
||||||
|
"settings.privacy.leaves_device": "sai deste dispositivo",
|
||||||
|
"settings.privacy.sync_label": "Sincronização",
|
||||||
|
"settings.privacy.enabled": "ativada",
|
||||||
|
"settings.privacy.mcp_label": "Servidor MCP",
|
||||||
|
"settings.privacy.mcp_on": "ativado · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "Entrada apenas em loopback — não acrescenta nada à lista de saída acima. Um agente ligado pode ainda reencaminhar o que lê para o seu próprio fornecedor de modelo; consulte o separador do servidor MCP.",
|
||||||
|
"settings.privacy.egress_title": "Lista de permissões de saída",
|
||||||
|
"settings.privacy.no_egress": "Nenhum host está na lista de permissões — o WA não faz qualquer saída de conteúdo.",
|
||||||
|
"settings.privacy.sync_targets_title": "Destinos de sincronização",
|
||||||
|
"settings.privacy.third_party": "terceiros",
|
||||||
|
"settings.privacy.your_server": "o seu servidor",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "sem TLS",
|
||||||
|
"settings.privacy.refresh": "Atualizar",
|
||||||
|
"settings.privacy.unavailable": "Autoverificação de privacidade indisponível.",
|
||||||
|
"settings.privacy.vault_title": "Cofre de cifragem",
|
||||||
|
"settings.privacy.vault_intro": "Cifre notas, transcrições e resumos em repouso com uma palavra-passe. (Os ficheiros de áudio ainda não são cifrados.)",
|
||||||
|
"settings.privacy.vault_password": "Palavra-passe do cofre",
|
||||||
|
"settings.privacy.enable_vault": "Ativar cofre",
|
||||||
|
"settings.privacy.vault_pw_hint": "Use pelo menos 8 caracteres. Se a esquecer, o conteúdo cifrado não poderá ser recuperado.",
|
||||||
|
"settings.privacy.vault_locked_1": "O cofre está ",
|
||||||
|
"settings.privacy.locked_word": "bloqueado",
|
||||||
|
"settings.privacy.vault_locked_2": ". Desbloqueie para ler as reuniões cifradas.",
|
||||||
|
"settings.privacy.password": "Palavra-passe",
|
||||||
|
"settings.privacy.unlock": "Desbloquear",
|
||||||
|
"settings.privacy.vault_unlocked_1": "O cofre está ",
|
||||||
|
"settings.privacy.unlocked_word": "desbloqueado",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". As novas notas, transcrições e resumos são cifrados em repouso.",
|
||||||
|
"settings.privacy.lock_now": "Bloquear agora",
|
||||||
|
"settings.privacy.change_password": "Alterar palavra-passe",
|
||||||
|
"settings.privacy.current_password": "Palavra-passe atual",
|
||||||
|
"settings.privacy.new_password": "Nova palavra-passe",
|
||||||
|
|
||||||
|
"settings.about.title": "Acerca de",
|
||||||
|
"settings.about.tagline": "Um assistente de reuniões totalmente local, de código aberto e nativo do Windows.",
|
||||||
|
"settings.about.build_commit": "Commit de compilação",
|
||||||
|
|
||||||
|
"consent.heading": "Antes de gravar",
|
||||||
|
"consent.body": "Gravar conversas sem o consentimento dos participantes pode ser ilegal na sua região. Verifique as leis locais sobre gravação. Isto é uma advertência, não aconselhamento jurídico.",
|
||||||
|
"consent.accept": "Compreendo — ativar a gravação",
|
||||||
|
"consent.cancel": "Cancelar",
|
||||||
|
|
||||||
|
"hosted.heading": "Antes de usar {provider}",
|
||||||
|
"hosted.body_1": "Gerar com",
|
||||||
|
"hosted.body_2": "envia a transcrição desta reunião para os seus servidores — sai deste dispositivo e fica sujeita à sua política de privacidade. O WhispAssist não tem controlo sobre o tratamento dos dados depois de saírem do seu dispositivo. Isto está desativado por predefinição; está a escolhê-lo agora.",
|
||||||
|
"hosted.accept": "Compreendo — continuar",
|
||||||
|
"hosted.cancel": "Cancelar",
|
||||||
|
|
||||||
|
"theme.group": "Tema",
|
||||||
|
"theme.system": "Acompanhar o sistema",
|
||||||
|
"theme.light": "Claro",
|
||||||
|
"theme.dark": "Escuro",
|
||||||
|
|
||||||
|
"import.dialog_label": "Adicionar uma reunião a partir de um ficheiro ou URL",
|
||||||
|
"import.title": "Adicionar uma reunião",
|
||||||
|
"import.close": "Fechar",
|
||||||
|
"import.close_title": "Fechar (Esc)",
|
||||||
|
"import.body": "Importe uma gravação existente — um ficheiro de áudio/vídeo local ou uma ligação (YouTube, uma página de streaming ou um URL de média direto). É transcrita e diarizada tal como uma gravação em direto.",
|
||||||
|
"import.file_or_url": "Ficheiro ou URL",
|
||||||
|
"import.source_placeholder": "Cole um URL ou procure um ficheiro…",
|
||||||
|
"import.choose_file": "Escolher um ficheiro local",
|
||||||
|
"import.browse": "Procurar…",
|
||||||
|
"import.title_label": "Título",
|
||||||
|
"import.optional": "opcional",
|
||||||
|
"import.title_placeholder": "Por predefinição, o nome do ficheiro",
|
||||||
|
"import.filter_av": "Áudio / vídeo",
|
||||||
|
"import.requires_1": "Requer",
|
||||||
|
"import.requires_2": "instalado e no seu PATH (mais",
|
||||||
|
"import.requires_3": "para URLs). O WhispAssist não os inclui.",
|
||||||
|
"import.importing": "A importar… isto pode demorar um pouco",
|
||||||
|
"import.import": "Importar",
|
||||||
|
"import.cancel": "Cancelar",
|
||||||
|
|
||||||
|
"tagchip.filter": "Filtrar reuniões etiquetadas com «{tag}»",
|
||||||
|
"tagchip.remove": "Remover etiqueta {tag}",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "Nível de entrada do sistema e do microfone",
|
||||||
|
"levelmeter.system": "Nível de entrada do sistema",
|
||||||
|
|
||||||
|
"app.tagline": "local · privado",
|
||||||
|
"app.note_template": "Modelo de notas",
|
||||||
|
"app.no_template": "Sem modelo",
|
||||||
|
"app.add_meeting": "Adicionar reunião",
|
||||||
|
"app.add_meeting_title": "Adicionar uma reunião a partir de um ficheiro ou URL",
|
||||||
|
"app.record": "Gravar",
|
||||||
|
"app.record_title": "Iniciar gravação (Ctrl+Shift+R)",
|
||||||
|
"app.stop": "Parar",
|
||||||
|
"app.stop_title": "Parar gravação (Ctrl+Shift+R)",
|
||||||
|
"app.cancel": "Cancelar",
|
||||||
|
"app.cancel_title": "Descartar esta gravação e eliminá-la",
|
||||||
|
"app.recording": "A gravar…",
|
||||||
|
"app.backend_title": "Backend de transcrição ativo",
|
||||||
|
"app.retention_title": "Guardar o áudio como .wav para esta reunião",
|
||||||
|
"app.saving": "a guardar",
|
||||||
|
"app.not_saved": "não guardado",
|
||||||
|
"app.reconnecting": "a reconectar o dispositivo de áudio…",
|
||||||
|
"app.settings": "Definições",
|
||||||
|
"app.settings_title": "Definições (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "Descartar esta gravação? O seu áudio e transcrição serão eliminados.",
|
||||||
|
"app.announce_started": "Gravação iniciada",
|
||||||
|
"app.announce_paused": "Gravação em pausa",
|
||||||
|
"app.announce_stopped": "Gravação parada",
|
||||||
|
"app.consent_dialog": "Consentimento de gravação",
|
||||||
|
"app.vault_dialog": "Desbloquear o cofre de cifragem",
|
||||||
|
"app.vault_title": "Desbloquear o cofre de cifragem",
|
||||||
|
"app.vault_desc": "As suas reuniões estão cifradas em repouso. Introduza a sua palavra-passe para as ler.",
|
||||||
|
"app.vault_password": "Palavra-passe do cofre",
|
||||||
|
"app.vault_incorrect": "Palavra-passe incorreta",
|
||||||
|
"app.unlock": "Desbloquear",
|
||||||
|
"app.continue_locked": "Continuar bloqueado",
|
||||||
|
"app.show_meetings": "Mostrar lista de reuniões",
|
||||||
|
"app.hide_meetings": "Ocultar lista de reuniões",
|
||||||
|
"app.show_summary": "Mostrar painel de resumo",
|
||||||
|
"app.hide_summary": "Ocultar painel de resumo",
|
||||||
|
"app.resize_meetings": "Redimensionar lista de reuniões",
|
||||||
|
"app.resize_summary": "Redimensionar painel de resumo",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "Procurar reuniões…",
|
||||||
|
"meetings.search_aria": "Procurar reuniões",
|
||||||
|
"meetings.filter_tag_aria": "Filtrar por etiqueta",
|
||||||
|
"meetings.all_tags": "Todas as etiquetas",
|
||||||
|
"meetings.from_date_aria": "Da data",
|
||||||
|
"meetings.to_date_aria": "Até à data",
|
||||||
|
"meetings.bulk_format_aria": "Formato de exportação em massa",
|
||||||
|
"meetings.bulk_export": "Exportação em massa",
|
||||||
|
"meetings.bulk_export_title": "Exportar todas as reuniões que correspondam aos filtros de etiqueta/data acima",
|
||||||
|
"meetings.exporting": "A exportar…",
|
||||||
|
"meetings.exported_one": "{n} reunião exportada.",
|
||||||
|
"meetings.exported_many": "{n} reuniões exportadas.",
|
||||||
|
"meetings.searching": "A procurar…",
|
||||||
|
"meetings.loading": "A carregar reuniões…",
|
||||||
|
"meetings.no_matches": "Sem correspondências.",
|
||||||
|
"meetings.empty": "Ainda não há reuniões — clique em Gravar acima para começar.",
|
||||||
|
"meetings.status.recording": "a gravar",
|
||||||
|
"meetings.status.transcribing": "a transcrever",
|
||||||
|
"meetings.status.recovering": "a recuperar",
|
||||||
|
"meetings.status.error": "erro",
|
||||||
|
"meetings.resume": "Retomar transcrição",
|
||||||
|
"meetings.delete_aria": "Eliminar reunião",
|
||||||
|
"meetings.delete_confirm": "Eliminar esta reunião? Isto remove a sua gravação, transcrição e notas.",
|
||||||
|
|
||||||
|
"transcript.heading": "Transcrição",
|
||||||
|
"transcript.title_aria": "Título da reunião",
|
||||||
|
"transcript.lang_title": "Idioma de transcrição",
|
||||||
|
"transcript.lang_auto": "a detetar…",
|
||||||
|
"transcript.show": "Mostrar transcrição",
|
||||||
|
"transcript.hide": "Ocultar transcrição",
|
||||||
|
"transcript.empty": "Não há transcrição para esta reunião.",
|
||||||
|
"transcript.will_appear": "A transcrição aparecerá aqui à medida que grava.",
|
||||||
|
"transcript.play_from_here": "Reproduzir a partir daqui",
|
||||||
|
"transcript.resize": "Redimensionar transcrição e notas",
|
||||||
|
"transcript.reprocess_placeholder": "Voltar a transcrever com…",
|
||||||
|
"transcript.reprocess_lang_aria": "Idioma de reprocessamento",
|
||||||
|
"transcript.reprocess_keep_lang": "Manter o idioma atual",
|
||||||
|
"transcript.reprocess_go": "Avançar",
|
||||||
|
"transcript.retranscribing": "A voltar a transcrever…",
|
||||||
|
"transcript.has_note": "Tem uma nota",
|
||||||
|
"transcript.note_placeholder": "Adicionar uma nota para este momento…",
|
||||||
|
"transcript.note_aria": "Nota para esta linha da transcrição",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Documento do Word",
|
||||||
|
"transcript.filter_obsidian": "Nota do Obsidian",
|
||||||
|
|
||||||
|
"notes.heading": "Notas",
|
||||||
|
"notes.show": "Mostrar notas",
|
||||||
|
"notes.hide": "Ocultar notas",
|
||||||
|
"notes.placeholder": "Notas…",
|
||||||
|
"notes.live_placeholder": "Escreva notas enquanto fala…",
|
||||||
|
"notes.toolbar_aria": "Formatação de notas",
|
||||||
|
"notes.bold": "Negrito",
|
||||||
|
"notes.italic": "Itálico",
|
||||||
|
"notes.h1": "Título 1",
|
||||||
|
"notes.h2": "Título 2",
|
||||||
|
"notes.bullet": "Lista com marcadores",
|
||||||
|
"notes.checkbox_title": "Caixa de verificação",
|
||||||
|
"notes.checkbox_aria": "Item de lista com caixa de verificação",
|
||||||
|
"notes.edit_raw": "Editar o markdown em bruto",
|
||||||
|
"notes.render": "Compor o markdown",
|
||||||
|
"notes.editor": "Editor",
|
||||||
|
"notes.preview": "Pré-visualização",
|
||||||
|
"notes.export_md_title": "Exportar notas como .md",
|
||||||
|
"notes.export_pdf_title": "Exportar notas como .pdf",
|
||||||
|
"notes.export_docx_title": "Exportar notas como .docx",
|
||||||
|
"notes.export_bundle_title": "Exportar áudio + transcrição + notas para uma pasta",
|
||||||
|
"notes.export_obsidian_title": "Exportar uma nota do Obsidian (notas + resumo + transcrição, sem áudio)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "Gravação",
|
||||||
|
"summary.loading_recording": "A carregar gravação…",
|
||||||
|
"summary.sync_heading": "Sincronização",
|
||||||
|
"summary.uploading": "A carregar…",
|
||||||
|
"summary.upload_now": "Carregar agora",
|
||||||
|
"summary.no_uploads": "Ainda não há carregamentos para esta reunião.",
|
||||||
|
"summary.retry": "repetir",
|
||||||
|
"summary.tags_heading": "Etiquetas",
|
||||||
|
"summary.tags_select": "Selecione uma reunião para a etiquetar.",
|
||||||
|
"summary.tag_add_placeholder": "Adicionar etiqueta…",
|
||||||
|
"summary.tag_first_placeholder": "projeto, cliente, tema…",
|
||||||
|
"summary.generating": "A gerar…",
|
||||||
|
"summary.generate_tags": "Gerar etiquetas",
|
||||||
|
"summary.saving": "A guardar…",
|
||||||
|
"summary.save_tags": "Guardar etiquetas",
|
||||||
|
"summary.summary_heading": "Resumo",
|
||||||
|
"summary.ai_provider": "Fornecedor de IA",
|
||||||
|
"summary.provider_off": "Desativado",
|
||||||
|
"summary.provider_custom": "Personalizado",
|
||||||
|
"summary.local": "local",
|
||||||
|
"summary.leaves_device": "sai deste dispositivo",
|
||||||
|
"summary.this_hosted_provider": "este fornecedor alojado",
|
||||||
|
"summary.select_generate": "Selecione uma reunião para gerar um resumo.",
|
||||||
|
"summary.decisions_heading": "Decisões",
|
||||||
|
"summary.regenerate": "Regenerar",
|
||||||
|
"summary.generated_hint": "Gerado localmente após a reunião (requer um fornecedor de LLM local).",
|
||||||
|
"summary.no_provider": "Nenhum fornecedor de IA configurado — ative um nas Definições.",
|
||||||
|
"summary.generate_summary": "Gerar resumo",
|
||||||
|
"summary.briefs_heading": "Resumos de funcionalidades",
|
||||||
|
"summary.briefs_select": "Selecione uma reunião para criar ou ver resumos de funcionalidades.",
|
||||||
|
"summary.briefs_desc": "Destila esta reunião numa especificação pronta para agentes (requer um fornecedor de LLM local) — entregue-a a um agente de programação, ou disponibilize-a através do servidor MCP quando este estiver ativado.",
|
||||||
|
"summary.brief_repo_placeholder": "Repositório de destino (opcional), por ex. acme/reporting-web",
|
||||||
|
"summary.distilling": "A destilar…",
|
||||||
|
"summary.create_brief": "Criar resumo de funcionalidade",
|
||||||
|
"summary.brief_mcp_title": "Disponível quando o servidor MCP está ativado",
|
||||||
|
"summary.copied": "Copiado",
|
||||||
|
"summary.copy_md": "Copiar como Markdown",
|
||||||
|
"summary.copy_json": "Copiar como JSON",
|
||||||
|
"summary.brief_problem": "Problema",
|
||||||
|
"summary.brief_outcome": "Resultado desejado",
|
||||||
|
"summary.brief_criteria": "Critérios de aceitação",
|
||||||
|
"summary.brief_none": "Nada capturado.",
|
||||||
|
"summary.brief_context": "Contexto",
|
||||||
|
"summary.action_items_heading": "Tarefas",
|
||||||
|
"summary.ai_select": "Selecione uma reunião para ver as suas tarefas.",
|
||||||
|
"summary.ai_empty": "Ainda nenhuma — adicione uma abaixo, ou gere um resumo para as extrair automaticamente.",
|
||||||
|
"summary.confirmed": "Confirmada",
|
||||||
|
"summary.ai_text_placeholder": "Tarefa…",
|
||||||
|
"summary.ai_text_aria": "Texto da tarefa",
|
||||||
|
"summary.owner": "Responsável",
|
||||||
|
"summary.due_date": "Data-limite",
|
||||||
|
"summary.reminder_title": "Agendar um lembrete local para esta data-limite",
|
||||||
|
"summary.ai_delete_title": "Eliminar esta tarefa",
|
||||||
|
"summary.ai_delete_aria": "Eliminar tarefa",
|
||||||
|
"summary.add_action_item": "Adicionar tarefa",
|
||||||
|
"summary.save_action_items": "Guardar tarefas",
|
||||||
|
"summary.calendar_heading": "Evento do calendário",
|
||||||
|
"summary.cal_select": "Selecione uma reunião para a associar a um evento do calendário.",
|
||||||
|
"summary.event_search_placeholder": "Procurar título do evento…",
|
||||||
|
"summary.linked_event": "Evento associado",
|
||||||
|
"summary.change_event": "Mudar evento…",
|
||||||
|
"summary.link_event": "Associar um evento…",
|
||||||
|
"summary.untitled_event": "(sem título)",
|
||||||
|
"summary.no_events_1": "Ainda não foram importados eventos — importe um",
|
||||||
|
"summary.no_events_2": "em Definições → Calendário.",
|
||||||
|
"summary.no_events_match": "Nenhum evento corresponde a esta procura/data — tente limpar uma.",
|
||||||
|
"summary.organizer_label": "Organizador: {name}",
|
||||||
|
"summary.participants_heading": "Participantes",
|
||||||
|
"summary.participants_hint": "Preenchido a partir do evento do calendário associado.",
|
||||||
|
"summary.speakers_heading": "Oradores",
|
||||||
|
"summary.speakers_select": "Selecione uma reunião para nomear os seus oradores.",
|
||||||
|
"summary.no_speakers": "Ainda não foram detetados oradores.",
|
||||||
|
"summary.speaker_name_placeholder": "Nome do orador",
|
||||||
|
"summary.save": "Guardar",
|
||||||
|
"summary.cancel": "Cancelar",
|
||||||
|
"summary.name_speaker": "Nomear este orador…",
|
||||||
|
"summary.add_new_name": "+ Adicionar novo nome…",
|
||||||
|
|
||||||
|
"settings.recording.title": "Gravação",
|
||||||
|
"settings.recording.record_default": "Gravar reuniões por predefinição",
|
||||||
|
"settings.recording.save_audio_as": "guardar áudio como",
|
||||||
|
"settings.recording.default_hint": "Desativado por predefinição. Quando desativado, o áudio é usado apenas para produzir a transcrição e é eliminado quando a reunião é finalizada. Também pode alternar a gravação por reunião.",
|
||||||
|
"settings.recording.consent_label": "Consentimento: {status}.",
|
||||||
|
"settings.recording.consent_ack": "confirmado",
|
||||||
|
"settings.recording.consent_not": "ainda não confirmado",
|
||||||
|
"settings.recording.auto_label": "Iniciar a gravação automaticamente quando um evento do calendário começa",
|
||||||
|
"settings.recording.auto_hint": "Apenas enquanto o WhispAssist estiver aberto. Quando chega a hora de início de um evento do calendário importado, uma gravação começa automaticamente (usando a sua definição de retenção predefinida acima). Nada é executado em segundo plano — o temporizador só é armado enquanto a aplicação está em execução. Importe eventos em Definições → Calendário."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "Запись",
|
||||||
|
"nav.hardware": "Оборудование",
|
||||||
|
"nav.storage": "Хранилище",
|
||||||
|
"nav.calendar": "Календарь",
|
||||||
|
"nav.sync": "Синхронизация",
|
||||||
|
"nav.ai": "ИИ",
|
||||||
|
"nav.mcp": "MCP-сервер",
|
||||||
|
"nav.privacy": "Конфиденциальность",
|
||||||
|
"nav.about": "О программе",
|
||||||
|
"nav.language": "Язык",
|
||||||
|
|
||||||
|
"settings.language.title": "Язык",
|
||||||
|
"settings.language.display": "Язык интерфейса",
|
||||||
|
"settings.language.display_hint": "Язык интерфейса приложения. Добавить язык так же просто, как поместить один файл перевода.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "Язык транскрипции",
|
||||||
|
"settings.transcription.label": "Язык",
|
||||||
|
"settings.transcription.auto": "Автоопределение",
|
||||||
|
"settings.transcription.applies_hint": "Применяется к следующей записи. Фактически использованный язык затем показывается для каждой встречи.",
|
||||||
|
"settings.transcription.english_only": "Только английский",
|
||||||
|
"settings.transcription.model_english_only": "«{model}» — только на английском.",
|
||||||
|
"settings.transcription.no_model": "Модель не выбрана.",
|
||||||
|
"settings.transcription.switch_multilingual": "Переключитесь на многоязычную модель выше, чтобы выбрать язык.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "Оборудование",
|
||||||
|
"settings.hardware.active_backend": "Активный бэкенд",
|
||||||
|
"settings.hardware.model_meta": "· модель {size}",
|
||||||
|
"settings.hardware.preferred_backend": "Предпочитаемый бэкенд",
|
||||||
|
"settings.hardware.auto_backend": "Авто (лучший доступный)",
|
||||||
|
"settings.hardware.not_available": "недоступно",
|
||||||
|
"settings.hardware.fallback_hint": "Автоматически переключается (NPU → NVIDIA → AMD → Intel → CPU), если выбранный бэкенд не загружается.",
|
||||||
|
"settings.hardware.audio_devices": "Аудиоустройства",
|
||||||
|
"settings.hardware.recording_device": "Устройство записи",
|
||||||
|
"settings.hardware.default_system_audio": "Системный звук по умолчанию",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist записывает то, что воспроизводит это устройство (петля) — другую сторону звонка. Выберите конкретный выход, если не хотите, чтобы он следовал за системным значением по умолчанию в Windows.",
|
||||||
|
"settings.hardware.microphone": "Микрофон",
|
||||||
|
"settings.hardware.mic_off": "Выкл. — не захватывать мой микрофон",
|
||||||
|
"settings.hardware.default_mic": "Микрофон по умолчанию",
|
||||||
|
"settings.hardware.mic_hint": "Добавляет ваш голос в живую транскрипцию, чтобы захватывались обе стороны встречи. Остаётся на вашем устройстве — ничего не выгружается. Выберите «Выкл.», чтобы транскрибировать только системный звук выше.",
|
||||||
|
"settings.hardware.npu_detected": "Обнаружен ускоритель NPU",
|
||||||
|
"settings.hardware.ready": "Готово",
|
||||||
|
"settings.hardware.downloading": "Загрузка…",
|
||||||
|
"settings.hardware.package_needed": "Требуется пакет",
|
||||||
|
"settings.hardware.npu_hint": "Вашему NPU нужен одноразовый пакет ускорения (модель Whisper ONNX{extra}). Он загружается в фоне; вы также можете запустить его сейчас.",
|
||||||
|
"settings.hardware.plus_openvino": " + среда выполнения OpenVINO",
|
||||||
|
"settings.hardware.download_npu": "Загрузить пакет NPU",
|
||||||
|
"settings.hardware.dml_title": "Ускорение GPU (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "Ваш GPU можно ускорить без Vulkan через DirectML — одноразовый пакет (модель Whisper ONNX{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + среда выполнения DirectML",
|
||||||
|
"settings.hardware.download_dml": "Загрузить пакет DirectML",
|
||||||
|
"settings.hardware.detection_unavailable": "Обнаружение оборудования недоступно.",
|
||||||
|
"settings.hardware.low_overhead": "Пресет низкой нагрузки",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + наименьшая модель, для работы от батареи/в фоне",
|
||||||
|
"settings.hardware.models": "Модели",
|
||||||
|
"settings.hardware.downloading_model": "Загрузка {name}",
|
||||||
|
"settings.hardware.active_badge": "Активно",
|
||||||
|
"settings.hardware.installed": "Установлено",
|
||||||
|
"settings.hardware.use": "Использовать",
|
||||||
|
"settings.hardware.remove": "Удалить",
|
||||||
|
"settings.hardware.cant_remove_active": "Нельзя удалить активную модель",
|
||||||
|
"settings.hardware.download": "Загрузить",
|
||||||
|
"settings.hardware.diarization": "Разделение по дикторам",
|
||||||
|
"settings.hardware.diarization_hint": "Установите обе модели, чтобы завершённые записи разделялись по дикторам (Диктор 1, Диктор 2, …) вместо единой сплошной транскрипции. Работает полностью офлайн, один раз за встречу после её остановки. Без них каждая строка приписывается одному диктору.",
|
||||||
|
|
||||||
|
"settings.storage.title": "Хранилище",
|
||||||
|
"settings.storage.location_label": "Расположение",
|
||||||
|
"settings.storage.location_hint": "Изменение расположения хранилища пока не поддерживается — здесь сейчас находятся встречи, модели и база данных.",
|
||||||
|
"settings.storage.retention_title": "Хранение",
|
||||||
|
"settings.storage.retention_hint": "Автоматически удаляет самые старые встречи сверх лимита. Проверяется один раз при запуске; пусто — без лимита. Никогда не затрагивает встречу, которая записывается в данный момент.",
|
||||||
|
"settings.storage.max_age": "Макс. возраст (дней)",
|
||||||
|
"settings.storage.max_size": "Макс. размер (ГБ)",
|
||||||
|
"settings.storage.no_limit": "без лимита",
|
||||||
|
"settings.storage.export_import_title": "Экспорт и импорт",
|
||||||
|
"settings.storage.export_hint_1": "Перемещайте записи между компьютерами в виде переносимых папок-пакетов (аудио, транскрипция, заметки, сводка и",
|
||||||
|
"settings.storage.export_hint_2": "манифест). Экспортируйте в любую папку — синхронизируемый диск, USB-накопитель или локальную точку монтирования цели синхронизации — а затем импортируйте на другой машине. Импортированные встречи получают новый id, поэтому повторный импорт ничего не перезаписывает.",
|
||||||
|
"settings.storage.export_dialog_title": "Экспортировать все встречи в…",
|
||||||
|
"settings.storage.import_dialog_title": "Импортировать пакет (или папку пакетов)…",
|
||||||
|
"settings.storage.exporting": "Экспорт…",
|
||||||
|
"settings.storage.export_all": "Экспортировать все встречи…",
|
||||||
|
"settings.storage.importing": "Импорт…",
|
||||||
|
"settings.storage.import_all": "Импортировать встречи…",
|
||||||
|
"settings.storage.exported_one": "Экспортирована {n} встреча.",
|
||||||
|
"settings.storage.exported_many": "Экспортировано встреч: {n}.",
|
||||||
|
"settings.storage.imported_one": "Импортирована {n} встреча.",
|
||||||
|
"settings.storage.imported_many": "Импортировано встреч: {n}.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "Календарь и Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "Импортируйте события и участников из локальной резервной копии Outlook",
|
||||||
|
"settings.calendar.intro_2": "— только чтение, ничего не записывается обратно в файл. Ничего не покидает это устройство.",
|
||||||
|
"settings.calendar.pst_file": "Файл .pst",
|
||||||
|
"settings.calendar.no_file": "Файл не выбран",
|
||||||
|
"settings.calendar.browse": "Обзор…",
|
||||||
|
"settings.calendar.password": "Пароль",
|
||||||
|
"settings.calendar.password_note": "требуется редко",
|
||||||
|
"settings.calendar.optional": "необязательно",
|
||||||
|
"settings.calendar.import_range": "Диапазон импорта",
|
||||||
|
"settings.calendar.range_30": "Последние 30 дней",
|
||||||
|
"settings.calendar.range_90": "Последние 90 дней",
|
||||||
|
"settings.calendar.range_180": "Последние 6 месяцев",
|
||||||
|
"settings.calendar.range_365": "Последний год",
|
||||||
|
"settings.calendar.range_all": "За всё время",
|
||||||
|
"settings.calendar.range_hint": "Долго существующий почтовый ящик может содержать годы повторяющихся/праздничных записей — сужение диапазона оставляет в импортированном календаре только действительно актуальное. Применяется как к этой кнопке импорта, так и к автоматическому повторному импорту при запуске.",
|
||||||
|
"settings.calendar.importing": "Импорт…",
|
||||||
|
"settings.calendar.import": "Импортировать",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} событий",
|
||||||
|
"settings.calendar.auto_reimport": "Повторно импортировать этот файл автоматически при запуске",
|
||||||
|
"settings.calendar.auto_reimport_hint": "Выполняется один раз при запуске, а не по таймеру — повторный импорт безопасно повторять (существующие события сопоставляются и обновляются, а не дублируются).",
|
||||||
|
"settings.calendar.import_failed": "Ошибка импорта: {error} — сам файл не затронут; проверьте путь и повторите попытку.",
|
||||||
|
"settings.calendar.cleanup_title": "Очистка",
|
||||||
|
"settings.calendar.cleanup_hint": "События, уже привязанные к записанной встрече, всегда сохраняются, независимо от выбранного ниже варианта.",
|
||||||
|
"settings.calendar.cleanup_30": "Старше 30 дней",
|
||||||
|
"settings.calendar.cleanup_90": "Старше 90 дней",
|
||||||
|
"settings.calendar.cleanup_180": "Старше 6 месяцев",
|
||||||
|
"settings.calendar.cleanup_365": "Старше 1 года",
|
||||||
|
"settings.calendar.cleanup_all": "Удалить все",
|
||||||
|
"settings.calendar.cleaning": "Очистка…",
|
||||||
|
"settings.calendar.cleanup_btn": "Очистить календарь",
|
||||||
|
"settings.calendar.cleanup_result": "Удалено: {deleted}{extra}.",
|
||||||
|
"settings.calendar.cleanup_kept": ", сохранено {n} (привязаны к встрече)",
|
||||||
|
"settings.calendar.cleanup_failed": "Ошибка очистки: {error}",
|
||||||
|
"settings.calendar.imported_events": "Импортированные события",
|
||||||
|
"settings.calendar.no_events": "События ещё не импортированы.",
|
||||||
|
"settings.calendar.search_title": "Поиск по названию",
|
||||||
|
"settings.calendar.search_placeholder": "Название встречи…",
|
||||||
|
"settings.calendar.date_label": "Дата",
|
||||||
|
"settings.calendar.events_count": "{shown} из {total} событий",
|
||||||
|
"settings.calendar.untitled": "(без названия)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "Настройки",
|
||||||
|
"settings.close": "Закрыть",
|
||||||
|
"settings.close_title": "Закрыть (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "Режим предпросмотра — бэкенд синхронизации ещё не реализован (Фаза 9). Изменения сохраняются только в интерфейсе.",
|
||||||
|
"settings.sync.title": "Синхронизация и выгрузка",
|
||||||
|
"settings.sync.enable_label": "Включить выгрузку артефактов встреч в настроенные цели",
|
||||||
|
"settings.sync.enable_hint": "Выключено по умолчанию. Ничего не выгружается, пока это не включено и не включена цель. Самостоятельно размещённые цели хранят данные на вашем собственном сервере; сторонние облака чётко помечены.",
|
||||||
|
"settings.sync.targets_title": "Цели",
|
||||||
|
"settings.sync.no_targets": "Целей пока нет. Добавьте одну ниже.",
|
||||||
|
"settings.sync.third_party": "сторонняя",
|
||||||
|
"settings.sync.your_server": "ваш сервер",
|
||||||
|
"settings.sync.edit": "Изменить",
|
||||||
|
"settings.sync.remove": "Удалить",
|
||||||
|
"settings.sync.edit_target": "Изменить цель",
|
||||||
|
"settings.sync.add_target": "Добавить цель",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV охватывает Nextcloud, ownCloud, Cloudreve, Seafile и Synology. (Seafile: включите SeafDAV на стороне сервера.)",
|
||||||
|
"settings.sync.name": "Имя",
|
||||||
|
"settings.sync.name_placeholder": "Домашний Nextcloud",
|
||||||
|
"settings.sync.provider": "Провайдер",
|
||||||
|
"settings.sync.server_url": "URL сервера",
|
||||||
|
"settings.sync.server_url_note_1": "Только URL вашего сервера — WhispAssist добавляет",
|
||||||
|
"settings.sync.server_url_note_2": "автоматически.",
|
||||||
|
"settings.sync.remote_folder": "Удалённая папка",
|
||||||
|
"settings.sync.username": "Имя пользователя",
|
||||||
|
"settings.sync.app_password": "Пароль приложения",
|
||||||
|
"settings.sync.pw_keep": "оставьте пустым, чтобы сохранить текущий пароль",
|
||||||
|
"settings.sync.pw_store": "хранится в хранилище учётных данных ОС",
|
||||||
|
"settings.sync.upload_legend": "Выгрузить",
|
||||||
|
"settings.sync.artifact_transcript": "транскрипция",
|
||||||
|
"settings.sync.artifact_notes": "заметки",
|
||||||
|
"settings.sync.artifact_summary": "сводка",
|
||||||
|
"settings.sync.artifact_recording": "запись (.wav, если сохранена)",
|
||||||
|
"settings.sync.allow_plaintext": "Разрешить открытый http для LAN-адреса (не рекомендуется)",
|
||||||
|
"settings.sync.encrypt_before": "Шифровать перед выгрузкой (место назначения хранит только шифртекст)",
|
||||||
|
"settings.sync.requires_vault": "— требует разблокированного хранилища",
|
||||||
|
"settings.sync.test_connection": "Проверить соединение",
|
||||||
|
"settings.sync.save_changes": "Сохранить изменения",
|
||||||
|
"settings.sync.add_target_btn": "Добавить цель",
|
||||||
|
"settings.sync.cancel": "Отмена",
|
||||||
|
"settings.sync.third_party_banner": "{kind} — стороннее облако; выгрузка отправляет ваши данные за пределы устройства в {kind}.",
|
||||||
|
"settings.sync.link_account": "Привязать аккаунт {kind}…",
|
||||||
|
"settings.sync.oauth_hint": "Открывает вход через OAuth (перенаправление на петлю); токен хранится в хранилище учётных данных ОС.",
|
||||||
|
"settings.sync.footnote": "Учётные данные никогда не записываются в настройки или базу данных — только в хранилище учётных данных ОС. Для целей вне LAN требуется TLS.",
|
||||||
|
|
||||||
|
"settings.ai.title": "Провайдер сводок ИИ",
|
||||||
|
"settings.ai.intro_1": "Сводки по умолчанию выполняются на локальной LLM. Укажите здесь на Ollama на этом ПК или другой машине в вашей LAN (напр.",
|
||||||
|
"settings.ai.intro_2": "— обе считаются локальными, поэтому ничего не покидает вашу сеть. Размещённые провайдеры (Anthropic) необязательны, выключены по умолчанию и отправляют транскрипцию третьей стороне, как только вы включите одного.",
|
||||||
|
"settings.ai.provider": "Провайдер",
|
||||||
|
"settings.ai.off": "Выкл.",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (локально / LAN)",
|
||||||
|
"settings.ai.provider_custom": "Пользовательский (совместимый с OpenAI)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — размещённый, покидает это устройство",
|
||||||
|
"settings.ai.endpoint": "Конечная точка",
|
||||||
|
"settings.ai.model": "Модель",
|
||||||
|
"settings.ai.api_key": "Ключ API",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (уже задан — оставьте пустым, чтобы сохранить)",
|
||||||
|
"settings.ai.show_api_key": "Показать ключ API",
|
||||||
|
"settings.ai.hide_api_key": "Скрыть ключ API",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic — размещённая сторонняя служба; транскрипция этой встречи покидает ваше устройство, когда вы генерируете сводку.",
|
||||||
|
"settings.ai.endpoint_banner": "Эта конечная точка не на вашей машине и не в вашей LAN — ваша транскрипция покинула бы вашу сеть.",
|
||||||
|
"settings.ai.saving": "Сохранение…",
|
||||||
|
"settings.ai.save": "Сохранить",
|
||||||
|
"settings.ai.test_connection": "Проверить соединение",
|
||||||
|
"settings.ai.this_hosted_provider": "этот размещённый провайдер",
|
||||||
|
"settings.ai.reachable": "Доступно",
|
||||||
|
"settings.ai.unreachable": "Недоступно",
|
||||||
|
"settings.ai.model_count_one": "· {n} модель",
|
||||||
|
"settings.ai.model_count_many": "· моделей: {n}",
|
||||||
|
"settings.ai.on_lan": "· на вашей машине / LAN",
|
||||||
|
"settings.ai.leaves_network": "· покидает вашу сеть",
|
||||||
|
"settings.ai.advanced_title": "Расширенная настройка Ollama",
|
||||||
|
"settings.ai.changed": "изменено: {n}",
|
||||||
|
"settings.ai.reset_all": "Сбросить всё к значениям по умолчанию",
|
||||||
|
"settings.ai.system_prompt": "Системный промпт",
|
||||||
|
"settings.ai.system_prompt_placeholder": "напр. Вы — лаконичный составитель сводок встреч.",
|
||||||
|
"settings.ai.system_prompt_hint": "Добавляется перед обязательным форматом вывода WhispAssist, чтобы ваши инструкции не могли нарушить разбор сводки/задач.",
|
||||||
|
"settings.ai.think": "Размышление",
|
||||||
|
"settings.ai.think_help": "Усилие рассуждения (только для моделей рассуждения).",
|
||||||
|
"settings.ai.keep_alive": "Держать в памяти",
|
||||||
|
"settings.ai.keep_alive_help": "Как долго модель остаётся в ОЗУ. напр. 5m, 1h, 0 (выгрузить), -1 (навсегда).",
|
||||||
|
"settings.ai.runtime_hardware": "Среда выполнения и оборудование",
|
||||||
|
"settings.ai.rarely_needed": "— требуется редко",
|
||||||
|
"settings.ai.save_advanced": "Сохранить расширенные",
|
||||||
|
"settings.ai.saved": "Сохранено",
|
||||||
|
"settings.ai.turn_off": "Выключить",
|
||||||
|
"settings.ai.default_value": "По умолчанию {value}.",
|
||||||
|
"settings.ai.reset_default": "Сбросить к значению по умолчанию",
|
||||||
|
"settings.ai.reset_field": "Сбросить {name}",
|
||||||
|
|
||||||
|
"settings.mcp.title": "MCP-сервер",
|
||||||
|
"settings.mcp.banner_1": "Это позволяет вашему собственному кодовому агенту (Claude Code, Codex, Copilot, OpenCode, …) получать контекст встречи на вашей локальной машине. После подключения",
|
||||||
|
"settings.mcp.banner_agent": "этот агент",
|
||||||
|
"settings.mcp.banner_2": "может пересылать прочитанное в облако собственного провайдера модели — вне контроля WhispAssist. Сам WhispAssist никогда никуда не отправляет эти данные; сервер слушает только на этом устройстве (",
|
||||||
|
"settings.mcp.banner_3": ") и каждое чтение регистрируется ниже.",
|
||||||
|
"settings.mcp.enable": "Включить MCP-сервер",
|
||||||
|
"settings.mcp.enable_hint": "Выключено по умолчанию. Только петля, с защитой токеном — из сети ничего недоступно.",
|
||||||
|
"settings.mcp.transport": "Транспорт",
|
||||||
|
"settings.mcp.transport_http": "Потоковый HTTP",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (агент порождает процесс)",
|
||||||
|
"settings.mcp.port": "Порт",
|
||||||
|
"settings.mcp.transport_change_hint": "Чтобы изменить транспорт/порт, сначала выключите сервер, затем включите снова.",
|
||||||
|
"settings.mcp.scope_title": "Область",
|
||||||
|
"settings.mcp.expose": "Открыть",
|
||||||
|
"settings.mcp.scope_none": "Нет — ничего не предоставляется",
|
||||||
|
"settings.mcp.scope_selected": "Выбранные — только брифы функций, помеченные вами как общие",
|
||||||
|
"settings.mcp.scope_all": "Все — встречи, транскрипции, задачи и общие брифы",
|
||||||
|
"settings.mcp.expose_recordings": "Также разрешить встречи с сохранённой записью (выключено по умолчанию — транскрипция записанной встречи удерживается даже в области «Все», пока это не включено)",
|
||||||
|
"settings.mcp.token_title": "Новый токен аутентификации — показывается один раз, скопируйте его сейчас",
|
||||||
|
"settings.mcp.token_hint": "Он больше не будет показан. Он хранится в хранилище учётных данных ОС; если вы его потеряете, выключите и снова включите сервер, чтобы создать новый.",
|
||||||
|
"settings.mcp.copied": "Скопировано",
|
||||||
|
"settings.mcp.copy": "Копировать",
|
||||||
|
"settings.mcp.endpoint": "Конечная точка",
|
||||||
|
"settings.mcp.endpoint_hint": "Укажите конфигурацию MCP-клиента вашего агента на {kind}, используя токен выше как учётные данные bearer.",
|
||||||
|
"settings.mcp.kind_command": "команда",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "Журнал доступа",
|
||||||
|
"settings.mcp.access_log_hint": "Каждое чтение инструмента агентом, разрешённое или отклонённое (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "Ни один агент пока ничего не прочитал.",
|
||||||
|
"settings.mcp.log_meeting": "встреча {id}",
|
||||||
|
"settings.mcp.refresh": "Обновить",
|
||||||
|
|
||||||
|
"settings.privacy.title": "Конфиденциальность",
|
||||||
|
"settings.privacy.intro": "Что WhispAssist на самом деле сейчас имеет право отправлять за пределы этого устройства. Когда всё выключено (по умолчанию), ничего не покидает устройство.",
|
||||||
|
"settings.privacy.llm_endpoint": "Конечная точка LLM",
|
||||||
|
"settings.privacy.off": "выкл.",
|
||||||
|
"settings.privacy.local_only": "только локально",
|
||||||
|
"settings.privacy.leaves_device": "покидает это устройство",
|
||||||
|
"settings.privacy.sync_label": "Синхронизация",
|
||||||
|
"settings.privacy.enabled": "включена",
|
||||||
|
"settings.privacy.mcp_label": "MCP-сервер",
|
||||||
|
"settings.privacy.mcp_on": "вкл. · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "Входящий только по петле — ничего не добавляет в список исходящего выше. Подключённый агент всё же может пересылать прочитанное собственному провайдеру модели; см. вкладку MCP-сервера.",
|
||||||
|
"settings.privacy.egress_title": "Список разрешённого исходящего трафика",
|
||||||
|
"settings.privacy.no_egress": "Ни один хост не в списке разрешённых — WA не осуществляет исходящей передачи содержимого.",
|
||||||
|
"settings.privacy.sync_targets_title": "Цели синхронизации",
|
||||||
|
"settings.privacy.third_party": "сторонняя",
|
||||||
|
"settings.privacy.your_server": "ваш сервер",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "без TLS",
|
||||||
|
"settings.privacy.refresh": "Обновить",
|
||||||
|
"settings.privacy.unavailable": "Самопроверка конфиденциальности недоступна.",
|
||||||
|
"settings.privacy.vault_title": "Хранилище шифрования",
|
||||||
|
"settings.privacy.vault_intro": "Шифруйте заметки, транскрипции и сводки в состоянии покоя с помощью пароля. (Аудиофайлы пока не шифруются.)",
|
||||||
|
"settings.privacy.vault_password": "Пароль хранилища",
|
||||||
|
"settings.privacy.enable_vault": "Включить хранилище",
|
||||||
|
"settings.privacy.vault_pw_hint": "Используйте не менее 8 символов. Если вы его забудете, зашифрованное содержимое нельзя будет восстановить.",
|
||||||
|
"settings.privacy.vault_locked_1": "Хранилище ",
|
||||||
|
"settings.privacy.locked_word": "заблокировано",
|
||||||
|
"settings.privacy.vault_locked_2": ". Разблокируйте, чтобы читать зашифрованные встречи.",
|
||||||
|
"settings.privacy.password": "Пароль",
|
||||||
|
"settings.privacy.unlock": "Разблокировать",
|
||||||
|
"settings.privacy.vault_unlocked_1": "Хранилище ",
|
||||||
|
"settings.privacy.unlocked_word": "разблокировано",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". Новые заметки, транскрипции и сводки шифруются в состоянии покоя.",
|
||||||
|
"settings.privacy.lock_now": "Заблокировать сейчас",
|
||||||
|
"settings.privacy.change_password": "Изменить пароль",
|
||||||
|
"settings.privacy.current_password": "Текущий пароль",
|
||||||
|
"settings.privacy.new_password": "Новый пароль",
|
||||||
|
|
||||||
|
"settings.about.title": "О программе",
|
||||||
|
"settings.about.tagline": "Полностью локальный, открытый, нативный для Windows помощник для встреч.",
|
||||||
|
"settings.about.build_commit": "Коммит сборки",
|
||||||
|
|
||||||
|
"consent.heading": "Прежде чем записывать",
|
||||||
|
"consent.body": "Запись разговоров без согласия участников может быть незаконной в вашем регионе. Проверьте местные законы о записи. Это предупреждение, а не юридическая консультация.",
|
||||||
|
"consent.accept": "Понятно — включить запись",
|
||||||
|
"consent.cancel": "Отмена",
|
||||||
|
|
||||||
|
"hosted.heading": "Прежде чем использовать {provider}",
|
||||||
|
"hosted.body_1": "Генерация с помощью",
|
||||||
|
"hosted.body_2": "отправляет транскрипцию этой встречи на их серверы — она покидает это устройство и подпадает под их политику конфиденциальности. WhispAssist не контролирует обработку данных после того, как они покидают ваше устройство. Это выключено по умолчанию; вы выбираете это сейчас.",
|
||||||
|
"hosted.accept": "Понятно — продолжить",
|
||||||
|
"hosted.cancel": "Отмена",
|
||||||
|
|
||||||
|
"theme.group": "Тема",
|
||||||
|
"theme.system": "Как в системе",
|
||||||
|
"theme.light": "Светлая",
|
||||||
|
"theme.dark": "Тёмная",
|
||||||
|
|
||||||
|
"import.dialog_label": "Добавить встречу из файла или URL",
|
||||||
|
"import.title": "Добавить встречу",
|
||||||
|
"import.close": "Закрыть",
|
||||||
|
"import.close_title": "Закрыть (Esc)",
|
||||||
|
"import.body": "Импортируйте существующую запись — локальный аудио-/видеофайл или ссылку (YouTube, страницу потоковой трансляции или прямой URL медиа). Она транскрибируется и разделяется по дикторам так же, как живая запись.",
|
||||||
|
"import.file_or_url": "Файл или URL",
|
||||||
|
"import.source_placeholder": "Вставьте URL или найдите файл…",
|
||||||
|
"import.choose_file": "Выбрать локальный файл",
|
||||||
|
"import.browse": "Обзор…",
|
||||||
|
"import.title_label": "Название",
|
||||||
|
"import.optional": "необязательно",
|
||||||
|
"import.title_placeholder": "По умолчанию — имя файла",
|
||||||
|
"import.filter_av": "Аудио / видео",
|
||||||
|
"import.requires_1": "Требует",
|
||||||
|
"import.requires_2": "установленным и в вашем PATH (плюс",
|
||||||
|
"import.requires_3": "для URL). WhispAssist их не включает.",
|
||||||
|
"import.importing": "Импорт… это может занять некоторое время",
|
||||||
|
"import.import": "Импортировать",
|
||||||
|
"import.cancel": "Отмена",
|
||||||
|
|
||||||
|
"tagchip.filter": "Фильтровать встречи с меткой «{tag}»",
|
||||||
|
"tagchip.remove": "Удалить метку {tag}",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "Уровень входа системы и микрофона",
|
||||||
|
"levelmeter.system": "Уровень входа системы",
|
||||||
|
|
||||||
|
"app.tagline": "локально · приватно",
|
||||||
|
"app.note_template": "Шаблон заметок",
|
||||||
|
"app.no_template": "Без шаблона",
|
||||||
|
"app.add_meeting": "Добавить встречу",
|
||||||
|
"app.add_meeting_title": "Добавить встречу из файла или URL",
|
||||||
|
"app.record": "Запись",
|
||||||
|
"app.record_title": "Начать запись (Ctrl+Shift+R)",
|
||||||
|
"app.stop": "Стоп",
|
||||||
|
"app.stop_title": "Остановить запись (Ctrl+Shift+R)",
|
||||||
|
"app.cancel": "Отмена",
|
||||||
|
"app.cancel_title": "Отклонить эту запись и удалить её",
|
||||||
|
"app.recording": "Запись…",
|
||||||
|
"app.backend_title": "Активный бэкенд транскрипции",
|
||||||
|
"app.retention_title": "Сохранить аудио как .wav для этой встречи",
|
||||||
|
"app.saving": "сохранение",
|
||||||
|
"app.not_saved": "не сохранено",
|
||||||
|
"app.reconnecting": "переподключение аудиоустройства…",
|
||||||
|
"app.settings": "Настройки",
|
||||||
|
"app.settings_title": "Настройки (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "Отклонить эту запись? Её аудио и транскрипция будут удалены.",
|
||||||
|
"app.announce_started": "Запись начата",
|
||||||
|
"app.announce_paused": "Запись приостановлена",
|
||||||
|
"app.announce_stopped": "Запись остановлена",
|
||||||
|
"app.consent_dialog": "Согласие на запись",
|
||||||
|
"app.vault_dialog": "Разблокировать хранилище шифрования",
|
||||||
|
"app.vault_title": "Разблокировать хранилище шифрования",
|
||||||
|
"app.vault_desc": "Ваши встречи зашифрованы в состоянии покоя. Введите пароль, чтобы прочитать их.",
|
||||||
|
"app.vault_password": "Пароль хранилища",
|
||||||
|
"app.vault_incorrect": "Неверный пароль",
|
||||||
|
"app.unlock": "Разблокировать",
|
||||||
|
"app.continue_locked": "Продолжить заблокированным",
|
||||||
|
"app.show_meetings": "Показать список встреч",
|
||||||
|
"app.hide_meetings": "Скрыть список встреч",
|
||||||
|
"app.show_summary": "Показать панель сводки",
|
||||||
|
"app.hide_summary": "Скрыть панель сводки",
|
||||||
|
"app.resize_meetings": "Изменить размер списка встреч",
|
||||||
|
"app.resize_summary": "Изменить размер панели сводки",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "Поиск встреч…",
|
||||||
|
"meetings.search_aria": "Поиск встреч",
|
||||||
|
"meetings.filter_tag_aria": "Фильтровать по метке",
|
||||||
|
"meetings.all_tags": "Все метки",
|
||||||
|
"meetings.from_date_aria": "С даты",
|
||||||
|
"meetings.to_date_aria": "По дату",
|
||||||
|
"meetings.bulk_format_aria": "Формат массового экспорта",
|
||||||
|
"meetings.bulk_export": "Массовый экспорт",
|
||||||
|
"meetings.bulk_export_title": "Экспортировать все встречи, соответствующие фильтрам метки/даты выше",
|
||||||
|
"meetings.exporting": "Экспорт…",
|
||||||
|
"meetings.exported_one": "Экспортирована {n} встреча.",
|
||||||
|
"meetings.exported_many": "Экспортировано встреч: {n}.",
|
||||||
|
"meetings.searching": "Поиск…",
|
||||||
|
"meetings.loading": "Загрузка встреч…",
|
||||||
|
"meetings.no_matches": "Нет совпадений.",
|
||||||
|
"meetings.empty": "Встреч пока нет — нажмите «Запись» выше, чтобы начать.",
|
||||||
|
"meetings.status.recording": "запись",
|
||||||
|
"meetings.status.transcribing": "транскрибирование",
|
||||||
|
"meetings.status.recovering": "восстановление",
|
||||||
|
"meetings.status.error": "ошибка",
|
||||||
|
"meetings.resume": "Возобновить транскрипцию",
|
||||||
|
"meetings.delete_aria": "Удалить встречу",
|
||||||
|
"meetings.delete_confirm": "Удалить эту встречу? Это удалит её запись, транскрипцию и заметки.",
|
||||||
|
|
||||||
|
"transcript.heading": "Транскрипция",
|
||||||
|
"transcript.title_aria": "Название встречи",
|
||||||
|
"transcript.lang_title": "Язык транскрипции",
|
||||||
|
"transcript.lang_auto": "определение…",
|
||||||
|
"transcript.show": "Показать транскрипцию",
|
||||||
|
"transcript.hide": "Скрыть транскрипцию",
|
||||||
|
"transcript.empty": "Для этой встречи нет транскрипции.",
|
||||||
|
"transcript.will_appear": "Транскрипция будет появляться здесь по мере записи.",
|
||||||
|
"transcript.play_from_here": "Воспроизвести отсюда",
|
||||||
|
"transcript.resize": "Изменить размер транскрипции и заметок",
|
||||||
|
"transcript.reprocess_placeholder": "Перетранскрибировать с…",
|
||||||
|
"transcript.reprocess_lang_aria": "Язык повторной обработки",
|
||||||
|
"transcript.reprocess_keep_lang": "Сохранить текущий язык",
|
||||||
|
"transcript.reprocess_go": "Выполнить",
|
||||||
|
"transcript.retranscribing": "Повторная транскрипция…",
|
||||||
|
"transcript.has_note": "Есть заметка",
|
||||||
|
"transcript.note_placeholder": "Добавить заметку для этого момента…",
|
||||||
|
"transcript.note_aria": "Заметка для этой строки транскрипции",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Документ Word",
|
||||||
|
"transcript.filter_obsidian": "Заметка Obsidian",
|
||||||
|
|
||||||
|
"notes.heading": "Заметки",
|
||||||
|
"notes.show": "Показать заметки",
|
||||||
|
"notes.hide": "Скрыть заметки",
|
||||||
|
"notes.placeholder": "Заметки…",
|
||||||
|
"notes.live_placeholder": "Печатайте заметки, пока говорите…",
|
||||||
|
"notes.toolbar_aria": "Форматирование заметок",
|
||||||
|
"notes.bold": "Полужирный",
|
||||||
|
"notes.italic": "Курсив",
|
||||||
|
"notes.h1": "Заголовок 1",
|
||||||
|
"notes.h2": "Заголовок 2",
|
||||||
|
"notes.bullet": "Маркированный список",
|
||||||
|
"notes.checkbox_title": "Флажок",
|
||||||
|
"notes.checkbox_aria": "Элемент списка с флажком",
|
||||||
|
"notes.edit_raw": "Редактировать исходный markdown",
|
||||||
|
"notes.render": "Отобразить markdown",
|
||||||
|
"notes.editor": "Редактор",
|
||||||
|
"notes.preview": "Предпросмотр",
|
||||||
|
"notes.export_md_title": "Экспортировать заметки как .md",
|
||||||
|
"notes.export_pdf_title": "Экспортировать заметки как .pdf",
|
||||||
|
"notes.export_docx_title": "Экспортировать заметки как .docx",
|
||||||
|
"notes.export_bundle_title": "Экспортировать аудио + транскрипцию + заметки в папку",
|
||||||
|
"notes.export_obsidian_title": "Экспортировать одну заметку Obsidian (заметки + сводка + транскрипция, без аудио)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "Запись",
|
||||||
|
"summary.loading_recording": "Загрузка записи…",
|
||||||
|
"summary.sync_heading": "Синхронизация",
|
||||||
|
"summary.uploading": "Выгрузка…",
|
||||||
|
"summary.upload_now": "Выгрузить сейчас",
|
||||||
|
"summary.no_uploads": "Для этой встречи ещё нет выгрузок.",
|
||||||
|
"summary.retry": "повторить",
|
||||||
|
"summary.tags_heading": "Метки",
|
||||||
|
"summary.tags_select": "Выберите встречу, чтобы пометить её.",
|
||||||
|
"summary.tag_add_placeholder": "Добавить метку…",
|
||||||
|
"summary.tag_first_placeholder": "проект, клиент, тема…",
|
||||||
|
"summary.generating": "Генерация…",
|
||||||
|
"summary.generate_tags": "Сгенерировать метки",
|
||||||
|
"summary.saving": "Сохранение…",
|
||||||
|
"summary.save_tags": "Сохранить метки",
|
||||||
|
"summary.summary_heading": "Сводка",
|
||||||
|
"summary.ai_provider": "Провайдер ИИ",
|
||||||
|
"summary.provider_off": "Выкл.",
|
||||||
|
"summary.provider_custom": "Пользовательский",
|
||||||
|
"summary.local": "локально",
|
||||||
|
"summary.leaves_device": "покидает это устройство",
|
||||||
|
"summary.this_hosted_provider": "этот размещённый провайдер",
|
||||||
|
"summary.select_generate": "Выберите встречу, чтобы сгенерировать сводку.",
|
||||||
|
"summary.decisions_heading": "Решения",
|
||||||
|
"summary.regenerate": "Перегенерировать",
|
||||||
|
"summary.generated_hint": "Сгенерировано локально после встречи (требуется локальный провайдер LLM).",
|
||||||
|
"summary.no_provider": "Провайдер ИИ не настроен — включите его в Настройках.",
|
||||||
|
"summary.generate_summary": "Сгенерировать сводку",
|
||||||
|
"summary.briefs_heading": "Брифы функций",
|
||||||
|
"summary.briefs_select": "Выберите встречу, чтобы создать или просмотреть брифы функций.",
|
||||||
|
"summary.briefs_desc": "Дистиллирует эту встречу в готовую для агента спецификацию (требуется локальный провайдер LLM) — передайте её кодовому агенту или предоставьте через MCP-сервер, когда он включён.",
|
||||||
|
"summary.brief_repo_placeholder": "Целевой репозиторий (необязательно), напр. acme/reporting-web",
|
||||||
|
"summary.distilling": "Дистилляция…",
|
||||||
|
"summary.create_brief": "Создать бриф функции",
|
||||||
|
"summary.brief_mcp_title": "Доступно, когда MCP-сервер включён",
|
||||||
|
"summary.copied": "Скопировано",
|
||||||
|
"summary.copy_md": "Копировать как Markdown",
|
||||||
|
"summary.copy_json": "Копировать как JSON",
|
||||||
|
"summary.brief_problem": "Проблема",
|
||||||
|
"summary.brief_outcome": "Желаемый результат",
|
||||||
|
"summary.brief_criteria": "Критерии приёмки",
|
||||||
|
"summary.brief_none": "Ничего не зафиксировано.",
|
||||||
|
"summary.brief_context": "Контекст",
|
||||||
|
"summary.action_items_heading": "Задачи",
|
||||||
|
"summary.ai_select": "Выберите встречу, чтобы увидеть её задачи.",
|
||||||
|
"summary.ai_empty": "Пока нет — добавьте одну ниже или сгенерируйте сводку, чтобы извлечь их автоматически.",
|
||||||
|
"summary.confirmed": "Подтверждено",
|
||||||
|
"summary.ai_text_placeholder": "Задача…",
|
||||||
|
"summary.ai_text_aria": "Текст задачи",
|
||||||
|
"summary.owner": "Ответственный",
|
||||||
|
"summary.due_date": "Срок",
|
||||||
|
"summary.reminder_title": "Запланировать локальное напоминание на этот срок",
|
||||||
|
"summary.ai_delete_title": "Удалить эту задачу",
|
||||||
|
"summary.ai_delete_aria": "Удалить задачу",
|
||||||
|
"summary.add_action_item": "Добавить задачу",
|
||||||
|
"summary.save_action_items": "Сохранить задачи",
|
||||||
|
"summary.calendar_heading": "Событие календаря",
|
||||||
|
"summary.cal_select": "Выберите встречу, чтобы связать её с событием календаря.",
|
||||||
|
"summary.event_search_placeholder": "Поиск названия события…",
|
||||||
|
"summary.linked_event": "Связанное событие",
|
||||||
|
"summary.change_event": "Сменить событие…",
|
||||||
|
"summary.link_event": "Связать событие…",
|
||||||
|
"summary.untitled_event": "(без названия)",
|
||||||
|
"summary.no_events_1": "События ещё не импортированы — импортируйте",
|
||||||
|
"summary.no_events_2": "в Настройки → Календарь.",
|
||||||
|
"summary.no_events_match": "Ни одно событие не соответствует этому поиску/дате — попробуйте очистить одно.",
|
||||||
|
"summary.organizer_label": "Организатор: {name}",
|
||||||
|
"summary.participants_heading": "Участники",
|
||||||
|
"summary.participants_hint": "Заполнено из связанного события календаря.",
|
||||||
|
"summary.speakers_heading": "Дикторы",
|
||||||
|
"summary.speakers_select": "Выберите встречу, чтобы назвать её дикторов.",
|
||||||
|
"summary.no_speakers": "Дикторы пока не обнаружены.",
|
||||||
|
"summary.speaker_name_placeholder": "Имя диктора",
|
||||||
|
"summary.save": "Сохранить",
|
||||||
|
"summary.cancel": "Отмена",
|
||||||
|
"summary.name_speaker": "Назвать этого диктора…",
|
||||||
|
"summary.add_new_name": "+ Добавить новое имя…",
|
||||||
|
|
||||||
|
"settings.recording.title": "Запись",
|
||||||
|
"settings.recording.record_default": "Записывать встречи по умолчанию",
|
||||||
|
"settings.recording.save_audio_as": "сохранять аудио как",
|
||||||
|
"settings.recording.default_hint": "Выключено по умолчанию. Когда выключено, аудио используется только для получения транскрипции и удаляется при завершении встречи. Вы также можете переключать запись для каждой встречи.",
|
||||||
|
"settings.recording.consent_label": "Согласие: {status}.",
|
||||||
|
"settings.recording.consent_ack": "подтверждено",
|
||||||
|
"settings.recording.consent_not": "ещё не подтверждено",
|
||||||
|
"settings.recording.auto_label": "Автоматически начинать запись, когда начинается событие календаря",
|
||||||
|
"settings.recording.auto_hint": "Только пока WhispAssist открыт. Когда наступает время начала импортированного события календаря, запись начинается автоматически (используя вашу настройку хранения по умолчанию выше). Ничего не выполняется в фоне — таймер взведён только пока приложение запущено. Импортируйте события в Настройки → Календарь."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "පටිගත කිරීම",
|
||||||
|
"nav.hardware": "දෘඪාංග",
|
||||||
|
"nav.storage": "ගබඩාව",
|
||||||
|
"nav.calendar": "දින දර්ශනය",
|
||||||
|
"nav.sync": "සමමුහුර්තකරණය",
|
||||||
|
"nav.ai": "AI",
|
||||||
|
"nav.mcp": "MCP සේවාදායකය",
|
||||||
|
"nav.privacy": "පෞද්ගලිකත්වය",
|
||||||
|
"nav.about": "පිළිබඳව",
|
||||||
|
"nav.language": "භාෂාව",
|
||||||
|
|
||||||
|
"settings.language.title": "භාෂාව",
|
||||||
|
"settings.language.display": "දර්ශන භාෂාව",
|
||||||
|
"settings.language.display_hint": "යෙදුම් අතුරුමුහුණතේ භාෂාව. භාෂාවක් එක් කිරීම පරිවර්තන ගොනුවක් එක් කිරීම තරම්ම පහසුය.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "පිටපත් කිරීමේ භාෂාව",
|
||||||
|
"settings.transcription.label": "භාෂාව",
|
||||||
|
"settings.transcription.auto": "ස්වයංක්රීයව හඳුනාගන්න",
|
||||||
|
"settings.transcription.applies_hint": "ඊළඟ පටිගත කිරීමට අදාළ වේ. සැබවින්ම භාවිත කළ භාෂාව පසුව සෑම රැස්වීමකම පෙන්වයි.",
|
||||||
|
"settings.transcription.english_only": "ඉංග්රීසි පමණි",
|
||||||
|
"settings.transcription.model_english_only": "“{model}” ඉංග්රීසි පමණි.",
|
||||||
|
"settings.transcription.no_model": "කිසිදු ආකෘතියක් තෝරා නැත.",
|
||||||
|
"settings.transcription.switch_multilingual": "භාෂාවක් තේරීමට ඉහත බහුභාෂා ආකෘතියකට මාරු වන්න.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "දෘඪාංග",
|
||||||
|
"settings.hardware.active_backend": "සක්රීය පසුඅන්තය",
|
||||||
|
"settings.hardware.model_meta": "· ආකෘතිය {size}",
|
||||||
|
"settings.hardware.preferred_backend": "කැමති පසුඅන්තය",
|
||||||
|
"settings.hardware.auto_backend": "ස්වයංක්රීය (හොඳම ලද හැකි)",
|
||||||
|
"settings.hardware.not_available": "ලද නොහැක",
|
||||||
|
"settings.hardware.fallback_hint": "තෝරාගත් පසුඅන්තය පූරණය නොවුනහොත් ස්වයංක්රීයව ආපසු වැටේ (NPU → NVIDIA → AMD → Intel → CPU).",
|
||||||
|
"settings.hardware.audio_devices": "ශ්රව්ය උපාංග",
|
||||||
|
"settings.hardware.recording_device": "පටිගත කිරීමේ උපාංගය",
|
||||||
|
"settings.hardware.default_system_audio": "පෙරනිමි පද්ධති ශ්රව්ය",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist මෙම උපාංගය වාදනය කරන දේ (ලූප්බැක්) පටිගත කරයි — ඇමතුමේ අනෙක් පැත්ත. Windows පද්ධති පෙරනිමිය අනුගමනය කිරීමට ඔබ අකැමති නම් නිශ්චිත ප්රතිදානයක් තෝරන්න.",
|
||||||
|
"settings.hardware.microphone": "මයික්රොෆෝනය",
|
||||||
|
"settings.hardware.mic_off": "අක්රිය — මගේ මයික්රොෆෝනය ග්රහණය නොකරන්න",
|
||||||
|
"settings.hardware.default_mic": "පෙරනිමි මයික්රොෆෝනය",
|
||||||
|
"settings.hardware.mic_hint": "රැස්වීමේ දෙපැත්තම ග්රහණය වන පරිදි සජීවී පිටපතට ඔබේම හඬ එක් කරයි. ඔබේ උපාංගයේ පවතී — කිසිවක් උඩුගත නොවේ. ඉහත පද්ධති ශ්රව්ය පමණක් පිටපත් කිරීමට “අක්රිය” තෝරන්න.",
|
||||||
|
"settings.hardware.npu_detected": "NPU ත්වරකය හඳුනාගන්නා ලදී",
|
||||||
|
"settings.hardware.ready": "සූදානම්",
|
||||||
|
"settings.hardware.downloading": "බාගත වෙමින්…",
|
||||||
|
"settings.hardware.package_needed": "පැකේජය අවශ්යයි",
|
||||||
|
"settings.hardware.npu_hint": "ඔබේ NPU හට එක්වරක් ත්වරණ පැකේජයක් (Whisper ONNX ආකෘතිය{extra}) අවශ්යයි. එය පසුබිමින් බාගත වේ; ඔබට එය දැන්ම ආරම්භ කළ හැක.",
|
||||||
|
"settings.hardware.plus_openvino": " + OpenVINO ධාවන කාලය",
|
||||||
|
"settings.hardware.download_npu": "NPU පැකේජය බාගන්න",
|
||||||
|
"settings.hardware.dml_title": "GPU ත්වරණය (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "ඔබේ GPU Vulkan නොමැතිව DirectML හරහා ත්වරණය කළ හැක — එක්වරක් පැකේජයක් (Whisper ONNX ආකෘතිය{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + DirectML ධාවන කාලය",
|
||||||
|
"settings.hardware.download_dml": "DirectML පැකේජය බාගන්න",
|
||||||
|
"settings.hardware.detection_unavailable": "දෘඪාංග හඳුනාගැනීම ලද නොහැක.",
|
||||||
|
"settings.hardware.low_overhead": "අඩු පොදු කාර්ය පෙරසැකසුම",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + කුඩාම ආකෘතිය, බැටරි/පසුබිම් භාවිතය සඳහා",
|
||||||
|
"settings.hardware.models": "ආකෘති",
|
||||||
|
"settings.hardware.downloading_model": "{name} බාගත වෙමින්",
|
||||||
|
"settings.hardware.active_badge": "සක්රීය",
|
||||||
|
"settings.hardware.installed": "ස්ථාපනය කර ඇත",
|
||||||
|
"settings.hardware.use": "භාවිත කරන්න",
|
||||||
|
"settings.hardware.remove": "ඉවත් කරන්න",
|
||||||
|
"settings.hardware.cant_remove_active": "සක්රීය ආකෘතිය ඉවත් කළ නොහැක",
|
||||||
|
"settings.hardware.download": "බාගන්න",
|
||||||
|
"settings.hardware.diarization": "කථික වෙන් කිරීම",
|
||||||
|
"settings.hardware.diarization_hint": "සම්පූර්ණ කළ පටිගත කිරීම් එක් අඛණ්ඩ පිටපතක් වෙනුවට කථිකයා අනුව (කථික 1, කථික 2, …) වෙන් වන පරිදි ආකෘති දෙකම ස්ථාපනය කරන්න. සම්පූර්ණයෙන් නොබැඳිව ධාවනය වේ, නැවතීමෙන් පසු රැස්වීමකට වරක්. ඒවා නොමැතිව, සෑම පේළියක්ම එකම කථිකයෙකුට ආරෝපණය වේ.",
|
||||||
|
|
||||||
|
"settings.storage.title": "ගබඩාව",
|
||||||
|
"settings.storage.location_label": "ස්ථානය",
|
||||||
|
"settings.storage.location_hint": "ගබඩා ස්ථානය වෙනස් කිරීම තවම සහාය නොදක්වයි — දැනට රැස්වීම්, ආකෘති සහ දත්ත සමුදාය පවතින්නේ මෙතැනයි.",
|
||||||
|
"settings.storage.retention_title": "රඳවා තබා ගැනීම",
|
||||||
|
"settings.storage.retention_hint": "සීමාවකට වඩා පැරණිම රැස්වීම් ස්වයංක්රීයව මකයි. ආරම්භයේදී වරක් පරීක්ෂා කරයි; හිස් යනු සීමාවක් නැත. දැනට පටිගත වන රැස්වීමකට කිසිවිටෙක ස්පර්ශ නොකරයි.",
|
||||||
|
"settings.storage.max_age": "උපරිම වයස (දින)",
|
||||||
|
"settings.storage.max_size": "උපරිම ප්රමාණය (GB)",
|
||||||
|
"settings.storage.no_limit": "සීමාවක් නැත",
|
||||||
|
"settings.storage.export_import_title": "නිර්යාත සහ ආයාත",
|
||||||
|
"settings.storage.export_hint_1": "පටිගත කිරීම් පරිගණක අතර ගෙනයා හැකි පැකේජ ෆෝල්ඩර ලෙස ගෙන යන්න (ශ්රව්ය, පිටපත, සටහන්, සාරාංශය සහ",
|
||||||
|
"settings.storage.export_hint_2": "මැනිෆෙස්ට් එකක්). ඕනෑම ෆෝල්ඩරයකට නිර්යාත කරන්න — සමමුහුර්ත ධාවකයක්, USB ස්ටික් එකක්, හෝ සමමුහුර්ත ඉලක්කයක දේශීය සවි කිරීමකට — පසුව එය අනෙක් යන්ත්රයේ ආයාත කරන්න. ආයාත කළ රැස්වීම්වලට නව id ලැබේ, එබැවින් නැවත ආයාත කිරීම කිසිවිටෙක කිසිවක් උඩින් ලියන්නේ නැත.",
|
||||||
|
"settings.storage.export_dialog_title": "සියලුම රැස්වීම් මෙතැනට නිර්යාත කරන්න…",
|
||||||
|
"settings.storage.import_dialog_title": "පැකේජයක් (හෝ පැකේජ ෆෝල්ඩරයක්) ආයාත කරන්න…",
|
||||||
|
"settings.storage.exporting": "නිර්යාත වෙමින්…",
|
||||||
|
"settings.storage.export_all": "සියලුම රැස්වීම් නිර්යාත කරන්න…",
|
||||||
|
"settings.storage.importing": "ආයාත වෙමින්…",
|
||||||
|
"settings.storage.import_all": "රැස්වීම් ආයාත කරන්න…",
|
||||||
|
"settings.storage.exported_one": "රැස්වීම් {n}ක් නිර්යාත කරන ලදී.",
|
||||||
|
"settings.storage.exported_many": "රැස්වීම් {n}ක් නිර්යාත කරන ලදී.",
|
||||||
|
"settings.storage.imported_one": "රැස්වීම් {n}ක් ආයාත කරන ලදී.",
|
||||||
|
"settings.storage.imported_many": "රැස්වීම් {n}ක් ආයාත කරන ලදී.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "දින දර්ශනය සහ Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "දේශීය Outlook උපස්ථයකින් සිදුවීම් සහ සහභාගිවන්නන් ආයාත කරන්න",
|
||||||
|
"settings.calendar.intro_2": "— කියවීමට පමණි, ගොනුවට කිසිවක් නැවත ලියන්නේ නැත. මෙම උපාංගයෙන් කිසිවක් පිටතට යන්නේ නැත.",
|
||||||
|
"settings.calendar.pst_file": ".pst ගොනුව",
|
||||||
|
"settings.calendar.no_file": "ගොනුවක් තෝරා නැත",
|
||||||
|
"settings.calendar.browse": "බ්රවුස් කරන්න…",
|
||||||
|
"settings.calendar.password": "මුරපදය",
|
||||||
|
"settings.calendar.password_note": "කලාතුරකින් අවශ්යයි",
|
||||||
|
"settings.calendar.optional": "විකල්ප",
|
||||||
|
"settings.calendar.import_range": "ආයාත පරාසය",
|
||||||
|
"settings.calendar.range_30": "පසුගිය දින 30",
|
||||||
|
"settings.calendar.range_90": "පසුගිය දින 90",
|
||||||
|
"settings.calendar.range_180": "පසුගිය මාස 6",
|
||||||
|
"settings.calendar.range_365": "පසුගිය වර්ෂ 1",
|
||||||
|
"settings.calendar.range_all": "සියලු කාලය",
|
||||||
|
"settings.calendar.range_hint": "දිගු කලක් පවතින තැපැල් පෙට්ටියක වසර ගණනක පුනරාවර්තන/නිවාඩු ඇතුළත් කිරීම් තිබිය හැක — පරාසය පටු කිරීම ආයාත කළ දින දර්ශනය සැබවින්ම අදාළ දෑට පමණක් තබා ගනී. මෙය මෙම ආයාත බොත්තමට මෙන්ම දියත් කිරීමේදී ස්වයංක්රීය නැවත ආයාතයට ද අදාළ වේ.",
|
||||||
|
"settings.calendar.importing": "ආයාත වෙමින්…",
|
||||||
|
"settings.calendar.import": "ආයාත කරන්න",
|
||||||
|
"settings.calendar.import_progress": "සිදුවීම් {processed}/{total}",
|
||||||
|
"settings.calendar.auto_reimport": "දියත් කිරීමේදී මෙම ගොනුව ස්වයංක්රීයව නැවත ආයාත කරන්න",
|
||||||
|
"settings.calendar.auto_reimport_hint": "කාල නියාමකයක් මත නොව, ආරම්භයේදී වරක් ධාවනය වේ — නැවත ආයාත කිරීම ආරක්ෂිතව නැවත කළ හැක (පවතින සිදුවීම් ගැලපී යාවත්කාලීන වේ, අනුපිටපත් නොවේ).",
|
||||||
|
"settings.calendar.import_failed": "ආයාතය අසාර්ථකයි: {error} — ගොනුවම ස්පර්ශ නොකෙරේ; මාර්ගය පරීක්ෂා කර නැවත උත්සාහ කරන්න.",
|
||||||
|
"settings.calendar.cleanup_title": "පිරිසිදු කරන්න",
|
||||||
|
"settings.calendar.cleanup_hint": "පටිගත කළ රැස්වීමකට දැනටමත් අමුණා ඇති සිදුවීම්, පහත ඕනෑම විකල්පයක් තෝරාගත්තද, සැමවිටම තබා ගැනේ.",
|
||||||
|
"settings.calendar.cleanup_30": "දින 30කට වඩා පැරණි",
|
||||||
|
"settings.calendar.cleanup_90": "දින 90කට වඩා පැරණි",
|
||||||
|
"settings.calendar.cleanup_180": "මාස 6කට වඩා පැරණි",
|
||||||
|
"settings.calendar.cleanup_365": "වර්ෂ 1කට වඩා පැරණි",
|
||||||
|
"settings.calendar.cleanup_all": "සියල්ල මකන්න",
|
||||||
|
"settings.calendar.cleaning": "පිරිසිදු වෙමින්…",
|
||||||
|
"settings.calendar.cleanup_btn": "දින දර්ශනය පිරිසිදු කරන්න",
|
||||||
|
"settings.calendar.cleanup_result": "{deleted}{extra} මකන ලදී.",
|
||||||
|
"settings.calendar.cleanup_kept": ", {n}ක් තබා ගන්නා ලදී (රැස්වීමකට සම්බන්ධ)",
|
||||||
|
"settings.calendar.cleanup_failed": "පිරිසිදු කිරීම අසාර්ථකයි: {error}",
|
||||||
|
"settings.calendar.imported_events": "ආයාත කළ සිදුවීම්",
|
||||||
|
"settings.calendar.no_events": "තවම සිදුවීම් ආයාත කර නැත.",
|
||||||
|
"settings.calendar.search_title": "මාතෘකාව සොයන්න",
|
||||||
|
"settings.calendar.search_placeholder": "රැස්වීමේ නම…",
|
||||||
|
"settings.calendar.date_label": "දිනය",
|
||||||
|
"settings.calendar.events_count": "සිදුවීම් {total}න් {shown}ක්",
|
||||||
|
"settings.calendar.untitled": "(මාතෘකා රහිත)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "සැකසුම්",
|
||||||
|
"settings.close": "වසන්න",
|
||||||
|
"settings.close_title": "වසන්න (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "පෙරදසුන් ප්රකාරය — සමමුහුර්ත පසුඅන්තය තවම ක්රියාත්මක කර නැත (අදියර 9). වෙනස්කම් අතුරුමුහුණතේ පමණක් තබා ගැනේ.",
|
||||||
|
"settings.sync.title": "සමමුහුර්තකරණය සහ උඩුගත කිරීම",
|
||||||
|
"settings.sync.enable_label": "වින්යාස කළ ඉලක්ක වෙත රැස්වීම් අංග උඩුගත කිරීම සක්රීය කරන්න",
|
||||||
|
"settings.sync.enable_hint": "පෙරනිමියෙන් අක්රියයි. මෙය සක්රීය වී ඉලක්කයක් සක්රීය නොවන තෙක් කිසිවක් උඩුගත නොවේ. ස්වයං-සත්කාරක ඉලක්ක දත්ත ඔබේම සේවාදායකයේ තබා ගනී; තෙවන පාර්ශව වලාකුළු පැහැදිලිව ලේබල් කර ඇත.",
|
||||||
|
"settings.sync.targets_title": "ඉලක්ක",
|
||||||
|
"settings.sync.no_targets": "තවම ඉලක්ක නැත. පහත එකක් එක් කරන්න.",
|
||||||
|
"settings.sync.third_party": "තෙවන පාර්ශව",
|
||||||
|
"settings.sync.your_server": "ඔබේ සේවාදායකය",
|
||||||
|
"settings.sync.edit": "සංස්කරණය කරන්න",
|
||||||
|
"settings.sync.remove": "ඉවත් කරන්න",
|
||||||
|
"settings.sync.edit_target": "ඉලක්කය සංස්කරණය කරන්න",
|
||||||
|
"settings.sync.add_target": "ඉලක්කයක් එක් කරන්න",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV මගින් Nextcloud, ownCloud, Cloudreve, Seafile සහ Synology ආවරණය කරයි. (Seafile: සේවාදායක පැත්තේ SeafDAV සක්රීය කරන්න.)",
|
||||||
|
"settings.sync.name": "නම",
|
||||||
|
"settings.sync.name_placeholder": "නිවසේ Nextcloud",
|
||||||
|
"settings.sync.provider": "සැපයුම්කරු",
|
||||||
|
"settings.sync.server_url": "සේවාදායක URL",
|
||||||
|
"settings.sync.server_url_note_1": "ඔබේ සේවාදායක URL පමණි — WhispAssist ස්වයංක්රීයව",
|
||||||
|
"settings.sync.server_url_note_2": "එක් කරයි.",
|
||||||
|
"settings.sync.remote_folder": "දුරස්ථ ෆෝල්ඩරය",
|
||||||
|
"settings.sync.username": "පරිශීලක නාමය",
|
||||||
|
"settings.sync.app_password": "යෙදුම් මුරපදය",
|
||||||
|
"settings.sync.pw_keep": "වත්මන් මුරපදය තබා ගැනීමට හිස්ව තබන්න",
|
||||||
|
"settings.sync.pw_store": "OS අක්තපත්ර ගබඩාවේ ගබඩා කර ඇත",
|
||||||
|
"settings.sync.upload_legend": "උඩුගත කරන්න",
|
||||||
|
"settings.sync.artifact_transcript": "පිටපත",
|
||||||
|
"settings.sync.artifact_notes": "සටහන්",
|
||||||
|
"settings.sync.artifact_summary": "සාරාංශය",
|
||||||
|
"settings.sync.artifact_recording": "පටිගත කිරීම (.wav, රඳවා තබා ගත්තේ නම්)",
|
||||||
|
"settings.sync.allow_plaintext": "LAN ලිපිනයකට සරල පෙළ http ඉඩ දෙන්න (නිර්දේශ නොකෙරේ)",
|
||||||
|
"settings.sync.encrypt_before": "උඩුගත කිරීමට පෙර සංකේතනය කරන්න (ගමනාන්තය සංකේත පෙළ පමණක් ගබඩා කරයි)",
|
||||||
|
"settings.sync.requires_vault": "— අගුළු හළ සුරක්ෂිතාගාරයක් අවශ්යයි",
|
||||||
|
"settings.sync.test_connection": "සම්බන්ධතාවය පරීක්ෂා කරන්න",
|
||||||
|
"settings.sync.save_changes": "වෙනස්කම් සුරකින්න",
|
||||||
|
"settings.sync.add_target_btn": "ඉලක්කය එක් කරන්න",
|
||||||
|
"settings.sync.cancel": "අවලංගු කරන්න",
|
||||||
|
"settings.sync.third_party_banner": "{kind} යනු තෙවන පාර්ශව වලාකුළකි — උඩුගත කිරීම ඔබේ දත්ත ඔබේ උපාංගයෙන් පිටතට {kind} වෙත යවයි.",
|
||||||
|
"settings.sync.link_account": "{kind} ගිණුම සම්බන්ධ කරන්න…",
|
||||||
|
"settings.sync.oauth_hint": "OAuth පිවිසුමක් විවෘත කරයි (ලූප්බැක් යළි-යොමුව); ටෝකනය ඔබේ OS අක්තපත්ර ගබඩාවේ ගබඩා වේ.",
|
||||||
|
"settings.sync.footnote": "අක්තපත්ර කිසිවිටෙක සැකසුම් හෝ දත්ත සමුදායට ලියන්නේ නැත — OS අක්තපත්ර ගබඩාවට පමණි. LAN නොවන ඉලක්ක සඳහා TLS අවශ්යයි.",
|
||||||
|
|
||||||
|
"settings.ai.title": "AI සාරාංශ සැපයුම්කරු",
|
||||||
|
"settings.ai.intro_1": "සාරාංශ පෙරනිමියෙන් දේශීය LLM එකක ධාවනය වේ. මෙය මෙම PC හෝ ඔබේ LAN හි වෙනත් යන්ත්රයක Ollama වෙත යොමු කරන්න (උදා.",
|
||||||
|
"settings.ai.intro_2": "— දෙකම දේශීය ලෙස ගණන් ගැනේ, එබැවින් ඔබේ ජාලයෙන් කිසිවක් පිටතට යන්නේ නැත. සත්කාරක සැපයුම්කරුවන් (Anthropic) විකල්ප වන අතර, පෙරනිමියෙන් අක්රියයි, ඔබ එකක් සක්රීය කළ විගස පිටපත තෙවන පාර්ශවයකට යවයි.",
|
||||||
|
"settings.ai.provider": "සැපයුම්කරු",
|
||||||
|
"settings.ai.off": "අක්රිය",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (දේශීය / LAN)",
|
||||||
|
"settings.ai.provider_custom": "අභිරුචි (OpenAI-අනුකූල)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — සත්කාරක, මෙම උපාංගයෙන් පිටතට යයි",
|
||||||
|
"settings.ai.endpoint": "අන්ත ලක්ෂ්යය",
|
||||||
|
"settings.ai.model": "ආකෘතිය",
|
||||||
|
"settings.ai.api_key": "API යතුර",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (දැනටමත් සකසා ඇත — තබා ගැනීමට හිස්ව තබන්න)",
|
||||||
|
"settings.ai.show_api_key": "API යතුර පෙන්වන්න",
|
||||||
|
"settings.ai.hide_api_key": "API යතුර සඟවන්න",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic යනු සත්කාරක තෙවන පාර්ශව සේවාවකි — ඔබ සාරාංශයක් ජනනය කරන විට මෙම රැස්වීමේ පිටපත ඔබේ උපාංගයෙන් පිටතට යයි.",
|
||||||
|
"settings.ai.endpoint_banner": "මෙම අන්ත ලක්ෂ්යය ඔබේ යන්ත්රයේ හෝ LAN හි නැත — ඔබේ පිටපත ඔබේ ජාලයෙන් පිටතට යනු ඇත.",
|
||||||
|
"settings.ai.saving": "සුරකිමින්…",
|
||||||
|
"settings.ai.save": "සුරකින්න",
|
||||||
|
"settings.ai.test_connection": "සම්බන්ධතාවය පරීක්ෂා කරන්න",
|
||||||
|
"settings.ai.this_hosted_provider": "මෙම සත්කාරක සැපයුම්කරු",
|
||||||
|
"settings.ai.reachable": "ළඟා විය හැක",
|
||||||
|
"settings.ai.unreachable": "ළඟා විය නොහැක",
|
||||||
|
"settings.ai.model_count_one": "· ආකෘති {n}",
|
||||||
|
"settings.ai.model_count_many": "· ආකෘති {n}",
|
||||||
|
"settings.ai.on_lan": "· ඔබේ යන්ත්රයේ / LAN හි",
|
||||||
|
"settings.ai.leaves_network": "· ඔබේ ජාලයෙන් පිටතට යයි",
|
||||||
|
"settings.ai.advanced_title": "උසස් Ollama වින්යාසය",
|
||||||
|
"settings.ai.changed": "{n}ක් වෙනස් විය",
|
||||||
|
"settings.ai.reset_all": "සියල්ල පෙරනිමියට යළි සකසන්න",
|
||||||
|
"settings.ai.system_prompt": "පද්ධති විමසුම",
|
||||||
|
"settings.ai.system_prompt_placeholder": "උදා. ඔබ සංක්ෂිප්ත රැස්වීම් සාරාංශකරුවෙකි.",
|
||||||
|
"settings.ai.system_prompt_hint": "WhispAssist හි අවශ්ය ප්රතිදාන ආකෘතියට පෙර එක් කරයි, එබැවින් ඔබේ උපදෙස්වලට සාරාංශ/ක්රියා-අයිතම විග්රහ කිරීම බිඳ දැමිය නොහැක.",
|
||||||
|
"settings.ai.think": "සිතන්න",
|
||||||
|
"settings.ai.think_help": "තර්කන උත්සාහය (තර්කන ආකෘති පමණි).",
|
||||||
|
"settings.ai.keep_alive": "සක්රීයව තබා ගන්න",
|
||||||
|
"settings.ai.keep_alive_help": "ආකෘතිය RAM හි කොපමණ කාලයක් පවතීද. උදා. 5m, 1h, 0 (බෑම), -1 (සදාකල්).",
|
||||||
|
"settings.ai.runtime_hardware": "ධාවන කාලය සහ දෘඪාංග",
|
||||||
|
"settings.ai.rarely_needed": "— කලාතුරකින් අවශ්යයි",
|
||||||
|
"settings.ai.save_advanced": "උසස් සුරකින්න",
|
||||||
|
"settings.ai.saved": "සුරකින ලදී",
|
||||||
|
"settings.ai.turn_off": "අක්රිය කරන්න",
|
||||||
|
"settings.ai.default_value": "පෙරනිමිය {value}.",
|
||||||
|
"settings.ai.reset_default": "පෙරනිමියට යළි සකසන්න",
|
||||||
|
"settings.ai.reset_field": "{name} යළි සකසන්න",
|
||||||
|
|
||||||
|
"settings.mcp.title": "MCP සේවාදායකය",
|
||||||
|
"settings.mcp.banner_1": "මෙය ඔබේම කේතන නියෝජිතයාට (Claude Code, Codex, Copilot, OpenCode, …) ඔබේ දේශීය යන්ත්රයේ රැස්වීම් සන්දර්භය ලබා ගැනීමට ඉඩ දෙයි. සම්බන්ධ වූ පසු,",
|
||||||
|
"settings.mcp.banner_agent": "එම නියෝජිතයා",
|
||||||
|
"settings.mcp.banner_2": "එය කියවන දේ තම මාදිලි සැපයුම්කරුගේ වලාකුළට යැවිය හැක — WhispAssist හි පාලනයෙන් පිටත. WhispAssist විසින්ම මෙම දත්ත කිසිවිටෙක කොතැනකටවත් නොයවයි; සේවාදායකය මෙම උපාංගයේ පමණක් සවන් දෙයි (",
|
||||||
|
"settings.mcp.banner_3": "), සහ සෑම කියවීමක්ම පහත ලොග් වේ.",
|
||||||
|
"settings.mcp.enable": "MCP සේවාදායකය සක්රීය කරන්න",
|
||||||
|
"settings.mcp.enable_hint": "පෙරනිමියෙන් අක්රියයි. ලූප්බැක් පමණි, ටෝකන්-ආරක්ෂිත — ජාලයෙන් කිසිවකට ළඟා විය නොහැක.",
|
||||||
|
"settings.mcp.transport": "ප්රවාහනය",
|
||||||
|
"settings.mcp.transport_http": "ප්රවාහ HTTP",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (නියෝජිතයා ක්රියාවලියක් ජනනය කරයි)",
|
||||||
|
"settings.mcp.port": "තොට",
|
||||||
|
"settings.mcp.transport_change_hint": "ප්රවාහනය/තොට වෙනස් කිරීමට, පළමුව සේවාදායකය අක්රිය කර, පසුව නැවත සක්රීය කරන්න.",
|
||||||
|
"settings.mcp.scope_title": "විෂය පථය",
|
||||||
|
"settings.mcp.expose": "නිරාවරණය කරන්න",
|
||||||
|
"settings.mcp.scope_none": "කිසිවක් නැත — කිසිවක් බෙදා නොගැනේ",
|
||||||
|
"settings.mcp.scope_selected": "තෝරාගත් — ඔබ බෙදාගත් ලෙස සලකුණු කළ විශේෂාංග සාරාංශ පමණි",
|
||||||
|
"settings.mcp.scope_all": "සියල්ල — රැස්වීම්, පිටපත්, ක්රියා-අයිතම සහ බෙදාගත් සාරාංශ",
|
||||||
|
"settings.mcp.expose_recordings": "සුරැකි පටිගත කිරීමක් සහිත රැස්වීම්ද ඉඩ දෙන්න (පෙරනිමියෙන් අක්රියයි — මෙය සක්රීය වන තෙක් පටිගත කළ රැස්වීමක පිටපත “සියල්ල” විෂය පථයේ පවා රඳවා ගැනේ)",
|
||||||
|
"settings.mcp.token_title": "නව සත්යාපන ටෝකනය — වරක් පෙන්වයි, දැන්ම එය පිටපත් කරන්න",
|
||||||
|
"settings.mcp.token_hint": "මෙය නැවත නොපෙන්වයි. එය ඔබේ OS අක්තපත්ර ගබඩාවේ ගබඩා වේ; එය නැති වුවහොත්, නවයක් සෑදීමට සේවාදායකය අක්රිය කර නැවත සක්රීය කරන්න.",
|
||||||
|
"settings.mcp.copied": "පිටපත් කරන ලදී",
|
||||||
|
"settings.mcp.copy": "පිටපත් කරන්න",
|
||||||
|
"settings.mcp.endpoint": "අන්ත ලක්ෂ්යය",
|
||||||
|
"settings.mcp.endpoint_hint": "ඔබේ නියෝජිතයාගේ MCP සේවාදායක වින්යාසය {kind} වෙත යොමු කරන්න, ඉහත ටෝකනය bearer අක්තපත්රයක් ලෙස භාවිත කරමින්.",
|
||||||
|
"settings.mcp.kind_command": "විධානය",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "ප්රවේශ ලොගය",
|
||||||
|
"settings.mcp.access_log_hint": "නියෝජිතයෙකු කරන සෑම මෙවලම් කියවීමක්ම, අවසර දුන්නද ප්රතික්ෂේප කළද (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "තවම කිසිදු නියෝජිතයෙකු කිසිවක් කියවා නැත.",
|
||||||
|
"settings.mcp.log_meeting": "රැස්වීම {id}",
|
||||||
|
"settings.mcp.refresh": "නැවුම් කරන්න",
|
||||||
|
|
||||||
|
"settings.privacy.title": "පෞද්ගලිකත්වය",
|
||||||
|
"settings.privacy.intro": "WhispAssist හට දැන් සැබවින්ම මෙම උපාංගයෙන් පිටතට යැවීමට අවසර ඇති දේ. සියල්ල අක්රිය විට (පෙරනිමිය), උපාංගයෙන් කිසිවක් පිටතට යන්නේ නැත.",
|
||||||
|
"settings.privacy.llm_endpoint": "LLM අන්ත ලක්ෂ්යය",
|
||||||
|
"settings.privacy.off": "අක්රිය",
|
||||||
|
"settings.privacy.local_only": "දේශීය පමණි",
|
||||||
|
"settings.privacy.leaves_device": "මෙම උපාංගයෙන් පිටතට යයි",
|
||||||
|
"settings.privacy.sync_label": "සමමුහුර්තකරණය",
|
||||||
|
"settings.privacy.enabled": "සක්රීයයි",
|
||||||
|
"settings.privacy.mcp_label": "MCP සේවාදායකය",
|
||||||
|
"settings.privacy.mcp_on": "සක්රීය · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "ලූප්බැක් හි පමණක් පැමිණෙන — ඉහත පිටතට යන ලැයිස්තුවට කිසිවක් එක් නොකරයි. සම්බන්ධිත නියෝජිතයෙකුට තවමත් තමා කියවන දේ තම මාදිලි සැපයුම්කරුට යැවිය හැක; MCP සේවාදායක ටැබය බලන්න.",
|
||||||
|
"settings.privacy.egress_title": "පිටතට යාමේ අවසර ලැයිස්තුව",
|
||||||
|
"settings.privacy.no_egress": "කිසිදු ධාරකයක් අවසර ලැයිස්තුවේ නැත — WA කිසිදු අන්තර්ගත පිටතට යාමක් නොකරයි.",
|
||||||
|
"settings.privacy.sync_targets_title": "සමමුහුර්ත ඉලක්ක",
|
||||||
|
"settings.privacy.third_party": "තෙවන පාර්ශව",
|
||||||
|
"settings.privacy.your_server": "ඔබේ සේවාදායකය",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "TLS නැත",
|
||||||
|
"settings.privacy.refresh": "නැවුම් කරන්න",
|
||||||
|
"settings.privacy.unavailable": "පෞද්ගලිකත්ව ස්වයං-පරීක්ෂාව ලද නොහැක.",
|
||||||
|
"settings.privacy.vault_title": "සංකේතන සුරක්ෂිතාගාරය",
|
||||||
|
"settings.privacy.vault_intro": "මුරපදයක් සමඟ සටහන්, පිටපත් සහ සාරාංශ නිශ්චලව සංකේතනය කරන්න. (ශ්රව්ය ගොනු තවම සංකේතනය නොකෙරේ.)",
|
||||||
|
"settings.privacy.vault_password": "සුරක්ෂිතාගාර මුරපදය",
|
||||||
|
"settings.privacy.enable_vault": "සුරක්ෂිතාගාරය සක්රීය කරන්න",
|
||||||
|
"settings.privacy.vault_pw_hint": "අවම වශයෙන් අක්ෂර 8ක් භාවිත කරන්න. ඔබ එය අමතක කළහොත්, සංකේතනය කළ අන්තර්ගතය ප්රතිසාධනය කළ නොහැක.",
|
||||||
|
"settings.privacy.vault_locked_1": "සුරක්ෂිතාගාරය ",
|
||||||
|
"settings.privacy.locked_word": "අගුළු දමා ඇත",
|
||||||
|
"settings.privacy.vault_locked_2": ". සංකේතනය කළ රැස්වීම් කියවීමට අගුළු හරින්න.",
|
||||||
|
"settings.privacy.password": "මුරපදය",
|
||||||
|
"settings.privacy.unlock": "අගුළු හරින්න",
|
||||||
|
"settings.privacy.vault_unlocked_1": "සුරක්ෂිතාගාරය ",
|
||||||
|
"settings.privacy.unlocked_word": "අගුළු හැර ඇත",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". නව සටහන්, පිටපත් සහ සාරාංශ නිශ්චලව සංකේතනය වේ.",
|
||||||
|
"settings.privacy.lock_now": "දැන් අගුළු දමන්න",
|
||||||
|
"settings.privacy.change_password": "මුරපදය වෙනස් කරන්න",
|
||||||
|
"settings.privacy.current_password": "වත්මන් මුරපදය",
|
||||||
|
"settings.privacy.new_password": "නව මුරපදය",
|
||||||
|
|
||||||
|
"settings.about.title": "පිළිබඳව",
|
||||||
|
"settings.about.tagline": "සම්පූර්ණයෙන් දේශීය, විවෘත මූලාශ්ර, Windows-ස්වදේශීය රැස්වීම් සහායකයෙකි.",
|
||||||
|
"settings.about.build_commit": "ගොඩනැගීමේ කැපවීම",
|
||||||
|
|
||||||
|
"consent.heading": "පටිගත කිරීමට පෙර",
|
||||||
|
"consent.body": "සහභාගිවන්නන්ගේ අවසරයකින් තොරව සංවාද පටිගත කිරීම ඔබේ ප්රදේශයේ නීතිවිරෝධී විය හැක. ඔබේ දේශීය පටිගත කිරීමේ නීති පරීක්ෂා කරන්න. මෙය අවවාදයකි, නීති උපදෙසක් නොවේ.",
|
||||||
|
"consent.accept": "මම තේරුම් ගනිමි — පටිගත කිරීම සක්රීය කරන්න",
|
||||||
|
"consent.cancel": "අවලංගු කරන්න",
|
||||||
|
|
||||||
|
"hosted.heading": "{provider} භාවිත කිරීමට පෙර",
|
||||||
|
"hosted.body_1": "මගින් ජනනය කිරීම",
|
||||||
|
"hosted.body_2": "මෙම රැස්වීමේ පිටපත ඔවුන්ගේ සේවාදායක වෙත යවයි — එය මෙම උපාංගයෙන් පිටතට ගොස් ඔවුන්ගේ පෞද්ගලිකත්ව ප්රතිපත්තියට යටත් වේ. දත්ත ඔබේ උපාංගයෙන් පිටතට ගිය පසු එහි හැසිරවීම මත WhispAssist හට කිසිදු පාලනයක් නැත. මෙය පෙරනිමියෙන් අක්රියයි; ඔබ එය දැන් තෝරා ගනී.",
|
||||||
|
"hosted.accept": "මම තේරුම් ගනිමි — ඉදිරියට",
|
||||||
|
"hosted.cancel": "අවලංගු කරන්න",
|
||||||
|
|
||||||
|
"theme.group": "තේමාව",
|
||||||
|
"theme.system": "පද්ධතියට ගළපන්න",
|
||||||
|
"theme.light": "ලා",
|
||||||
|
"theme.dark": "අඳුරු",
|
||||||
|
|
||||||
|
"import.dialog_label": "ගොනුවකින් හෝ URL එකකින් රැස්වීමක් එක් කරන්න",
|
||||||
|
"import.title": "රැස්වීමක් එක් කරන්න",
|
||||||
|
"import.close": "වසන්න",
|
||||||
|
"import.close_title": "වසන්න (Esc)",
|
||||||
|
"import.body": "පවතින පටිගත කිරීමක් ආයාත කරන්න — දේශීය ශ්රව්ය/වීඩියෝ ගොනුවක්, හෝ සබැඳියක් (YouTube, ප්රවාහ පිටුවක්, හෝ සෘජු මාධ්ය URL එකක්). එය සජීවී පටිගත කිරීමක් මෙන්ම පිටපත් කර කථික වෙන් කරයි.",
|
||||||
|
"import.file_or_url": "ගොනුව හෝ URL",
|
||||||
|
"import.source_placeholder": "URL එකක් අලවන්න, හෝ ගොනුවක් සඳහා බ්රවුස් කරන්න…",
|
||||||
|
"import.choose_file": "දේශීය ගොනුවක් තෝරන්න",
|
||||||
|
"import.browse": "බ්රවුස් කරන්න…",
|
||||||
|
"import.title_label": "මාතෘකාව",
|
||||||
|
"import.optional": "විකල්ප",
|
||||||
|
"import.title_placeholder": "පෙරනිමියෙන් ගොනුවේ නම",
|
||||||
|
"import.filter_av": "ශ්රව්ය / වීඩියෝ",
|
||||||
|
"import.requires_1": "අවශ්යයි",
|
||||||
|
"import.requires_2": "ස්ථාපනය කර ඔබේ PATH හි (සහ",
|
||||||
|
"import.requires_3": "URL සඳහා). WhispAssist ඒවා ඇතුළත් නොකරයි.",
|
||||||
|
"import.importing": "ආයාත වෙමින්… මෙයට යම් කාලයක් ගත විය හැක",
|
||||||
|
"import.import": "ආයාත කරන්න",
|
||||||
|
"import.cancel": "අවලංගු කරන්න",
|
||||||
|
|
||||||
|
"tagchip.filter": "“{tag}” ටැගය සහිත රැස්වීම් පෙරන්න",
|
||||||
|
"tagchip.remove": "{tag} ටැගය ඉවත් කරන්න",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "පද්ධති සහ මයික්රොෆෝන ආදාන මට්ටම",
|
||||||
|
"levelmeter.system": "පද්ධති ආදාන මට්ටම",
|
||||||
|
|
||||||
|
"app.tagline": "දේශීය · පෞද්ගලික",
|
||||||
|
"app.note_template": "සටහන් අච්චුව",
|
||||||
|
"app.no_template": "අච්චුවක් නැත",
|
||||||
|
"app.add_meeting": "රැස්වීම එක් කරන්න",
|
||||||
|
"app.add_meeting_title": "ගොනුවකින් හෝ URL එකකින් රැස්වීමක් එක් කරන්න",
|
||||||
|
"app.record": "පටිගත කරන්න",
|
||||||
|
"app.record_title": "පටිගත කිරීම ආරම්භ කරන්න (Ctrl+Shift+R)",
|
||||||
|
"app.stop": "නවත්වන්න",
|
||||||
|
"app.stop_title": "පටිගත කිරීම නවත්වන්න (Ctrl+Shift+R)",
|
||||||
|
"app.cancel": "අවලංගු කරන්න",
|
||||||
|
"app.cancel_title": "මෙම පටිගත කිරීම ඉවත දමා මකන්න",
|
||||||
|
"app.recording": "පටිගත වෙමින්…",
|
||||||
|
"app.backend_title": "සක්රීය පිටපත් කිරීමේ පසුඅන්තය",
|
||||||
|
"app.retention_title": "මෙම රැස්වීම සඳහා ශ්රව්ය .wav ලෙස සුරකින්න",
|
||||||
|
"app.saving": "සුරකිමින්",
|
||||||
|
"app.not_saved": "සුරකා නැත",
|
||||||
|
"app.reconnecting": "ශ්රව්ය උපාංගය නැවත සම්බන්ධ වෙමින්…",
|
||||||
|
"app.settings": "සැකසුම්",
|
||||||
|
"app.settings_title": "සැකසුම් (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "මෙම පටිගත කිරීම ඉවත දමන්නද? එහි ශ්රව්ය සහ පිටපත මකා දැමේ.",
|
||||||
|
"app.announce_started": "පටිගත කිරීම ආරම්භ විය",
|
||||||
|
"app.announce_paused": "පටිගත කිරීම විරාම විය",
|
||||||
|
"app.announce_stopped": "පටිගත කිරීම නැවතුණි",
|
||||||
|
"app.consent_dialog": "පටිගත කිරීමේ අවසරය",
|
||||||
|
"app.vault_dialog": "සංකේතන සුරක්ෂිතාගාරය අගුළු හරින්න",
|
||||||
|
"app.vault_title": "සංකේතන සුරක්ෂිතාගාරය අගුළු හරින්න",
|
||||||
|
"app.vault_desc": "ඔබේ රැස්වීම් නිශ්චලව සංකේතනය කර ඇත. ඒවා කියවීමට ඔබේ මුරපදය ඇතුළත් කරන්න.",
|
||||||
|
"app.vault_password": "සුරක්ෂිතාගාර මුරපදය",
|
||||||
|
"app.vault_incorrect": "වැරදි මුරපදය",
|
||||||
|
"app.unlock": "අගුළු හරින්න",
|
||||||
|
"app.continue_locked": "අගුළු දමා ඉදිරියට",
|
||||||
|
"app.show_meetings": "රැස්වීම් ලැයිස්තුව පෙන්වන්න",
|
||||||
|
"app.hide_meetings": "රැස්වීම් ලැයිස්තුව සඟවන්න",
|
||||||
|
"app.show_summary": "සාරාංශ පැනලය පෙන්වන්න",
|
||||||
|
"app.hide_summary": "සාරාංශ පැනලය සඟවන්න",
|
||||||
|
"app.resize_meetings": "රැස්වීම් ලැයිස්තුවේ ප්රමාණය වෙනස් කරන්න",
|
||||||
|
"app.resize_summary": "සාරාංශ පැනලයේ ප්රමාණය වෙනස් කරන්න",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "රැස්වීම් සොයන්න…",
|
||||||
|
"meetings.search_aria": "රැස්වීම් සොයන්න",
|
||||||
|
"meetings.filter_tag_aria": "ටැගය අනුව පෙරන්න",
|
||||||
|
"meetings.all_tags": "සියලු ටැග",
|
||||||
|
"meetings.from_date_aria": "දිනයේ සිට",
|
||||||
|
"meetings.to_date_aria": "දිනය දක්වා",
|
||||||
|
"meetings.bulk_format_aria": "තොග නිර්යාත ආකෘතිය",
|
||||||
|
"meetings.bulk_export": "තොග නිර්යාතය",
|
||||||
|
"meetings.bulk_export_title": "ඉහත ටැග/දින පෙරහන් වලට ගැලපෙන සෑම රැස්වීමක්ම නිර්යාත කරන්න",
|
||||||
|
"meetings.exporting": "නිර්යාත වෙමින්…",
|
||||||
|
"meetings.exported_one": "රැස්වීම් {n}ක් නිර්යාත කරන ලදී.",
|
||||||
|
"meetings.exported_many": "රැස්වීම් {n}ක් නිර්යාත කරන ලදී.",
|
||||||
|
"meetings.searching": "සොයමින්…",
|
||||||
|
"meetings.loading": "රැස්වීම් පූරණය වෙමින්…",
|
||||||
|
"meetings.no_matches": "ගැලපීම් නැත.",
|
||||||
|
"meetings.empty": "තවම රැස්වීම් නැත — ආරම්භ කිරීමට ඉහත පටිගත කරන්න ක්ලික් කරන්න.",
|
||||||
|
"meetings.status.recording": "පටිගත වෙමින්",
|
||||||
|
"meetings.status.transcribing": "පිටපත් වෙමින්",
|
||||||
|
"meetings.status.recovering": "ප්රතිසාධනය වෙමින්",
|
||||||
|
"meetings.status.error": "දෝෂය",
|
||||||
|
"meetings.resume": "පිටපත් කිරීම නැවත ආරම්භ කරන්න",
|
||||||
|
"meetings.delete_aria": "රැස්වීම මකන්න",
|
||||||
|
"meetings.delete_confirm": "මෙම රැස්වීම මකන්නද? මෙය එහි පටිගත කිරීම, පිටපත සහ සටහන් ඉවත් කරයි.",
|
||||||
|
|
||||||
|
"transcript.heading": "පිටපත",
|
||||||
|
"transcript.title_aria": "රැස්වීමේ මාතෘකාව",
|
||||||
|
"transcript.lang_title": "පිටපත් කිරීමේ භාෂාව",
|
||||||
|
"transcript.lang_auto": "හඳුනා ගනිමින්…",
|
||||||
|
"transcript.show": "පිටපත පෙන්වන්න",
|
||||||
|
"transcript.hide": "පිටපත සඟවන්න",
|
||||||
|
"transcript.empty": "මෙම රැස්වීම සඳහා පිටපතක් නැත.",
|
||||||
|
"transcript.will_appear": "ඔබ පටිගත කරන විට පිටපත මෙතැන දිස්වේ.",
|
||||||
|
"transcript.play_from_here": "මෙතැන සිට වාදනය කරන්න",
|
||||||
|
"transcript.resize": "පිටපත සහ සටහන් ප්රමාණය වෙනස් කරන්න",
|
||||||
|
"transcript.reprocess_placeholder": "මෙය සමඟ නැවත පිටපත් කරන්න…",
|
||||||
|
"transcript.reprocess_lang_aria": "නැවත සැකසුම් භාෂාව",
|
||||||
|
"transcript.reprocess_keep_lang": "වත්මන් භාෂාව තබා ගන්න",
|
||||||
|
"transcript.reprocess_go": "යන්න",
|
||||||
|
"transcript.retranscribing": "නැවත පිටපත් වෙමින්…",
|
||||||
|
"transcript.has_note": "සටහනක් ඇත",
|
||||||
|
"transcript.note_placeholder": "මෙම මොහොත සඳහා සටහනක් එක් කරන්න…",
|
||||||
|
"transcript.note_aria": "මෙම පිටපත් පේළිය සඳහා සටහන",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Word ලේඛනය",
|
||||||
|
"transcript.filter_obsidian": "Obsidian සටහන",
|
||||||
|
|
||||||
|
"notes.heading": "සටහන්",
|
||||||
|
"notes.show": "සටහන් පෙන්වන්න",
|
||||||
|
"notes.hide": "සටහන් සඟවන්න",
|
||||||
|
"notes.placeholder": "සටහන්…",
|
||||||
|
"notes.live_placeholder": "කතා කරන අතරතුර සටහන් ටයිප් කරන්න…",
|
||||||
|
"notes.toolbar_aria": "සටහන් හැඩතලකරණය",
|
||||||
|
"notes.bold": "තදකුරු",
|
||||||
|
"notes.italic": "ඇල අකුරු",
|
||||||
|
"notes.h1": "ශීර්ෂය 1",
|
||||||
|
"notes.h2": "ශීර්ෂය 2",
|
||||||
|
"notes.bullet": "බුලට් ලැයිස්තුව",
|
||||||
|
"notes.checkbox_title": "සලකුණු කොටුව",
|
||||||
|
"notes.checkbox_aria": "සලකුණු කොටු ලැයිස්තු අයිතමය",
|
||||||
|
"notes.edit_raw": "අමු markdown සංස්කරණය කරන්න",
|
||||||
|
"notes.render": "markdown විදහන්න",
|
||||||
|
"notes.editor": "සංස්කාරකය",
|
||||||
|
"notes.preview": "පෙරදසුන",
|
||||||
|
"notes.export_md_title": "සටහන් .md ලෙස නිර්යාත කරන්න",
|
||||||
|
"notes.export_pdf_title": "සටහන් .pdf ලෙස නිර්යාත කරන්න",
|
||||||
|
"notes.export_docx_title": "සටහන් .docx ලෙස නිර්යාත කරන්න",
|
||||||
|
"notes.export_bundle_title": "ශ්රව්ය + පිටපත + සටහන් ෆෝල්ඩරයකට නිර්යාත කරන්න",
|
||||||
|
"notes.export_obsidian_title": "එක් Obsidian සටහනක් නිර්යාත කරන්න (සටහන් + සාරාංශය + පිටපත, ශ්රව්ය නැත)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "පටිගත කිරීම",
|
||||||
|
"summary.loading_recording": "පටිගත කිරීම පූරණය වෙමින්…",
|
||||||
|
"summary.sync_heading": "සමමුහුර්තකරණය",
|
||||||
|
"summary.uploading": "උඩුගත වෙමින්…",
|
||||||
|
"summary.upload_now": "දැන් උඩුගත කරන්න",
|
||||||
|
"summary.no_uploads": "මෙම රැස්වීම සඳහා තවම උඩුගත කිරීම් නැත.",
|
||||||
|
"summary.retry": "නැවත උත්සාහ කරන්න",
|
||||||
|
"summary.tags_heading": "ටැග",
|
||||||
|
"summary.tags_select": "රැස්වීමක් ටැග් කිරීමට එය තෝරන්න.",
|
||||||
|
"summary.tag_add_placeholder": "ටැගය එක් කරන්න…",
|
||||||
|
"summary.tag_first_placeholder": "ව්යාපෘතිය, සේවාදායකයා, මාතෘකාව…",
|
||||||
|
"summary.generating": "ජනනය වෙමින්…",
|
||||||
|
"summary.generate_tags": "ටැග ජනනය කරන්න",
|
||||||
|
"summary.saving": "සුරකිමින්…",
|
||||||
|
"summary.save_tags": "ටැග සුරකින්න",
|
||||||
|
"summary.summary_heading": "සාරාංශය",
|
||||||
|
"summary.ai_provider": "AI සැපයුම්කරු",
|
||||||
|
"summary.provider_off": "අක්රිය",
|
||||||
|
"summary.provider_custom": "අභිරුචි",
|
||||||
|
"summary.local": "දේශීය",
|
||||||
|
"summary.leaves_device": "මෙම උපාංගයෙන් පිටතට යයි",
|
||||||
|
"summary.this_hosted_provider": "මෙම සත්කාරක සැපයුම්කරු",
|
||||||
|
"summary.select_generate": "සාරාංශයක් ජනනය කිරීමට රැස්වීමක් තෝරන්න.",
|
||||||
|
"summary.decisions_heading": "තීරණ",
|
||||||
|
"summary.regenerate": "නැවත ජනනය කරන්න",
|
||||||
|
"summary.generated_hint": "රැස්වීමෙන් පසු දේශීයව ජනනය කරන ලදී (දේශීය LLM සැපයුම්කරුවෙකු අවශ්යයි).",
|
||||||
|
"summary.no_provider": "AI සැපයුම්කරුවෙකු වින්යාස කර නැත — සැකසුම්වල එකක් සක්රීය කරන්න.",
|
||||||
|
"summary.generate_summary": "සාරාංශය ජනනය කරන්න",
|
||||||
|
"summary.briefs_heading": "විශේෂාංග සාරාංශ",
|
||||||
|
"summary.briefs_select": "විශේෂාංග සාරාංශ සෑදීමට හෝ බැලීමට රැස්වීමක් තෝරන්න.",
|
||||||
|
"summary.briefs_desc": "මෙම රැස්වීම නියෝජිතයාට සූදානම් පිරිවිතරයකට පෙරයි (දේශීය LLM සැපයුම්කරුවෙකු අවශ්යයි) — එය කේතන නියෝජිතයෙකුට දෙන්න, හෝ MCP සේවාදායකය සක්රීය වූ පසු එය හරහා සපයන්න.",
|
||||||
|
"summary.brief_repo_placeholder": "ඉලක්ක ගබඩාව (විකල්ප), උදා. acme/reporting-web",
|
||||||
|
"summary.distilling": "පෙරමින්…",
|
||||||
|
"summary.create_brief": "විශේෂාංග සාරාංශය සාදන්න",
|
||||||
|
"summary.brief_mcp_title": "MCP සේවාදායකය සක්රීය විට ලද හැක",
|
||||||
|
"summary.copied": "පිටපත් කරන ලදී",
|
||||||
|
"summary.copy_md": "Markdown ලෙස පිටපත් කරන්න",
|
||||||
|
"summary.copy_json": "JSON ලෙස පිටපත් කරන්න",
|
||||||
|
"summary.brief_problem": "ගැටලුව",
|
||||||
|
"summary.brief_outcome": "අපේක්ෂිත ප්රතිඵලය",
|
||||||
|
"summary.brief_criteria": "පිළිගැනීමේ නිර්ණායක",
|
||||||
|
"summary.brief_none": "කිසිවක් ග්රහණය කර නැත.",
|
||||||
|
"summary.brief_context": "සන්දර්භය",
|
||||||
|
"summary.action_items_heading": "ක්රියා-අයිතම",
|
||||||
|
"summary.ai_select": "එහි ක්රියා-අයිතම බැලීමට රැස්වීමක් තෝරන්න.",
|
||||||
|
"summary.ai_empty": "තවම කිසිවක් නැත — පහත එකක් එක් කරන්න, හෝ ඒවා ස්වයංක්රීයව උකහා ගැනීමට සාරාංශයක් ජනනය කරන්න.",
|
||||||
|
"summary.confirmed": "තහවුරු කරන ලදී",
|
||||||
|
"summary.ai_text_placeholder": "ක්රියා-අයිතමය…",
|
||||||
|
"summary.ai_text_aria": "ක්රියා-අයිතම පෙළ",
|
||||||
|
"summary.owner": "හිමිකරු",
|
||||||
|
"summary.due_date": "නියමිත දිනය",
|
||||||
|
"summary.reminder_title": "මෙම නියමිත දිනය සඳහා දේශීය මතක් කිරීමක් උපලේඛනගත කරන්න",
|
||||||
|
"summary.ai_delete_title": "මෙම ක්රියා-අයිතමය මකන්න",
|
||||||
|
"summary.ai_delete_aria": "ක්රියා-අයිතමය මකන්න",
|
||||||
|
"summary.add_action_item": "ක්රියා-අයිතමය එක් කරන්න",
|
||||||
|
"summary.save_action_items": "ක්රියා-අයිතම සුරකින්න",
|
||||||
|
"summary.calendar_heading": "දින දර්ශන සිදුවීම",
|
||||||
|
"summary.cal_select": "රැස්වීමක් දින දර්ශන සිදුවීමකට සම්බන්ධ කිරීමට එය තෝරන්න.",
|
||||||
|
"summary.event_search_placeholder": "සිදුවීම් මාතෘකාව සොයන්න…",
|
||||||
|
"summary.linked_event": "සම්බන්ධිත සිදුවීම",
|
||||||
|
"summary.change_event": "සිදුවීම වෙනස් කරන්න…",
|
||||||
|
"summary.link_event": "සිදුවීමක් සම්බන්ධ කරන්න…",
|
||||||
|
"summary.untitled_event": "(මාතෘකා රහිත)",
|
||||||
|
"summary.no_events_1": "තවම සිදුවීම් ආයාත කර නැත — එකක්",
|
||||||
|
"summary.no_events_2": "සැකසුම් → දින දර්ශනයෙන් ආයාත කරන්න.",
|
||||||
|
"summary.no_events_match": "මෙම සෙවීම/දිනයට කිසිදු සිදුවීමක් නොගැලපේ — එකක් හිස් කිරීමට උත්සාහ කරන්න.",
|
||||||
|
"summary.organizer_label": "සංවිධායක: {name}",
|
||||||
|
"summary.participants_heading": "සහභාගිවන්නන්",
|
||||||
|
"summary.participants_hint": "සම්බන්ධිත දින දර්ශන සිදුවීමෙන් පුරවනු ලැබේ.",
|
||||||
|
"summary.speakers_heading": "කථිකයන්",
|
||||||
|
"summary.speakers_select": "එහි කථිකයන්ට නම් දීමට රැස්වීමක් තෝරන්න.",
|
||||||
|
"summary.no_speakers": "තවම කථිකයන් හඳුනාගෙන නැත.",
|
||||||
|
"summary.speaker_name_placeholder": "කථිකයාගේ නම",
|
||||||
|
"summary.save": "සුරකින්න",
|
||||||
|
"summary.cancel": "අවලංගු කරන්න",
|
||||||
|
"summary.name_speaker": "මෙම කථිකයාට නම් කරන්න…",
|
||||||
|
"summary.add_new_name": "+ නව නමක් එක් කරන්න…",
|
||||||
|
|
||||||
|
"settings.recording.title": "පටිගත කිරීම",
|
||||||
|
"settings.recording.record_default": "පෙරනිමියෙන් රැස්වීම් පටිගත කරන්න",
|
||||||
|
"settings.recording.save_audio_as": "ශ්රව්ය මෙසේ සුරකින්න",
|
||||||
|
"settings.recording.default_hint": "පෙරනිමියෙන් අක්රියයි. අක්රිය විට, ශ්රව්ය පිටපත සෑදීමට පමණක් භාවිත වී රැස්වීම අවසන් වන විට මකා දැමේ. ඔබට රැස්වීමකට පටිගත කිරීම ද මාරු කළ හැක.",
|
||||||
|
"settings.recording.consent_label": "අවසරය: {status}.",
|
||||||
|
"settings.recording.consent_ack": "පිළිගත්තා",
|
||||||
|
"settings.recording.consent_not": "තවම පිළිගෙන නැත",
|
||||||
|
"settings.recording.auto_label": "දින දර්ශන සිදුවීමක් ආරම්භ වන විට ස්වයංක්රීයව පටිගත කිරීම ආරම්භ කරන්න",
|
||||||
|
"settings.recording.auto_hint": "WhispAssist විවෘතව තිබෙන විට පමණි. ආයාත කළ දින දර්ශන සිදුවීමක ආරම්භක වේලාව පැමිණි විට, පටිගත කිරීමක් ස්වයංක්රීයව ආරම්භ වේ (ඉහත ඔබේ පෙරනිමි රඳවා තබා ගැනීමේ සැකසුම භාවිත කරමින්). පසුබිමින් කිසිවක් ධාවනය නොවේ — යෙදුම ධාවනය වන විට පමණක් කාල නියාමකය සක්රීයයි. සැකසුම් → දින දර්ශනය යටතේ සිදුවීම් ආයාත කරන්න."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "Inspelning",
|
||||||
|
"nav.hardware": "Maskinvara",
|
||||||
|
"nav.storage": "Lagring",
|
||||||
|
"nav.calendar": "Kalender",
|
||||||
|
"nav.sync": "Synkronisering",
|
||||||
|
"nav.ai": "AI",
|
||||||
|
"nav.mcp": "MCP-server",
|
||||||
|
"nav.privacy": "Integritet",
|
||||||
|
"nav.about": "Om",
|
||||||
|
"nav.language": "Språk",
|
||||||
|
|
||||||
|
"settings.language.title": "Språk",
|
||||||
|
"settings.language.display": "Visningsspråk",
|
||||||
|
"settings.language.display_hint": "Språket för appens gränssnitt. Att lägga till ett språk är lika enkelt som att släppa in en enda översättningsfil.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "Transkriberingsspråk",
|
||||||
|
"settings.transcription.label": "Språk",
|
||||||
|
"settings.transcription.auto": "Identifiera automatiskt",
|
||||||
|
"settings.transcription.applies_hint": "Gäller nästa inspelning. Det språk som faktiskt användes visas därefter på varje möte.",
|
||||||
|
"settings.transcription.english_only": "Endast engelska",
|
||||||
|
"settings.transcription.model_english_only": "”{model}” är endast på engelska.",
|
||||||
|
"settings.transcription.no_model": "Ingen modell vald.",
|
||||||
|
"settings.transcription.switch_multilingual": "Byt till en flerspråkig modell ovan för att välja ett språk.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "Maskinvara",
|
||||||
|
"settings.hardware.active_backend": "Aktiv backend",
|
||||||
|
"settings.hardware.model_meta": "· modell {size}",
|
||||||
|
"settings.hardware.preferred_backend": "Föredragen backend",
|
||||||
|
"settings.hardware.auto_backend": "Automatisk (bäst tillgängliga)",
|
||||||
|
"settings.hardware.not_available": "ej tillgänglig",
|
||||||
|
"settings.hardware.fallback_hint": "Faller automatiskt tillbaka (NPU → NVIDIA → AMD → Intel → CPU) om den valda backenden inte går att läsa in.",
|
||||||
|
"settings.hardware.audio_devices": "Ljudenheter",
|
||||||
|
"settings.hardware.recording_device": "Inspelningsenhet",
|
||||||
|
"settings.hardware.default_system_audio": "Standardsystemljud",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist spelar in det som den här enheten spelar upp (loopback) — den andra sidan av samtalet. Välj en specifik utgång om du inte vill att den ska följa Windows systemstandard.",
|
||||||
|
"settings.hardware.microphone": "Mikrofon",
|
||||||
|
"settings.hardware.mic_off": "Av — fånga inte min mikrofon",
|
||||||
|
"settings.hardware.default_mic": "Standardmikrofon",
|
||||||
|
"settings.hardware.mic_hint": "Lägger till din egen röst i den direktsända transkriptionen så att båda sidor av mötet fångas. Stannar på din enhet — inget laddas upp. Välj ”Av” för att endast transkribera systemljudet ovan.",
|
||||||
|
"settings.hardware.npu_detected": "NPU-accelerator upptäckt",
|
||||||
|
"settings.hardware.ready": "Redo",
|
||||||
|
"settings.hardware.downloading": "Laddar ner…",
|
||||||
|
"settings.hardware.package_needed": "Paket krävs",
|
||||||
|
"settings.hardware.npu_hint": "Din NPU behöver ett engångspaket för acceleration (Whisper ONNX-modell{extra}). Det laddas ner i bakgrunden; du kan också starta det nu.",
|
||||||
|
"settings.hardware.plus_openvino": " + OpenVINO-körmiljö",
|
||||||
|
"settings.hardware.download_npu": "Ladda ner NPU-paket",
|
||||||
|
"settings.hardware.dml_title": "GPU-acceleration (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "Din GPU kan accelereras utan Vulkan via DirectML — ett engångspaket (Whisper ONNX-modell{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + DirectML-körmiljö",
|
||||||
|
"settings.hardware.download_dml": "Ladda ner DirectML-paket",
|
||||||
|
"settings.hardware.detection_unavailable": "Identifiering av maskinvara ej tillgänglig.",
|
||||||
|
"settings.hardware.low_overhead": "Förinställning med låg belastning",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + minsta modellen, för batteri-/bakgrundsanvändning",
|
||||||
|
"settings.hardware.models": "Modeller",
|
||||||
|
"settings.hardware.downloading_model": "Laddar ner {name}",
|
||||||
|
"settings.hardware.active_badge": "Aktiv",
|
||||||
|
"settings.hardware.installed": "Installerad",
|
||||||
|
"settings.hardware.use": "Använd",
|
||||||
|
"settings.hardware.remove": "Ta bort",
|
||||||
|
"settings.hardware.cant_remove_active": "Kan inte ta bort den aktiva modellen",
|
||||||
|
"settings.hardware.download": "Ladda ner",
|
||||||
|
"settings.hardware.diarization": "Talardiarisering",
|
||||||
|
"settings.hardware.diarization_hint": "Installera båda modellerna för att få färdiga inspelningar uppdelade per talare (Talare 1, Talare 2, …) i stället för en enda löpande transkription. Körs helt offline, en gång per möte efter att det stoppats. Utan dem tillskrivs varje rad en enda talare.",
|
||||||
|
|
||||||
|
"settings.storage.title": "Lagring",
|
||||||
|
"settings.storage.location_label": "Plats",
|
||||||
|
"settings.storage.location_hint": "Att ändra lagringsplats stöds inte än — det är här möten, modeller och databasen för närvarande finns.",
|
||||||
|
"settings.storage.retention_title": "Kvarhållning",
|
||||||
|
"settings.storage.retention_hint": "Tar automatiskt bort de äldsta mötena över en gräns. Kontrolleras en gång vid start; tomt betyder ingen gräns. Rör aldrig ett möte som spelas in för närvarande.",
|
||||||
|
"settings.storage.max_age": "Maxålder (dagar)",
|
||||||
|
"settings.storage.max_size": "Maxstorlek (GB)",
|
||||||
|
"settings.storage.no_limit": "ingen gräns",
|
||||||
|
"settings.storage.export_import_title": "Export och import",
|
||||||
|
"settings.storage.export_hint_1": "Flytta inspelningar mellan datorer som portabla paketmappar (ljud, transkription, anteckningar, sammanfattning och ett",
|
||||||
|
"settings.storage.export_hint_2": "manifest). Exportera till valfri mapp — en synkroniserad enhet, ett USB-minne eller en synkroniseringsmåls lokala monteringspunkt — och importera det sedan på den andra datorn. Importerade möten får ett nytt id, så att importera på nytt skriver aldrig över något.",
|
||||||
|
"settings.storage.export_dialog_title": "Exportera alla möten till…",
|
||||||
|
"settings.storage.import_dialog_title": "Importera ett paket (eller en mapp med paket)…",
|
||||||
|
"settings.storage.exporting": "Exporterar…",
|
||||||
|
"settings.storage.export_all": "Exportera alla möten…",
|
||||||
|
"settings.storage.importing": "Importerar…",
|
||||||
|
"settings.storage.import_all": "Importera möten…",
|
||||||
|
"settings.storage.exported_one": "Exporterade {n} möte.",
|
||||||
|
"settings.storage.exported_many": "Exporterade {n} möten.",
|
||||||
|
"settings.storage.imported_one": "Importerade {n} möte.",
|
||||||
|
"settings.storage.imported_many": "Importerade {n} möten.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "Kalender och Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "Importera händelser och deltagare från en lokal Outlook-",
|
||||||
|
"settings.calendar.intro_2": "säkerhetskopia — skrivskyddad, inget skrivs tillbaka till filen. Inget lämnar den här enheten.",
|
||||||
|
"settings.calendar.pst_file": ".pst-fil",
|
||||||
|
"settings.calendar.no_file": "Ingen fil vald",
|
||||||
|
"settings.calendar.browse": "Bläddra…",
|
||||||
|
"settings.calendar.password": "Lösenord",
|
||||||
|
"settings.calendar.password_note": "sällan nödvändigt",
|
||||||
|
"settings.calendar.optional": "valfritt",
|
||||||
|
"settings.calendar.import_range": "Importintervall",
|
||||||
|
"settings.calendar.range_30": "Senaste 30 dagarna",
|
||||||
|
"settings.calendar.range_90": "Senaste 90 dagarna",
|
||||||
|
"settings.calendar.range_180": "Senaste 6 månaderna",
|
||||||
|
"settings.calendar.range_365": "Senaste året",
|
||||||
|
"settings.calendar.range_all": "All tid",
|
||||||
|
"settings.calendar.range_hint": "En långlivad brevlåda kan innehålla år av återkommande poster/helgdagar — att smalna av intervallet håller den importerade kalendern till det som faktiskt är relevant. Gäller både den här importknappen och den automatiska omimporten vid start.",
|
||||||
|
"settings.calendar.importing": "Importerar…",
|
||||||
|
"settings.calendar.import": "Importera",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} händelser",
|
||||||
|
"settings.calendar.auto_reimport": "Importera den här filen automatiskt vid start",
|
||||||
|
"settings.calendar.auto_reimport_hint": "Körs en gång vid start, inte på en timer — omimport är säker att upprepa (befintliga händelser matchas och uppdateras, inte dupliceras).",
|
||||||
|
"settings.calendar.import_failed": "Importen misslyckades: {error} — själva filen är orörd; kontrollera sökvägen och försök igen.",
|
||||||
|
"settings.calendar.cleanup_title": "Rensa upp",
|
||||||
|
"settings.calendar.cleanup_hint": "Händelser som redan är kopplade till ett inspelat möte behålls alltid, oavsett vilket alternativ som väljs nedan.",
|
||||||
|
"settings.calendar.cleanup_30": "Äldre än 30 dagar",
|
||||||
|
"settings.calendar.cleanup_90": "Äldre än 90 dagar",
|
||||||
|
"settings.calendar.cleanup_180": "Äldre än 6 månader",
|
||||||
|
"settings.calendar.cleanup_365": "Äldre än 1 år",
|
||||||
|
"settings.calendar.cleanup_all": "Ta bort alla",
|
||||||
|
"settings.calendar.cleaning": "Rensar…",
|
||||||
|
"settings.calendar.cleanup_btn": "Rensa kalendern",
|
||||||
|
"settings.calendar.cleanup_result": "Tog bort {deleted}{extra}.",
|
||||||
|
"settings.calendar.cleanup_kept": ", behöll {n} (kopplade till ett möte)",
|
||||||
|
"settings.calendar.cleanup_failed": "Rensningen misslyckades: {error}",
|
||||||
|
"settings.calendar.imported_events": "Importerade händelser",
|
||||||
|
"settings.calendar.no_events": "Inga händelser importerade än.",
|
||||||
|
"settings.calendar.search_title": "Sök titel",
|
||||||
|
"settings.calendar.search_placeholder": "Mötesnamn…",
|
||||||
|
"settings.calendar.date_label": "Datum",
|
||||||
|
"settings.calendar.events_count": "{shown} av {total} händelser",
|
||||||
|
"settings.calendar.untitled": "(utan titel)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "Inställningar",
|
||||||
|
"settings.close": "Stäng",
|
||||||
|
"settings.close_title": "Stäng (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "Förhandsgranskningsläge — synkroniseringsbackenden är inte implementerad än (Fas 9). Ändringar behålls bara i gränssnittet.",
|
||||||
|
"settings.sync.title": "Synkronisering och uppladdning",
|
||||||
|
"settings.sync.enable_label": "Aktivera uppladdning av mötesartefakter till konfigurerade mål",
|
||||||
|
"settings.sync.enable_hint": "Av som standard. Inget laddas upp om inte detta är på och ett mål är aktiverat. Självhostade mål behåller data på din egen server; tredjepartsmoln är tydligt märkta.",
|
||||||
|
"settings.sync.targets_title": "Mål",
|
||||||
|
"settings.sync.no_targets": "Inga mål än. Lägg till ett nedan.",
|
||||||
|
"settings.sync.third_party": "tredjepart",
|
||||||
|
"settings.sync.your_server": "din server",
|
||||||
|
"settings.sync.edit": "Redigera",
|
||||||
|
"settings.sync.remove": "Ta bort",
|
||||||
|
"settings.sync.edit_target": "Redigera mål",
|
||||||
|
"settings.sync.add_target": "Lägg till ett mål",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV täcker Nextcloud, ownCloud, Cloudreve, Seafile och Synology. (Seafile: aktivera SeafDAV på serversidan.)",
|
||||||
|
"settings.sync.name": "Namn",
|
||||||
|
"settings.sync.name_placeholder": "Hem-Nextcloud",
|
||||||
|
"settings.sync.provider": "Leverantör",
|
||||||
|
"settings.sync.server_url": "Server-URL",
|
||||||
|
"settings.sync.server_url_note_1": "Bara din server-URL — WhispAssist lägger till",
|
||||||
|
"settings.sync.server_url_note_2": "automatiskt.",
|
||||||
|
"settings.sync.remote_folder": "Fjärrmapp",
|
||||||
|
"settings.sync.username": "Användarnamn",
|
||||||
|
"settings.sync.app_password": "App-lösenord",
|
||||||
|
"settings.sync.pw_keep": "lämna tomt för att behålla nuvarande lösenord",
|
||||||
|
"settings.sync.pw_store": "lagras i operativsystemets autentiseringsförvar",
|
||||||
|
"settings.sync.upload_legend": "Ladda upp",
|
||||||
|
"settings.sync.artifact_transcript": "transkription",
|
||||||
|
"settings.sync.artifact_notes": "anteckningar",
|
||||||
|
"settings.sync.artifact_summary": "sammanfattning",
|
||||||
|
"settings.sync.artifact_recording": "inspelning (.wav, om den behålls)",
|
||||||
|
"settings.sync.allow_plaintext": "Tillåt oskyddad http för en LAN-adress (rekommenderas inte)",
|
||||||
|
"settings.sync.encrypt_before": "Kryptera före uppladdning (destinationen lagrar endast chiffertext)",
|
||||||
|
"settings.sync.requires_vault": "— kräver ett upplåst valv",
|
||||||
|
"settings.sync.test_connection": "Testa anslutningen",
|
||||||
|
"settings.sync.save_changes": "Spara ändringar",
|
||||||
|
"settings.sync.add_target_btn": "Lägg till mål",
|
||||||
|
"settings.sync.cancel": "Avbryt",
|
||||||
|
"settings.sync.third_party_banner": "{kind} är ett tredjepartsmoln — uppladdning skickar dina data bort från din enhet till {kind}.",
|
||||||
|
"settings.sync.link_account": "Länka {kind}-konto…",
|
||||||
|
"settings.sync.oauth_hint": "Öppnar en OAuth-inloggning (loopback-omdirigering); token lagras i operativsystemets autentiseringsförvar.",
|
||||||
|
"settings.sync.footnote": "Autentiseringsuppgifter skrivs aldrig till inställningarna eller databasen — endast till operativsystemets autentiseringsförvar. TLS krävs för mål utanför LAN.",
|
||||||
|
|
||||||
|
"settings.ai.title": "AI-sammanfattningsleverantör",
|
||||||
|
"settings.ai.intro_1": "Sammanfattningar körs på en lokal LLM som standard. Peka detta mot Ollama på den här datorn eller en annan maskin i ditt LAN (t.ex.",
|
||||||
|
"settings.ai.intro_2": "— båda räknas som lokala, så inget lämnar ditt nätverk. Hostade leverantörer (Anthropic) är valfria, av som standard, och skickar transkriptionen till en tredjepart så snart du aktiverar en.",
|
||||||
|
"settings.ai.provider": "Leverantör",
|
||||||
|
"settings.ai.off": "Av",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (lokal / LAN)",
|
||||||
|
"settings.ai.provider_custom": "Anpassad (OpenAI-kompatibel)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — hostad, lämnar den här enheten",
|
||||||
|
"settings.ai.endpoint": "Slutpunkt",
|
||||||
|
"settings.ai.model": "Modell",
|
||||||
|
"settings.ai.api_key": "API-nyckel",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (redan angiven — lämna tomt för att behålla den)",
|
||||||
|
"settings.ai.show_api_key": "Visa API-nyckel",
|
||||||
|
"settings.ai.hide_api_key": "Dölj API-nyckel",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic är en hostad tredjepartstjänst — det här mötets transkription lämnar din enhet när du genererar en sammanfattning.",
|
||||||
|
"settings.ai.endpoint_banner": "Den här slutpunkten är inte på din maskin eller ditt LAN — din transkription skulle lämna ditt nätverk.",
|
||||||
|
"settings.ai.saving": "Sparar…",
|
||||||
|
"settings.ai.save": "Spara",
|
||||||
|
"settings.ai.test_connection": "Testa anslutningen",
|
||||||
|
"settings.ai.this_hosted_provider": "denna hostade leverantör",
|
||||||
|
"settings.ai.reachable": "Nåbar",
|
||||||
|
"settings.ai.unreachable": "Onåbar",
|
||||||
|
"settings.ai.model_count_one": "· {n} modell",
|
||||||
|
"settings.ai.model_count_many": "· {n} modeller",
|
||||||
|
"settings.ai.on_lan": "· på din maskin / LAN",
|
||||||
|
"settings.ai.leaves_network": "· lämnar ditt nätverk",
|
||||||
|
"settings.ai.advanced_title": "Avancerad Ollama-konfiguration",
|
||||||
|
"settings.ai.changed": "{n} ändrade",
|
||||||
|
"settings.ai.reset_all": "Återställ alla till standard",
|
||||||
|
"settings.ai.system_prompt": "Systemprompt",
|
||||||
|
"settings.ai.system_prompt_placeholder": "t.ex. Du är en koncis mötessammanfattare.",
|
||||||
|
"settings.ai.system_prompt_hint": "Läggs till före WhispAssists obligatoriska utdataformat, så att dina instruktioner inte kan förstöra tolkningen av sammanfattning/åtgärdspunkter.",
|
||||||
|
"settings.ai.think": "Tänk",
|
||||||
|
"settings.ai.think_help": "Resonemangsansträngning (endast resonemangsmodeller).",
|
||||||
|
"settings.ai.keep_alive": "Håll igång",
|
||||||
|
"settings.ai.keep_alive_help": "Hur länge modellen stannar i RAM. t.ex. 5m, 1h, 0 (avlasta), -1 (för alltid).",
|
||||||
|
"settings.ai.runtime_hardware": "Körning och maskinvara",
|
||||||
|
"settings.ai.rarely_needed": "— sällan nödvändigt",
|
||||||
|
"settings.ai.save_advanced": "Spara avancerat",
|
||||||
|
"settings.ai.saved": "Sparat",
|
||||||
|
"settings.ai.turn_off": "Stäng av",
|
||||||
|
"settings.ai.default_value": "Standard {value}.",
|
||||||
|
"settings.ai.reset_default": "Återställ till standard",
|
||||||
|
"settings.ai.reset_field": "Återställ {name}",
|
||||||
|
|
||||||
|
"settings.mcp.title": "MCP-server",
|
||||||
|
"settings.mcp.banner_1": "Detta låter din egen kodningsagent (Claude Code, Codex, Copilot, OpenCode, …) hämta möteskontext på din lokala maskin. När den är ansluten",
|
||||||
|
"settings.mcp.banner_agent": "kan den agenten",
|
||||||
|
"settings.mcp.banner_2": "vidarebefordra det den läser till sin egen modellleverantörs moln — utanför WhispAssists kontroll. WhispAssist självt skickar aldrig dessa data någonstans; servern lyssnar bara på den här enheten (",
|
||||||
|
"settings.mcp.banner_3": ") och varje läsning loggas nedan.",
|
||||||
|
"settings.mcp.enable": "Aktivera MCP-servern",
|
||||||
|
"settings.mcp.enable_hint": "Av som standard. Endast loopback, tokenskyddad — inget är nåbart från nätverket.",
|
||||||
|
"settings.mcp.transport": "Transport",
|
||||||
|
"settings.mcp.transport_http": "Strömmande HTTP",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (agenten startar en process)",
|
||||||
|
"settings.mcp.port": "Port",
|
||||||
|
"settings.mcp.transport_change_hint": "För att ändra transport/port, stäng av servern först och slå sedan på den igen.",
|
||||||
|
"settings.mcp.scope_title": "Omfång",
|
||||||
|
"settings.mcp.expose": "Exponera",
|
||||||
|
"settings.mcp.scope_none": "Inget — inget delas",
|
||||||
|
"settings.mcp.scope_selected": "Utvalda — endast funktionsbriefer du har markerat som delade",
|
||||||
|
"settings.mcp.scope_all": "Alla — möten, transkriptioner, åtgärdspunkter och delade briefer",
|
||||||
|
"settings.mcp.expose_recordings": "Tillåt även möten med en sparad inspelning (av som standard — ett inspelat mötes transkription hålls tillbaka även i omfånget ”Alla” tills detta är på)",
|
||||||
|
"settings.mcp.token_title": "Ny autentiseringstoken — visas en gång, kopiera den nu",
|
||||||
|
"settings.mcp.token_hint": "Detta visas inte igen. Det lagras i operativsystemets autentiseringsförvar; om du förlorar det, stäng av och slå på servern igen för att skapa en ny.",
|
||||||
|
"settings.mcp.copied": "Kopierad",
|
||||||
|
"settings.mcp.copy": "Kopiera",
|
||||||
|
"settings.mcp.endpoint": "Slutpunkt",
|
||||||
|
"settings.mcp.endpoint_hint": "Peka din agents MCP-klientkonfiguration mot {kind}, med token ovan som bearer-uppgift.",
|
||||||
|
"settings.mcp.kind_command": "kommando",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "Åtkomstlogg",
|
||||||
|
"settings.mcp.access_log_hint": "Varje verktygsläsning en agent gör, tillåten eller nekad (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "Ingen agent har läst något än.",
|
||||||
|
"settings.mcp.log_meeting": "möte {id}",
|
||||||
|
"settings.mcp.refresh": "Uppdatera",
|
||||||
|
|
||||||
|
"settings.privacy.title": "Integritet",
|
||||||
|
"settings.privacy.intro": "Vad WhispAssist faktiskt får skicka bort från den här enheten just nu. Med allt avstängt (standard) lämnar inget enheten alls.",
|
||||||
|
"settings.privacy.llm_endpoint": "LLM-slutpunkt",
|
||||||
|
"settings.privacy.off": "av",
|
||||||
|
"settings.privacy.local_only": "endast lokal",
|
||||||
|
"settings.privacy.leaves_device": "lämnar den här enheten",
|
||||||
|
"settings.privacy.sync_label": "Synkronisering",
|
||||||
|
"settings.privacy.enabled": "aktiverad",
|
||||||
|
"settings.privacy.mcp_label": "MCP-server",
|
||||||
|
"settings.privacy.mcp_on": "på · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "Inkommande endast på loopback — lägger inget till utgångslistan ovan. En ansluten agent kan ändå vidarebefordra det den läser till sin egen modellleverantör; se fliken för MCP-servern.",
|
||||||
|
"settings.privacy.egress_title": "Tillåtelselista för utgående",
|
||||||
|
"settings.privacy.no_egress": "Inga värdar är tillåtna — WA gör ingen utgående överföring av innehåll.",
|
||||||
|
"settings.privacy.sync_targets_title": "Synkroniseringsmål",
|
||||||
|
"settings.privacy.third_party": "tredjepart",
|
||||||
|
"settings.privacy.your_server": "din server",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "ingen TLS",
|
||||||
|
"settings.privacy.refresh": "Uppdatera",
|
||||||
|
"settings.privacy.unavailable": "Självkontroll av integritet ej tillgänglig.",
|
||||||
|
"settings.privacy.vault_title": "Krypteringsvalv",
|
||||||
|
"settings.privacy.vault_intro": "Kryptera anteckningar, transkriptioner och sammanfattningar i vila med ett lösenord. (Ljudfiler krypteras inte än.)",
|
||||||
|
"settings.privacy.vault_password": "Valvlösenord",
|
||||||
|
"settings.privacy.enable_vault": "Aktivera valv",
|
||||||
|
"settings.privacy.vault_pw_hint": "Använd minst 8 tecken. Om du glömmer det kan krypterat innehåll inte återställas.",
|
||||||
|
"settings.privacy.vault_locked_1": "Valvet är ",
|
||||||
|
"settings.privacy.locked_word": "låst",
|
||||||
|
"settings.privacy.vault_locked_2": ". Lås upp för att läsa krypterade möten.",
|
||||||
|
"settings.privacy.password": "Lösenord",
|
||||||
|
"settings.privacy.unlock": "Lås upp",
|
||||||
|
"settings.privacy.vault_unlocked_1": "Valvet är ",
|
||||||
|
"settings.privacy.unlocked_word": "upplåst",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". Nya anteckningar, transkriptioner och sammanfattningar krypteras i vila.",
|
||||||
|
"settings.privacy.lock_now": "Lås nu",
|
||||||
|
"settings.privacy.change_password": "Ändra lösenord",
|
||||||
|
"settings.privacy.current_password": "Nuvarande lösenord",
|
||||||
|
"settings.privacy.new_password": "Nytt lösenord",
|
||||||
|
|
||||||
|
"settings.about.title": "Om",
|
||||||
|
"settings.about.tagline": "En helt lokal, öppen källkods-, Windows-inbyggd mötesassistent.",
|
||||||
|
"settings.about.build_commit": "Build-commit",
|
||||||
|
|
||||||
|
"consent.heading": "Innan du spelar in",
|
||||||
|
"consent.body": "Att spela in samtal utan deltagarnas samtycke kan vara olagligt i din region. Kontrollera dina lokala inspelningslagar. Detta är en varning, inte juridisk rådgivning.",
|
||||||
|
"consent.accept": "Jag förstår — aktivera inspelning",
|
||||||
|
"consent.cancel": "Avbryt",
|
||||||
|
|
||||||
|
"hosted.heading": "Innan du använder {provider}",
|
||||||
|
"hosted.body_1": "Att generera med",
|
||||||
|
"hosted.body_2": "skickar det här mötets transkription till deras servrar — den lämnar den här enheten och omfattas av deras integritetspolicy. WhispAssist har ingen kontroll över datahanteringen när den väl lämnar din enhet. Detta är av som standard; du väljer det nu.",
|
||||||
|
"hosted.accept": "Jag förstår — fortsätt",
|
||||||
|
"hosted.cancel": "Avbryt",
|
||||||
|
|
||||||
|
"theme.group": "Tema",
|
||||||
|
"theme.system": "Följ systemet",
|
||||||
|
"theme.light": "Ljust",
|
||||||
|
"theme.dark": "Mörkt",
|
||||||
|
|
||||||
|
"import.dialog_label": "Lägg till ett möte från en fil eller URL",
|
||||||
|
"import.title": "Lägg till ett möte",
|
||||||
|
"import.close": "Stäng",
|
||||||
|
"import.close_title": "Stäng (Esc)",
|
||||||
|
"import.body": "Importera en befintlig inspelning — en lokal ljud-/videofil eller en länk (YouTube, en strömningssida eller en direkt medie-URL). Den transkriberas och diariseras precis som en direktinspelning.",
|
||||||
|
"import.file_or_url": "Fil eller URL",
|
||||||
|
"import.source_placeholder": "Klistra in en URL eller bläddra efter en fil…",
|
||||||
|
"import.choose_file": "Välj en lokal fil",
|
||||||
|
"import.browse": "Bläddra…",
|
||||||
|
"import.title_label": "Titel",
|
||||||
|
"import.optional": "valfritt",
|
||||||
|
"import.title_placeholder": "Standard är filnamnet",
|
||||||
|
"import.filter_av": "Ljud / video",
|
||||||
|
"import.requires_1": "Kräver",
|
||||||
|
"import.requires_2": "installerat och i din PATH (plus",
|
||||||
|
"import.requires_3": "för URL:er). WhispAssist levererar dem inte.",
|
||||||
|
"import.importing": "Importerar… detta kan ta en stund",
|
||||||
|
"import.import": "Importera",
|
||||||
|
"import.cancel": "Avbryt",
|
||||||
|
|
||||||
|
"tagchip.filter": "Filtrera möten taggade ”{tag}”",
|
||||||
|
"tagchip.remove": "Ta bort taggen {tag}",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "Ingångsnivå för system och mikrofon",
|
||||||
|
"levelmeter.system": "Ingångsnivå för system",
|
||||||
|
|
||||||
|
"app.tagline": "lokal · privat",
|
||||||
|
"app.note_template": "Anteckningsmall",
|
||||||
|
"app.no_template": "Ingen mall",
|
||||||
|
"app.add_meeting": "Lägg till möte",
|
||||||
|
"app.add_meeting_title": "Lägg till ett möte från en fil eller URL",
|
||||||
|
"app.record": "Spela in",
|
||||||
|
"app.record_title": "Starta inspelning (Ctrl+Skift+R)",
|
||||||
|
"app.stop": "Stoppa",
|
||||||
|
"app.stop_title": "Stoppa inspelning (Ctrl+Skift+R)",
|
||||||
|
"app.cancel": "Avbryt",
|
||||||
|
"app.cancel_title": "Kassera den här inspelningen och ta bort den",
|
||||||
|
"app.recording": "Spelar in…",
|
||||||
|
"app.backend_title": "Aktiv transkriberingsbackend",
|
||||||
|
"app.retention_title": "Spara ljud som .wav för det här mötet",
|
||||||
|
"app.saving": "sparar",
|
||||||
|
"app.not_saved": "inte sparat",
|
||||||
|
"app.reconnecting": "återansluter ljudenhet…",
|
||||||
|
"app.settings": "Inställningar",
|
||||||
|
"app.settings_title": "Inställningar (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "Kassera den här inspelningen? Dess ljud och transkription tas bort.",
|
||||||
|
"app.announce_started": "Inspelning startad",
|
||||||
|
"app.announce_paused": "Inspelning pausad",
|
||||||
|
"app.announce_stopped": "Inspelning stoppad",
|
||||||
|
"app.consent_dialog": "Samtycke till inspelning",
|
||||||
|
"app.vault_dialog": "Lås upp krypteringsvalvet",
|
||||||
|
"app.vault_title": "Lås upp krypteringsvalvet",
|
||||||
|
"app.vault_desc": "Dina möten är krypterade i vila. Ange ditt lösenord för att läsa dem.",
|
||||||
|
"app.vault_password": "Valvlösenord",
|
||||||
|
"app.vault_incorrect": "Felaktigt lösenord",
|
||||||
|
"app.unlock": "Lås upp",
|
||||||
|
"app.continue_locked": "Fortsätt låst",
|
||||||
|
"app.show_meetings": "Visa möteslista",
|
||||||
|
"app.hide_meetings": "Dölj möteslista",
|
||||||
|
"app.show_summary": "Visa sammanfattningspanel",
|
||||||
|
"app.hide_summary": "Dölj sammanfattningspanel",
|
||||||
|
"app.resize_meetings": "Ändra storlek på möteslistan",
|
||||||
|
"app.resize_summary": "Ändra storlek på sammanfattningspanelen",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "Sök möten…",
|
||||||
|
"meetings.search_aria": "Sök möten",
|
||||||
|
"meetings.filter_tag_aria": "Filtrera efter tagg",
|
||||||
|
"meetings.all_tags": "Alla taggar",
|
||||||
|
"meetings.from_date_aria": "Från datum",
|
||||||
|
"meetings.to_date_aria": "Till datum",
|
||||||
|
"meetings.bulk_format_aria": "Format för massexport",
|
||||||
|
"meetings.bulk_export": "Massexport",
|
||||||
|
"meetings.bulk_export_title": "Exportera alla möten som matchar tagg-/datumfiltren ovan",
|
||||||
|
"meetings.exporting": "Exporterar…",
|
||||||
|
"meetings.exported_one": "Exporterade {n} möte.",
|
||||||
|
"meetings.exported_many": "Exporterade {n} möten.",
|
||||||
|
"meetings.searching": "Söker…",
|
||||||
|
"meetings.loading": "Läser in möten…",
|
||||||
|
"meetings.no_matches": "Inga träffar.",
|
||||||
|
"meetings.empty": "Inga möten än — klicka på Spela in ovan för att börja.",
|
||||||
|
"meetings.status.recording": "spelar in",
|
||||||
|
"meetings.status.transcribing": "transkriberar",
|
||||||
|
"meetings.status.recovering": "återställer",
|
||||||
|
"meetings.status.error": "fel",
|
||||||
|
"meetings.resume": "Återuppta transkription",
|
||||||
|
"meetings.delete_aria": "Ta bort möte",
|
||||||
|
"meetings.delete_confirm": "Ta bort det här mötet? Detta tar bort dess inspelning, transkription och anteckningar.",
|
||||||
|
|
||||||
|
"transcript.heading": "Transkription",
|
||||||
|
"transcript.title_aria": "Mötestitel",
|
||||||
|
"transcript.lang_title": "Transkriberingsspråk",
|
||||||
|
"transcript.lang_auto": "identifierar…",
|
||||||
|
"transcript.show": "Visa transkription",
|
||||||
|
"transcript.hide": "Dölj transkription",
|
||||||
|
"transcript.empty": "Ingen transkription för det här mötet.",
|
||||||
|
"transcript.will_appear": "Transkriptionen visas här medan du spelar in.",
|
||||||
|
"transcript.play_from_here": "Spela upp härifrån",
|
||||||
|
"transcript.resize": "Ändra storlek på transkription och anteckningar",
|
||||||
|
"transcript.reprocess_placeholder": "Transkribera om med…",
|
||||||
|
"transcript.reprocess_lang_aria": "Språk för ombearbetning",
|
||||||
|
"transcript.reprocess_keep_lang": "Behåll nuvarande språk",
|
||||||
|
"transcript.reprocess_go": "Kör",
|
||||||
|
"transcript.retranscribing": "Transkriberar om…",
|
||||||
|
"transcript.has_note": "Har en anteckning",
|
||||||
|
"transcript.note_placeholder": "Lägg till en anteckning för det här ögonblicket…",
|
||||||
|
"transcript.note_aria": "Anteckning för den här transkriptionsraden",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Word-dokument",
|
||||||
|
"transcript.filter_obsidian": "Obsidian-anteckning",
|
||||||
|
|
||||||
|
"notes.heading": "Anteckningar",
|
||||||
|
"notes.show": "Visa anteckningar",
|
||||||
|
"notes.hide": "Dölj anteckningar",
|
||||||
|
"notes.placeholder": "Anteckningar…",
|
||||||
|
"notes.live_placeholder": "Skriv anteckningar medan du pratar…",
|
||||||
|
"notes.toolbar_aria": "Formatering av anteckningar",
|
||||||
|
"notes.bold": "Fet",
|
||||||
|
"notes.italic": "Kursiv",
|
||||||
|
"notes.h1": "Rubrik 1",
|
||||||
|
"notes.h2": "Rubrik 2",
|
||||||
|
"notes.bullet": "Punktlista",
|
||||||
|
"notes.checkbox_title": "Kryssruta",
|
||||||
|
"notes.checkbox_aria": "Listobjekt med kryssruta",
|
||||||
|
"notes.edit_raw": "Redigera rå markdown",
|
||||||
|
"notes.render": "Rendera markdown",
|
||||||
|
"notes.editor": "Redigerare",
|
||||||
|
"notes.preview": "Förhandsgranskning",
|
||||||
|
"notes.export_md_title": "Exportera anteckningar som .md",
|
||||||
|
"notes.export_pdf_title": "Exportera anteckningar som .pdf",
|
||||||
|
"notes.export_docx_title": "Exportera anteckningar som .docx",
|
||||||
|
"notes.export_bundle_title": "Exportera ljud + transkription + anteckningar till en mapp",
|
||||||
|
"notes.export_obsidian_title": "Exportera en Obsidian-anteckning (anteckningar + sammanfattning + transkription, inget ljud)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "Inspelning",
|
||||||
|
"summary.loading_recording": "Läser in inspelning…",
|
||||||
|
"summary.sync_heading": "Synkronisering",
|
||||||
|
"summary.uploading": "Laddar upp…",
|
||||||
|
"summary.upload_now": "Ladda upp nu",
|
||||||
|
"summary.no_uploads": "Inga uppladdningar för det här mötet än.",
|
||||||
|
"summary.retry": "försök igen",
|
||||||
|
"summary.tags_heading": "Taggar",
|
||||||
|
"summary.tags_select": "Välj ett möte för att tagga det.",
|
||||||
|
"summary.tag_add_placeholder": "Lägg till tagg…",
|
||||||
|
"summary.tag_first_placeholder": "projekt, kund, ämne…",
|
||||||
|
"summary.generating": "Genererar…",
|
||||||
|
"summary.generate_tags": "Generera taggar",
|
||||||
|
"summary.saving": "Sparar…",
|
||||||
|
"summary.save_tags": "Spara taggar",
|
||||||
|
"summary.summary_heading": "Sammanfattning",
|
||||||
|
"summary.ai_provider": "AI-leverantör",
|
||||||
|
"summary.provider_off": "Av",
|
||||||
|
"summary.provider_custom": "Anpassad",
|
||||||
|
"summary.local": "lokal",
|
||||||
|
"summary.leaves_device": "lämnar den här enheten",
|
||||||
|
"summary.this_hosted_provider": "denna hostade leverantör",
|
||||||
|
"summary.select_generate": "Välj ett möte för att generera en sammanfattning.",
|
||||||
|
"summary.decisions_heading": "Beslut",
|
||||||
|
"summary.regenerate": "Generera om",
|
||||||
|
"summary.generated_hint": "Genererad lokalt efter mötet (kräver en lokal LLM-leverantör).",
|
||||||
|
"summary.no_provider": "Ingen AI-leverantör konfigurerad — aktivera en i Inställningar.",
|
||||||
|
"summary.generate_summary": "Generera sammanfattning",
|
||||||
|
"summary.briefs_heading": "Funktionsbriefer",
|
||||||
|
"summary.briefs_select": "Välj ett möte för att skapa eller visa funktionsbriefer.",
|
||||||
|
"summary.briefs_desc": "Destillerar det här mötet till en agentfärdig specifikation (kräver en lokal LLM-leverantör) — lämna den till en kodningsagent, eller servera den över MCP-servern när den är på.",
|
||||||
|
"summary.brief_repo_placeholder": "Målrepo (valfritt), t.ex. acme/reporting-web",
|
||||||
|
"summary.distilling": "Destillerar…",
|
||||||
|
"summary.create_brief": "Skapa funktionsbrief",
|
||||||
|
"summary.brief_mcp_title": "Tillgänglig när MCP-servern är på",
|
||||||
|
"summary.copied": "Kopierad",
|
||||||
|
"summary.copy_md": "Kopiera som Markdown",
|
||||||
|
"summary.copy_json": "Kopiera som JSON",
|
||||||
|
"summary.brief_problem": "Problem",
|
||||||
|
"summary.brief_outcome": "Önskat resultat",
|
||||||
|
"summary.brief_criteria": "Acceptanskriterier",
|
||||||
|
"summary.brief_none": "Inget fångat.",
|
||||||
|
"summary.brief_context": "Sammanhang",
|
||||||
|
"summary.action_items_heading": "Åtgärdspunkter",
|
||||||
|
"summary.ai_select": "Välj ett möte för att se dess åtgärdspunkter.",
|
||||||
|
"summary.ai_empty": "Inga än — lägg till en nedan, eller generera en sammanfattning för att extrahera dem automatiskt.",
|
||||||
|
"summary.confirmed": "Bekräftad",
|
||||||
|
"summary.ai_text_placeholder": "Åtgärdspunkt…",
|
||||||
|
"summary.ai_text_aria": "Text för åtgärdspunkt",
|
||||||
|
"summary.owner": "Ansvarig",
|
||||||
|
"summary.due_date": "Förfallodatum",
|
||||||
|
"summary.reminder_title": "Schemalägg en lokal påminnelse för det här förfallodatumet",
|
||||||
|
"summary.ai_delete_title": "Ta bort den här åtgärdspunkten",
|
||||||
|
"summary.ai_delete_aria": "Ta bort åtgärdspunkt",
|
||||||
|
"summary.add_action_item": "Lägg till åtgärdspunkt",
|
||||||
|
"summary.save_action_items": "Spara åtgärdspunkter",
|
||||||
|
"summary.calendar_heading": "Kalenderhändelse",
|
||||||
|
"summary.cal_select": "Välj ett möte för att koppla det till en kalenderhändelse.",
|
||||||
|
"summary.event_search_placeholder": "Sök händelsetitel…",
|
||||||
|
"summary.linked_event": "Kopplad händelse",
|
||||||
|
"summary.change_event": "Byt händelse…",
|
||||||
|
"summary.link_event": "Koppla en händelse…",
|
||||||
|
"summary.untitled_event": "(utan titel)",
|
||||||
|
"summary.no_events_1": "Inga händelser importerade än — importera en",
|
||||||
|
"summary.no_events_2": "från Inställningar → Kalender.",
|
||||||
|
"summary.no_events_match": "Inga händelser matchar den här sökningen/datumet — prova att rensa en.",
|
||||||
|
"summary.organizer_label": "Organisatör: {name}",
|
||||||
|
"summary.participants_heading": "Deltagare",
|
||||||
|
"summary.participants_hint": "Fylls i från den kopplade kalenderhändelsen.",
|
||||||
|
"summary.speakers_heading": "Talare",
|
||||||
|
"summary.speakers_select": "Välj ett möte för att namnge dess talare.",
|
||||||
|
"summary.no_speakers": "Inga talare upptäckta än.",
|
||||||
|
"summary.speaker_name_placeholder": "Talarens namn",
|
||||||
|
"summary.save": "Spara",
|
||||||
|
"summary.cancel": "Avbryt",
|
||||||
|
"summary.name_speaker": "Namnge den här talaren…",
|
||||||
|
"summary.add_new_name": "+ Lägg till nytt namn…",
|
||||||
|
|
||||||
|
"settings.recording.title": "Inspelning",
|
||||||
|
"settings.recording.record_default": "Spela in möten som standard",
|
||||||
|
"settings.recording.save_audio_as": "spara ljud som",
|
||||||
|
"settings.recording.default_hint": "Av som standard. När det är av används ljud bara för att producera transkriptionen och tas bort när mötet slutförs. Du kan också växla inspelning per möte.",
|
||||||
|
"settings.recording.consent_label": "Samtycke: {status}.",
|
||||||
|
"settings.recording.consent_ack": "bekräftat",
|
||||||
|
"settings.recording.consent_not": "ännu inte bekräftat",
|
||||||
|
"settings.recording.auto_label": "Starta inspelning automatiskt när en kalenderhändelse börjar",
|
||||||
|
"settings.recording.auto_hint": "Endast medan WhispAssist är öppet. När starttiden för en importerad kalenderhändelse infaller startar en inspelning automatiskt (med din standardinställning för kvarhållning ovan). Inget körs i bakgrunden — timern är bara aktiverad medan appen körs. Importera händelser under Inställningar → Kalender."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "பதிவு",
|
||||||
|
"nav.hardware": "வன்பொருள்",
|
||||||
|
"nav.storage": "சேமிப்பகம்",
|
||||||
|
"nav.calendar": "நாட்காட்டி",
|
||||||
|
"nav.sync": "ஒத்திசைவு",
|
||||||
|
"nav.ai": "AI",
|
||||||
|
"nav.mcp": "MCP சேவையகம்",
|
||||||
|
"nav.privacy": "தனியுரிமை",
|
||||||
|
"nav.about": "பற்றி",
|
||||||
|
"nav.language": "மொழி",
|
||||||
|
|
||||||
|
"settings.language.title": "மொழி",
|
||||||
|
"settings.language.display": "காட்சி மொழி",
|
||||||
|
"settings.language.display_hint": "பயன்பாட்டு இடைமுகத்தின் மொழி. ஒரு மொழியைச் சேர்ப்பது ஒரே ஒரு மொழிபெயர்ப்பு கோப்பை இணைப்பது போலவே எளிது.",
|
||||||
|
|
||||||
|
"settings.transcription.title": "படியெடுப்பு மொழி",
|
||||||
|
"settings.transcription.label": "மொழி",
|
||||||
|
"settings.transcription.auto": "தானாகக் கண்டறி",
|
||||||
|
"settings.transcription.applies_hint": "அடுத்த பதிவுக்குப் பொருந்தும். உண்மையில் பயன்படுத்தப்பட்ட மொழி பின்னர் ஒவ்வொரு கூட்டத்திலும் காட்டப்படும்.",
|
||||||
|
"settings.transcription.english_only": "ஆங்கிலம் மட்டும்",
|
||||||
|
"settings.transcription.model_english_only": "“{model}” ஆங்கிலம் மட்டுமே.",
|
||||||
|
"settings.transcription.no_model": "எந்த மாதிரியும் தேர்ந்தெடுக்கப்படவில்லை.",
|
||||||
|
"settings.transcription.switch_multilingual": "ஒரு மொழியைத் தேர்ந்தெடுக்க மேலே ஒரு பன்மொழி மாதிரிக்கு மாறவும்.",
|
||||||
|
|
||||||
|
"settings.hardware.title": "வன்பொருள்",
|
||||||
|
"settings.hardware.active_backend": "செயலில் உள்ள பின்தளம்",
|
||||||
|
"settings.hardware.model_meta": "· மாதிரி {size}",
|
||||||
|
"settings.hardware.preferred_backend": "விருப்பமான பின்தளம்",
|
||||||
|
"settings.hardware.auto_backend": "தானியங்கி (சிறந்த கிடைக்கக்கூடியது)",
|
||||||
|
"settings.hardware.not_available": "கிடைக்கவில்லை",
|
||||||
|
"settings.hardware.fallback_hint": "தேர்ந்தெடுக்கப்பட்ட பின்தளம் ஏற்றப்படாவிட்டால் தானாகவே பின்வாங்கும் (NPU → NVIDIA → AMD → Intel → CPU).",
|
||||||
|
"settings.hardware.audio_devices": "ஒலி சாதனங்கள்",
|
||||||
|
"settings.hardware.recording_device": "பதிவு சாதனம்",
|
||||||
|
"settings.hardware.default_system_audio": "இயல்புநிலை கணினி ஒலி",
|
||||||
|
"settings.hardware.recording_device_hint": "இந்தச் சாதனம் இயக்குவதை (லூப்பேக்) WhispAssist பதிவு செய்கிறது — அழைப்பின் மறுபக்கம். Windows இன் கணினி இயல்புநிலையைப் பின்பற்ற விரும்பவில்லை என்றால் ஒரு குறிப்பிட்ட வெளியீட்டைத் தேர்ந்தெடுக்கவும்.",
|
||||||
|
"settings.hardware.microphone": "மைக்ரோஃபோன்",
|
||||||
|
"settings.hardware.mic_off": "ஆஃப் — என் மைக்ரோஃபோனைப் பிடிக்க வேண்டாம்",
|
||||||
|
"settings.hardware.default_mic": "இயல்புநிலை மைக்ரோஃபோன்",
|
||||||
|
"settings.hardware.mic_hint": "கூட்டத்தின் இரு பக்கங்களும் பதிவாகும் வகையில் உங்கள் சொந்தக் குரலை நேரடிப் படியெடுப்பில் சேர்க்கிறது. உங்கள் சாதனத்திலேயே இருக்கும் — எதுவும் பதிவேற்றப்படாது. மேலே உள்ள கணினி ஒலியை மட்டும் படியெடுக்க “ஆஃப்” தேர்ந்தெடுக்கவும்.",
|
||||||
|
"settings.hardware.npu_detected": "NPU முடுக்கி கண்டறியப்பட்டது",
|
||||||
|
"settings.hardware.ready": "தயார்",
|
||||||
|
"settings.hardware.downloading": "பதிவிறக்குகிறது…",
|
||||||
|
"settings.hardware.package_needed": "தொகுப்பு தேவை",
|
||||||
|
"settings.hardware.npu_hint": "உங்கள் NPU-க்கு ஒரு முறை முடுக்கல் தொகுப்பு (Whisper ONNX மாதிரி{extra}) தேவை. இது பின்னணியில் பதிவிறக்கப்படும்; நீங்கள் இப்போதே தொடங்கலாம்.",
|
||||||
|
"settings.hardware.plus_openvino": " + OpenVINO இயக்க நேரம்",
|
||||||
|
"settings.hardware.download_npu": "NPU தொகுப்பைப் பதிவிறக்கு",
|
||||||
|
"settings.hardware.dml_title": "GPU முடுக்கல் (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "உங்கள் GPU-வை Vulkan இல்லாமல் DirectML மூலம் முடுக்கலாம் — ஒரு முறை தொகுப்பு (Whisper ONNX மாதிரி{extra}).",
|
||||||
|
"settings.hardware.plus_directml": " + DirectML இயக்க நேரம்",
|
||||||
|
"settings.hardware.download_dml": "DirectML தொகுப்பைப் பதிவிறக்கு",
|
||||||
|
"settings.hardware.detection_unavailable": "வன்பொருள் கண்டறிதல் கிடைக்கவில்லை.",
|
||||||
|
"settings.hardware.low_overhead": "குறைந்த சுமை முன்னமைவு",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + மிகச் சிறிய மாதிரி, பேட்டரி/பின்னணி பயன்பாட்டிற்கு",
|
||||||
|
"settings.hardware.models": "மாதிரிகள்",
|
||||||
|
"settings.hardware.downloading_model": "{name} பதிவிறக்குகிறது",
|
||||||
|
"settings.hardware.active_badge": "செயலில்",
|
||||||
|
"settings.hardware.installed": "நிறுவப்பட்டது",
|
||||||
|
"settings.hardware.use": "பயன்படுத்து",
|
||||||
|
"settings.hardware.remove": "அகற்று",
|
||||||
|
"settings.hardware.cant_remove_active": "செயலில் உள்ள மாதிரியை அகற்ற முடியாது",
|
||||||
|
"settings.hardware.download": "பதிவிறக்கு",
|
||||||
|
"settings.hardware.diarization": "பேச்சாளர் பிரித்தல்",
|
||||||
|
"settings.hardware.diarization_hint": "இரு மாதிரிகளையும் நிறுவினால், முடிக்கப்பட்ட பதிவுகள் ஒரே தொடர் படியெடுப்புக்குப் பதிலாக பேச்சாளர் வாரியாக (பேச்சாளர் 1, பேச்சாளர் 2, …) பிரிக்கப்படும். முழுவதுமாக ஆஃப்லைனில் இயங்கும், நிறுத்தப்பட்ட பிறகு கூட்டத்திற்கு ஒரு முறை. அவை இல்லாமல், ஒவ்வொரு வரியும் ஒரே பேச்சாளருக்கு வழங்கப்படும்.",
|
||||||
|
|
||||||
|
"settings.storage.title": "சேமிப்பகம்",
|
||||||
|
"settings.storage.location_label": "இடம்",
|
||||||
|
"settings.storage.location_hint": "சேமிப்பக இடத்தை மாற்றுவது இன்னும் ஆதரிக்கப்படவில்லை — தற்போது கூட்டங்கள், மாதிரிகள் மற்றும் தரவுத்தளம் இங்கேதான் உள்ளன.",
|
||||||
|
"settings.storage.retention_title": "தக்கவைப்பு",
|
||||||
|
"settings.storage.retention_hint": "வரம்பைத் தாண்டிய பழைய கூட்டங்களைத் தானாகவே நீக்குகிறது. தொடக்கத்தில் ஒரு முறை சரிபார்க்கப்படும்; காலியாக இருந்தால் வரம்பு இல்லை. தற்போது பதிவாகும் கூட்டத்தை ஒருபோதும் தொடாது.",
|
||||||
|
"settings.storage.max_age": "அதிகபட்ச வயது (நாட்கள்)",
|
||||||
|
"settings.storage.max_size": "அதிகபட்ச அளவு (GB)",
|
||||||
|
"settings.storage.no_limit": "வரம்பு இல்லை",
|
||||||
|
"settings.storage.export_import_title": "ஏற்றுமதி & இறக்குமதி",
|
||||||
|
"settings.storage.export_hint_1": "பதிவுகளைக் கணினிகளுக்கு இடையே கொண்டுசெல்லக்கூடிய தொகுப்பு கோப்புறைகளாக நகர்த்தவும் (ஒலி, படியெடுப்பு, குறிப்புகள், சுருக்கம் மற்றும் ஒரு",
|
||||||
|
"settings.storage.export_hint_2": "மேனிஃபெஸ்ட்). எந்த கோப்புறைக்கும் ஏற்றுமதி செய்யவும் — ஒத்திசைக்கப்பட்ட இயக்கி, USB ஸ்டிக், அல்லது ஒரு ஒத்திசைவு இலக்கின் உள்ளூர் மவுன்ட் — பிறகு மற்ற இயந்திரத்தில் அதை இறக்குமதி செய்யவும். இறக்குமதி செய்யப்பட்ட கூட்டங்களுக்குப் புதிய id கிடைக்கும், எனவே மீண்டும் இறக்குமதி செய்வது எதையும் மேலெழுதாது.",
|
||||||
|
"settings.storage.export_dialog_title": "அனைத்து கூட்டங்களையும் இங்கே ஏற்றுமதி செய்…",
|
||||||
|
"settings.storage.import_dialog_title": "ஒரு தொகுப்பை (அல்லது தொகுப்புகளின் கோப்புறையை) இறக்குமதி செய்…",
|
||||||
|
"settings.storage.exporting": "ஏற்றுமதி செய்கிறது…",
|
||||||
|
"settings.storage.export_all": "அனைத்து கூட்டங்களையும் ஏற்றுமதி செய்…",
|
||||||
|
"settings.storage.importing": "இறக்குமதி செய்கிறது…",
|
||||||
|
"settings.storage.import_all": "கூட்டங்களை இறக்குமதி செய்…",
|
||||||
|
"settings.storage.exported_one": "{n} கூட்டம் ஏற்றுமதி செய்யப்பட்டது.",
|
||||||
|
"settings.storage.exported_many": "{n} கூட்டங்கள் ஏற்றுமதி செய்யப்பட்டன.",
|
||||||
|
"settings.storage.imported_one": "{n} கூட்டம் இறக்குமதி செய்யப்பட்டது.",
|
||||||
|
"settings.storage.imported_many": "{n} கூட்டங்கள் இறக்குமதி செய்யப்பட்டன.",
|
||||||
|
|
||||||
|
"settings.calendar.title": "நாட்காட்டி & Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "ஒரு உள்ளூர் Outlook காப்புப்பிரதியிலிருந்து நிகழ்வுகளையும் பங்கேற்பாளர்களையும் இறக்குமதி செய்யவும்",
|
||||||
|
"settings.calendar.intro_2": "— படிக்க மட்டும், கோப்பிற்கு எதுவும் மீண்டும் எழுதப்படாது. இந்தச் சாதனத்திலிருந்து எதுவும் வெளியேறாது.",
|
||||||
|
"settings.calendar.pst_file": ".pst கோப்பு",
|
||||||
|
"settings.calendar.no_file": "கோப்பு எதுவும் தேர்ந்தெடுக்கப்படவில்லை",
|
||||||
|
"settings.calendar.browse": "உலாவு…",
|
||||||
|
"settings.calendar.password": "கடவுச்சொல்",
|
||||||
|
"settings.calendar.password_note": "அரிதாகவே தேவை",
|
||||||
|
"settings.calendar.optional": "விருப்பமானது",
|
||||||
|
"settings.calendar.import_range": "இறக்குமதி வரம்பு",
|
||||||
|
"settings.calendar.range_30": "கடந்த 30 நாட்கள்",
|
||||||
|
"settings.calendar.range_90": "கடந்த 90 நாட்கள்",
|
||||||
|
"settings.calendar.range_180": "கடந்த 6 மாதங்கள்",
|
||||||
|
"settings.calendar.range_365": "கடந்த 1 ஆண்டு",
|
||||||
|
"settings.calendar.range_all": "எல்லா நேரமும்",
|
||||||
|
"settings.calendar.range_hint": "நீண்ட காலம் இருக்கும் அஞ்சல்பெட்டியில் பல ஆண்டுகளின் தொடர்/விடுமுறை உள்ளீடுகள் இருக்கலாம் — வரம்பைக் குறுக்குவது இறக்குமதி செய்யப்பட்ட நாட்காட்டியை உண்மையில் தொடர்புடையவற்றுக்கு மட்டும் வைத்திருக்கும். இந்த இறக்குமதி பொத்தானுக்கும் தொடக்கத்தில் தானியங்கி மறு இறக்குமதிக்கும் இது பொருந்தும்.",
|
||||||
|
"settings.calendar.importing": "இறக்குமதி செய்கிறது…",
|
||||||
|
"settings.calendar.import": "இறக்குமதி செய்",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} நிகழ்வுகள்",
|
||||||
|
"settings.calendar.auto_reimport": "தொடக்கத்தில் இந்தக் கோப்பைத் தானாக மீண்டும் இறக்குமதி செய்",
|
||||||
|
"settings.calendar.auto_reimport_hint": "டைமரில் அல்ல, தொடக்கத்தில் ஒரு முறை இயங்கும் — மீண்டும் இறக்குமதி செய்வது பாதுகாப்பாக மீண்டும் செய்யலாம் (இருக்கும் நிகழ்வுகள் பொருந்திப் புதுப்பிக்கப்படும், நகல் எடுக்கப்படாது).",
|
||||||
|
"settings.calendar.import_failed": "இறக்குமதி தோல்வி: {error} — கோப்பே தொடப்படாது; பாதையைச் சரிபார்த்து மீண்டும் முயற்சிக்கவும்.",
|
||||||
|
"settings.calendar.cleanup_title": "சுத்தம் செய்",
|
||||||
|
"settings.calendar.cleanup_hint": "பதிவு செய்யப்பட்ட கூட்டத்துடன் ஏற்கனவே இணைக்கப்பட்ட நிகழ்வுகள், கீழே எந்த விருப்பத்தைத் தேர்ந்தெடுத்தாலும், எப்போதும் வைத்திருக்கப்படும்.",
|
||||||
|
"settings.calendar.cleanup_30": "30 நாட்களுக்கு மேற்பட்டவை",
|
||||||
|
"settings.calendar.cleanup_90": "90 நாட்களுக்கு மேற்பட்டவை",
|
||||||
|
"settings.calendar.cleanup_180": "6 மாதங்களுக்கு மேற்பட்டவை",
|
||||||
|
"settings.calendar.cleanup_365": "1 ஆண்டுக்கு மேற்பட்டவை",
|
||||||
|
"settings.calendar.cleanup_all": "அனைத்தையும் நீக்கு",
|
||||||
|
"settings.calendar.cleaning": "சுத்தம் செய்கிறது…",
|
||||||
|
"settings.calendar.cleanup_btn": "நாட்காட்டியை சுத்தம் செய்",
|
||||||
|
"settings.calendar.cleanup_result": "{deleted}{extra} நீக்கப்பட்டன.",
|
||||||
|
"settings.calendar.cleanup_kept": ", {n} வைக்கப்பட்டன (ஒரு கூட்டத்துடன் இணைக்கப்பட்டவை)",
|
||||||
|
"settings.calendar.cleanup_failed": "சுத்தம் செய்தல் தோல்வி: {error}",
|
||||||
|
"settings.calendar.imported_events": "இறக்குமதி செய்யப்பட்ட நிகழ்வுகள்",
|
||||||
|
"settings.calendar.no_events": "இன்னும் நிகழ்வுகள் எதுவும் இறக்குமதி செய்யப்படவில்லை.",
|
||||||
|
"settings.calendar.search_title": "தலைப்பைத் தேடு",
|
||||||
|
"settings.calendar.search_placeholder": "கூட்டத்தின் பெயர்…",
|
||||||
|
"settings.calendar.date_label": "தேதி",
|
||||||
|
"settings.calendar.events_count": "{total} இல் {shown} நிகழ்வுகள்",
|
||||||
|
"settings.calendar.untitled": "(தலைப்பில்லாதது)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "அமைப்புகள்",
|
||||||
|
"settings.close": "மூடு",
|
||||||
|
"settings.close_title": "மூடு (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "முன்னோட்ட முறை — ஒத்திசைவு பின்தளம் இன்னும் செயல்படுத்தப்படவில்லை (கட்டம் 9). மாற்றங்கள் இடைமுகத்தில் மட்டுமே வைக்கப்படும்.",
|
||||||
|
"settings.sync.title": "ஒத்திசைவு & பதிவேற்றம்",
|
||||||
|
"settings.sync.enable_label": "கட்டமைக்கப்பட்ட இலக்குகளுக்கு கூட்ட கலைப்பொருட்களைப் பதிவேற்றுவதை இயக்கு",
|
||||||
|
"settings.sync.enable_hint": "இயல்பாக ஆஃப். இது இயக்கப்பட்டு ஒரு இலக்கு இயக்கப்படாத வரை எதுவும் பதிவேற்றப்படாது. சுய-ஹோஸ்ட் செய்யப்பட்ட இலக்குகள் தரவை உங்கள் சொந்த சேவையகத்தில் வைத்திருக்கும்; மூன்றாம் தரப்பு கிளவுட்கள் தெளிவாகக் குறிக்கப்பட்டுள்ளன.",
|
||||||
|
"settings.sync.targets_title": "இலக்குகள்",
|
||||||
|
"settings.sync.no_targets": "இன்னும் இலக்குகள் இல்லை. கீழே ஒன்றைச் சேர்க்கவும்.",
|
||||||
|
"settings.sync.third_party": "மூன்றாம் தரப்பு",
|
||||||
|
"settings.sync.your_server": "உங்கள் சேவையகம்",
|
||||||
|
"settings.sync.edit": "திருத்து",
|
||||||
|
"settings.sync.remove": "அகற்று",
|
||||||
|
"settings.sync.edit_target": "இலக்கைத் திருத்து",
|
||||||
|
"settings.sync.add_target": "ஒரு இலக்கைச் சேர்",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV, Nextcloud, ownCloud, Cloudreve, Seafile மற்றும் Synology ஆகியவற்றை உள்ளடக்கியது. (Seafile: சேவையக பக்கத்தில் SeafDAV ஐ இயக்கவும்.)",
|
||||||
|
"settings.sync.name": "பெயர்",
|
||||||
|
"settings.sync.name_placeholder": "வீட்டு Nextcloud",
|
||||||
|
"settings.sync.provider": "வழங்குநர்",
|
||||||
|
"settings.sync.server_url": "சேவையக URL",
|
||||||
|
"settings.sync.server_url_note_1": "உங்கள் சேவையக URL மட்டும் — WhispAssist தானாகவே",
|
||||||
|
"settings.sync.server_url_note_2": "சேர்க்கிறது.",
|
||||||
|
"settings.sync.remote_folder": "தொலைநிலை கோப்புறை",
|
||||||
|
"settings.sync.username": "பயனர்பெயர்",
|
||||||
|
"settings.sync.app_password": "பயன்பாட்டு கடவுச்சொல்",
|
||||||
|
"settings.sync.pw_keep": "தற்போதைய கடவுச்சொல்லை வைத்திருக்க காலியாக விடவும்",
|
||||||
|
"settings.sync.pw_store": "OS சான்றுச் சேமிப்பில் சேமிக்கப்படுகிறது",
|
||||||
|
"settings.sync.upload_legend": "பதிவேற்று",
|
||||||
|
"settings.sync.artifact_transcript": "படியெடுப்பு",
|
||||||
|
"settings.sync.artifact_notes": "குறிப்புகள்",
|
||||||
|
"settings.sync.artifact_summary": "சுருக்கம்",
|
||||||
|
"settings.sync.artifact_recording": "பதிவு (.wav, வைக்கப்பட்டால்)",
|
||||||
|
"settings.sync.allow_plaintext": "ஒரு LAN முகவரிக்கு எளிய உரை http ஐ அனுமதி (பரிந்துரைக்கப்படவில்லை)",
|
||||||
|
"settings.sync.encrypt_before": "பதிவேற்றுவதற்கு முன் மறையாக்கு (இலக்கு மறையாக்க உரையை மட்டுமே சேமிக்கும்)",
|
||||||
|
"settings.sync.requires_vault": "— திறக்கப்பட்ட பெட்டகம் தேவை",
|
||||||
|
"settings.sync.test_connection": "இணைப்பைச் சோதி",
|
||||||
|
"settings.sync.save_changes": "மாற்றங்களைச் சேமி",
|
||||||
|
"settings.sync.add_target_btn": "இலக்கைச் சேர்",
|
||||||
|
"settings.sync.cancel": "ரத்து செய்",
|
||||||
|
"settings.sync.third_party_banner": "{kind} ஒரு மூன்றாம் தரப்பு கிளவுட் — பதிவேற்றுவது உங்கள் தரவை உங்கள் சாதனத்திற்கு வெளியே {kind}-க்கு அனுப்புகிறது.",
|
||||||
|
"settings.sync.link_account": "{kind} கணக்கை இணை…",
|
||||||
|
"settings.sync.oauth_hint": "ஒரு OAuth உள்நுழைவைத் திறக்கிறது (லூப்பேக் திருப்பிவிடல்); டோக்கன் உங்கள் OS சான்றுச் சேமிப்பில் சேமிக்கப்படுகிறது.",
|
||||||
|
"settings.sync.footnote": "சான்றுகள் ஒருபோதும் அமைப்புகளிலோ தரவுத்தளத்திலோ எழுதப்படாது — OS சான்றுச் சேமிப்பில் மட்டுமே. LAN அல்லாத இலக்குகளுக்கு TLS தேவை.",
|
||||||
|
|
||||||
|
"settings.ai.title": "AI சுருக்க வழங்குநர்",
|
||||||
|
"settings.ai.intro_1": "சுருக்கங்கள் இயல்பாக ஒரு உள்ளூர் LLM இல் இயங்கும். இதை இந்த PC அல்லது உங்கள் LAN இல் உள்ள மற்றொரு இயந்திரத்தில் உள்ள Ollama-வை நோக்கிச் சுட்டவும் (எ.கா.",
|
||||||
|
"settings.ai.intro_2": "— இரண்டும் உள்ளூராகக் கருதப்படும், எனவே உங்கள் நெட்வொர்க்கிலிருந்து எதுவும் வெளியேறாது. ஹோஸ்ட் செய்யப்பட்ட வழங்குநர்கள் (Anthropic) விருப்பமானவை, இயல்பாக ஆஃப், மேலும் நீங்கள் ஒன்றை இயக்கியவுடன் படியெடுப்பை ஒரு மூன்றாம் தரப்பினருக்கு அனுப்பும்.",
|
||||||
|
"settings.ai.provider": "வழங்குநர்",
|
||||||
|
"settings.ai.off": "ஆஃப்",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (உள்ளூர் / LAN)",
|
||||||
|
"settings.ai.provider_custom": "தனிப்பயன் (OpenAI-இணக்கமான)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — ஹோஸ்ட் செய்யப்பட்டது, இந்தச் சாதனத்தை விட்டு வெளியேறுகிறது",
|
||||||
|
"settings.ai.endpoint": "இறுதிப்புள்ளி",
|
||||||
|
"settings.ai.model": "மாதிரி",
|
||||||
|
"settings.ai.api_key": "API விசை",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (ஏற்கனவே அமைக்கப்பட்டது — வைத்திருக்க காலியாக விடவும்)",
|
||||||
|
"settings.ai.show_api_key": "API விசையைக் காட்டு",
|
||||||
|
"settings.ai.hide_api_key": "API விசையை மறை",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic ஒரு ஹோஸ்ட் செய்யப்பட்ட மூன்றாம் தரப்பு சேவை — நீங்கள் ஒரு சுருக்கத்தை உருவாக்கும்போது இந்தக் கூட்டத்தின் படியெடுப்பு உங்கள் சாதனத்தை விட்டு வெளியேறுகிறது.",
|
||||||
|
"settings.ai.endpoint_banner": "இந்த இறுதிப்புள்ளி உங்கள் இயந்திரத்திலோ LAN இலோ இல்லை — உங்கள் படியெடுப்பு உங்கள் நெட்வொர்க்கை விட்டு வெளியேறும்.",
|
||||||
|
"settings.ai.saving": "சேமிக்கிறது…",
|
||||||
|
"settings.ai.save": "சேமி",
|
||||||
|
"settings.ai.test_connection": "இணைப்பைச் சோதி",
|
||||||
|
"settings.ai.this_hosted_provider": "இந்த ஹோஸ்ட் செய்யப்பட்ட வழங்குநர்",
|
||||||
|
"settings.ai.reachable": "அணுகக்கூடியது",
|
||||||
|
"settings.ai.unreachable": "அணுக முடியாதது",
|
||||||
|
"settings.ai.model_count_one": "· {n} மாதிரி",
|
||||||
|
"settings.ai.model_count_many": "· {n} மாதிரிகள்",
|
||||||
|
"settings.ai.on_lan": "· உங்கள் இயந்திரத்தில் / LAN இல்",
|
||||||
|
"settings.ai.leaves_network": "· உங்கள் நெட்வொர்க்கை விட்டு வெளியேறுகிறது",
|
||||||
|
"settings.ai.advanced_title": "மேம்பட்ட Ollama கட்டமைப்பு",
|
||||||
|
"settings.ai.changed": "{n} மாற்றப்பட்டன",
|
||||||
|
"settings.ai.reset_all": "அனைத்தையும் இயல்புநிலைக்கு மீட்டமை",
|
||||||
|
"settings.ai.system_prompt": "கணினி வினாத்தூண்டல்",
|
||||||
|
"settings.ai.system_prompt_placeholder": "எ.கா. நீங்கள் ஒரு சுருக்கமான கூட்டச் சுருக்கர்.",
|
||||||
|
"settings.ai.system_prompt_hint": "WhispAssist இன் தேவையான வெளியீட்டு வடிவமைப்பிற்கு முன் சேர்க்கப்படும், அதனால் உங்கள் வழிமுறைகள் சுருக்கம்/செயல்-உருப்படி பாகுபடுத்தலை உடைக்க முடியாது.",
|
||||||
|
"settings.ai.think": "சிந்தி",
|
||||||
|
"settings.ai.think_help": "பகுத்தறிவு முயற்சி (பகுத்தறிவு மாதிரிகள் மட்டும்).",
|
||||||
|
"settings.ai.keep_alive": "செயலில் வைத்திரு",
|
||||||
|
"settings.ai.keep_alive_help": "மாதிரி RAM இல் எவ்வளவு நேரம் இருக்கும். எ.கா. 5m, 1h, 0 (இறக்கு), -1 (எப்போதும்).",
|
||||||
|
"settings.ai.runtime_hardware": "இயக்க நேரம் & வன்பொருள்",
|
||||||
|
"settings.ai.rarely_needed": "— அரிதாகவே தேவை",
|
||||||
|
"settings.ai.save_advanced": "மேம்பட்டதைச் சேமி",
|
||||||
|
"settings.ai.saved": "சேமிக்கப்பட்டது",
|
||||||
|
"settings.ai.turn_off": "அணை",
|
||||||
|
"settings.ai.default_value": "இயல்புநிலை {value}.",
|
||||||
|
"settings.ai.reset_default": "இயல்புநிலைக்கு மீட்டமை",
|
||||||
|
"settings.ai.reset_field": "{name} ஐ மீட்டமை",
|
||||||
|
|
||||||
|
"settings.mcp.title": "MCP சேவையகம்",
|
||||||
|
"settings.mcp.banner_1": "இது உங்கள் சொந்த குறியீட்டு முகவரை (Claude Code, Codex, Copilot, OpenCode, …) உங்கள் உள்ளூர் இயந்திரத்தில் கூட்டச் சூழலை எடுக்க அனுமதிக்கிறது. இணைக்கப்பட்டவுடன்,",
|
||||||
|
"settings.mcp.banner_agent": "அந்த முகவர்",
|
||||||
|
"settings.mcp.banner_2": "தான் படிப்பதை அதன் சொந்த மாதிரி வழங்குநரின் கிளவுட்டிற்கு அனுப்பலாம் — WhispAssist இன் கட்டுப்பாட்டிற்கு வெளியே. WhispAssist தானே இந்தத் தரவை எங்கும் அனுப்பாது; சேவையகம் இந்தச் சாதனத்தில் மட்டுமே கேட்கிறது (",
|
||||||
|
"settings.mcp.banner_3": "), மேலும் ஒவ்வொரு வாசிப்பும் கீழே பதிவு செய்யப்படுகிறது.",
|
||||||
|
"settings.mcp.enable": "MCP சேவையகத்தை இயக்கு",
|
||||||
|
"settings.mcp.enable_hint": "இயல்பாக ஆஃப். லூப்பேக் மட்டும், டோக்கன்-பாதுகாக்கப்பட்டது — நெட்வொர்க்கிலிருந்து எதையும் அணுக முடியாது.",
|
||||||
|
"settings.mcp.transport": "போக்குவரத்து",
|
||||||
|
"settings.mcp.transport_http": "ஸ்ட்ரீமிங் HTTP",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (முகவர் ஒரு செயல்முறையை உருவாக்குகிறது)",
|
||||||
|
"settings.mcp.port": "போர்ட்",
|
||||||
|
"settings.mcp.transport_change_hint": "போக்குவரத்து/போர்ட்டை மாற்ற, முதலில் சேவையகத்தை அணைத்து, பின்னர் மீண்டும் இயக்கவும்.",
|
||||||
|
"settings.mcp.scope_title": "வரம்பு",
|
||||||
|
"settings.mcp.expose": "வெளிப்படுத்து",
|
||||||
|
"settings.mcp.scope_none": "எதுவும் இல்லை — எதுவும் பகிரப்படாது",
|
||||||
|
"settings.mcp.scope_selected": "தேர்ந்தெடுக்கப்பட்டவை — நீங்கள் பகிரப்பட்டதாகக் குறித்த அம்ச சுருக்கங்கள் மட்டும்",
|
||||||
|
"settings.mcp.scope_all": "அனைத்தும் — கூட்டங்கள், படியெடுப்புகள், செயல்-உருப்படிகள் மற்றும் பகிரப்பட்ட சுருக்கங்கள்",
|
||||||
|
"settings.mcp.expose_recordings": "சேமிக்கப்பட்ட பதிவு உள்ள கூட்டங்களையும் அனுமதி (இயல்பாக ஆஃப் — இது இயக்கப்படும் வரை, பதிவு செய்யப்பட்ட கூட்டத்தின் படியெடுப்பு “அனைத்தும்” வரம்பிலும் தடுக்கப்படும்)",
|
||||||
|
"settings.mcp.token_title": "புதிய அங்கீகார டோக்கன் — ஒரு முறை காட்டப்படும், இப்போதே அதை நகலெடுக்கவும்",
|
||||||
|
"settings.mcp.token_hint": "இது மீண்டும் காட்டப்படாது. இது உங்கள் OS சான்றுச் சேமிப்பில் சேமிக்கப்படுகிறது; அதை இழந்தால், புதிதாக ஒன்றை உருவாக்க சேவையகத்தை அணைத்து மீண்டும் இயக்கவும்.",
|
||||||
|
"settings.mcp.copied": "நகலெடுக்கப்பட்டது",
|
||||||
|
"settings.mcp.copy": "நகலெடு",
|
||||||
|
"settings.mcp.endpoint": "இறுதிப்புள்ளி",
|
||||||
|
"settings.mcp.endpoint_hint": "உங்கள் முகவரின் MCP கிளையன்ட் கட்டமைப்பை {kind}-ஐ நோக்கிச் சுட்டவும், மேலே உள்ள டோக்கனை bearer சான்றாகப் பயன்படுத்தவும்.",
|
||||||
|
"settings.mcp.kind_command": "கட்டளை",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "அணுகல் பதிவு",
|
||||||
|
"settings.mcp.access_log_hint": "ஒரு முகவர் செய்யும் ஒவ்வொரு கருவி வாசிப்பும், அனுமதிக்கப்பட்டதோ மறுக்கப்பட்டதோ (FR-MCP-5).",
|
||||||
|
"settings.mcp.no_reads": "இன்னும் எந்த முகவரும் எதையும் படிக்கவில்லை.",
|
||||||
|
"settings.mcp.log_meeting": "கூட்டம் {id}",
|
||||||
|
"settings.mcp.refresh": "புதுப்பி",
|
||||||
|
|
||||||
|
"settings.privacy.title": "தனியுரிமை",
|
||||||
|
"settings.privacy.intro": "WhispAssist தற்போது உண்மையில் இந்தச் சாதனத்திலிருந்து வெளியே அனுப்ப அனுமதிக்கப்பட்டவை. எல்லாம் ஆஃப் ஆக இருக்கும்போது (இயல்புநிலை), சாதனத்திலிருந்து எதுவும் வெளியேறாது.",
|
||||||
|
"settings.privacy.llm_endpoint": "LLM இறுதிப்புள்ளி",
|
||||||
|
"settings.privacy.off": "ஆஃப்",
|
||||||
|
"settings.privacy.local_only": "உள்ளூர் மட்டும்",
|
||||||
|
"settings.privacy.leaves_device": "இந்தச் சாதனத்தை விட்டு வெளியேறுகிறது",
|
||||||
|
"settings.privacy.sync_label": "ஒத்திசைவு",
|
||||||
|
"settings.privacy.enabled": "இயக்கப்பட்டது",
|
||||||
|
"settings.privacy.mcp_label": "MCP சேவையகம்",
|
||||||
|
"settings.privacy.mcp_on": "ஆன் · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "லூப்பேக்கில் மட்டும் உள்வரும் — மேலே உள்ள வெளிச்செல்லும் பட்டியலில் எதையும் சேர்க்காது. இணைக்கப்பட்ட முகவர் இன்னும் தான் படிப்பதை அதன் சொந்த மாதிரி வழங்குநருக்கு அனுப்பலாம்; MCP சேவையகத் தாவலைப் பார்க்கவும்.",
|
||||||
|
"settings.privacy.egress_title": "வெளிச்செல்லும் அனுமதிப் பட்டியல்",
|
||||||
|
"settings.privacy.no_egress": "எந்த ஹோஸ்ட்டும் அனுமதிப் பட்டியலில் இல்லை — WA எந்த உள்ளடக்க வெளிச்செல்லுதலையும் செய்யாது.",
|
||||||
|
"settings.privacy.sync_targets_title": "ஒத்திசைவு இலக்குகள்",
|
||||||
|
"settings.privacy.third_party": "மூன்றாம் தரப்பு",
|
||||||
|
"settings.privacy.your_server": "உங்கள் சேவையகம்",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "TLS இல்லை",
|
||||||
|
"settings.privacy.refresh": "புதுப்பி",
|
||||||
|
"settings.privacy.unavailable": "தனியுரிமை சுய-சோதனை கிடைக்கவில்லை.",
|
||||||
|
"settings.privacy.vault_title": "மறையாக்கப் பெட்டகம்",
|
||||||
|
"settings.privacy.vault_intro": "ஒரு கடவுச்சொல்லுடன் குறிப்புகள், படியெடுப்புகள் மற்றும் சுருக்கங்களை ஓய்வில் மறையாக்கு. (ஒலிக் கோப்புகள் இன்னும் மறையாக்கப்படவில்லை.)",
|
||||||
|
"settings.privacy.vault_password": "பெட்டகக் கடவுச்சொல்",
|
||||||
|
"settings.privacy.enable_vault": "பெட்டகத்தை இயக்கு",
|
||||||
|
"settings.privacy.vault_pw_hint": "குறைந்தது 8 எழுத்துகளைப் பயன்படுத்தவும். மறந்துவிட்டால், மறையாக்கப்பட்ட உள்ளடக்கத்தை மீட்க முடியாது.",
|
||||||
|
"settings.privacy.vault_locked_1": "பெட்டகம் ",
|
||||||
|
"settings.privacy.locked_word": "பூட்டப்பட்டுள்ளது",
|
||||||
|
"settings.privacy.vault_locked_2": ". மறையாக்கப்பட்ட கூட்டங்களைப் படிக்க திறக்கவும்.",
|
||||||
|
"settings.privacy.password": "கடவுச்சொல்",
|
||||||
|
"settings.privacy.unlock": "திற",
|
||||||
|
"settings.privacy.vault_unlocked_1": "பெட்டகம் ",
|
||||||
|
"settings.privacy.unlocked_word": "திறக்கப்பட்டுள்ளது",
|
||||||
|
"settings.privacy.vault_unlocked_2": ". புதிய குறிப்புகள், படியெடுப்புகள் மற்றும் சுருக்கங்கள் ஓய்வில் மறையாக்கப்படும்.",
|
||||||
|
"settings.privacy.lock_now": "இப்போது பூட்டு",
|
||||||
|
"settings.privacy.change_password": "கடவுச்சொல்லை மாற்று",
|
||||||
|
"settings.privacy.current_password": "தற்போதைய கடவுச்சொல்",
|
||||||
|
"settings.privacy.new_password": "புதிய கடவுச்சொல்",
|
||||||
|
|
||||||
|
"settings.about.title": "பற்றி",
|
||||||
|
"settings.about.tagline": "முழுவதுமாக உள்ளூர், திறந்த மூல, Windows-நேட்டிவ் கூட்ட உதவியாளர்.",
|
||||||
|
"settings.about.build_commit": "பில்ட் கமிட்",
|
||||||
|
|
||||||
|
"consent.heading": "பதிவு செய்வதற்கு முன்",
|
||||||
|
"consent.body": "பங்கேற்பாளர்களின் ஒப்புதல் இல்லாமல் உரையாடல்களைப் பதிவு செய்வது உங்கள் பகுதியில் சட்டவிரோதமாக இருக்கலாம். உங்கள் உள்ளூர் பதிவுச் சட்டங்களைச் சரிபார்க்கவும். இது ஒரு எச்சரிக்கை, சட்ட ஆலோசனை அல்ல.",
|
||||||
|
"consent.accept": "நான் புரிந்துகொள்கிறேன் — பதிவை இயக்கு",
|
||||||
|
"consent.cancel": "ரத்து செய்",
|
||||||
|
|
||||||
|
"hosted.heading": "{provider} ஐப் பயன்படுத்துவதற்கு முன்",
|
||||||
|
"hosted.body_1": "இதன் மூலம் உருவாக்குவது",
|
||||||
|
"hosted.body_2": "இந்தக் கூட்டத்தின் படியெடுப்பை அவர்களின் சேவையகங்களுக்கு அனுப்புகிறது — இது இந்தச் சாதனத்தை விட்டு வெளியேறி, அவர்களின் தனியுரிமைக் கொள்கைக்கு உட்பட்டது. தரவு உங்கள் சாதனத்தை விட்டு வெளியேறியவுடன் அதன் கையாளுதலின் மீது WhispAssist-க்கு எந்தக் கட்டுப்பாடும் இல்லை. இது இயல்பாக ஆஃப்; நீங்கள் இப்போது அதைத் தேர்ந்தெடுக்கிறீர்கள்.",
|
||||||
|
"hosted.accept": "நான் புரிந்துகொள்கிறேன் — தொடர்",
|
||||||
|
"hosted.cancel": "ரத்து செய்",
|
||||||
|
|
||||||
|
"theme.group": "தீம்",
|
||||||
|
"theme.system": "கணினியுடன் பொருந்து",
|
||||||
|
"theme.light": "வெளிர்",
|
||||||
|
"theme.dark": "இருள்",
|
||||||
|
|
||||||
|
"import.dialog_label": "ஒரு கோப்பு அல்லது URL இலிருந்து கூட்டத்தைச் சேர்",
|
||||||
|
"import.title": "கூட்டத்தைச் சேர்",
|
||||||
|
"import.close": "மூடு",
|
||||||
|
"import.close_title": "மூடு (Esc)",
|
||||||
|
"import.body": "ஏற்கனவே உள்ள பதிவை இறக்குமதி செய்யவும் — ஒரு உள்ளூர் ஒலி/வீடியோ கோப்பு, அல்லது ஒரு இணைப்பு (YouTube, ஒரு ஸ்ட்ரீமிங் பக்கம், அல்லது ஒரு நேரடி மீடியா URL). இது நேரடிப் பதிவு போலவே படியெடுக்கப்பட்டு பேச்சாளர் பிரிக்கப்படும்.",
|
||||||
|
"import.file_or_url": "கோப்பு அல்லது URL",
|
||||||
|
"import.source_placeholder": "ஒரு URL ஐ ஒட்டவும், அல்லது ஒரு கோப்பிற்கு உலாவவும்…",
|
||||||
|
"import.choose_file": "ஒரு உள்ளூர் கோப்பைத் தேர்வுசெய்",
|
||||||
|
"import.browse": "உலாவு…",
|
||||||
|
"import.title_label": "தலைப்பு",
|
||||||
|
"import.optional": "விருப்பமானது",
|
||||||
|
"import.title_placeholder": "இயல்பாகக் கோப்பின் பெயர்",
|
||||||
|
"import.filter_av": "ஒலி / வீடியோ",
|
||||||
|
"import.requires_1": "தேவை",
|
||||||
|
"import.requires_2": "நிறுவப்பட்டு உங்கள் PATH இல் (மேலும்",
|
||||||
|
"import.requires_3": "URL-களுக்கு). WhispAssist அவற்றை உள்ளடக்காது.",
|
||||||
|
"import.importing": "இறக்குமதி செய்கிறது… இதற்கு சிறிது நேரம் ஆகலாம்",
|
||||||
|
"import.import": "இறக்குமதி செய்",
|
||||||
|
"import.cancel": "ரத்து செய்",
|
||||||
|
|
||||||
|
"tagchip.filter": "“{tag}” குறிச்சொல் கொண்ட கூட்டங்களை வடிகட்டு",
|
||||||
|
"tagchip.remove": "குறிச்சொல் {tag} ஐ அகற்று",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "கணினி மற்றும் மைக்ரோஃபோன் உள்ளீட்டு நிலை",
|
||||||
|
"levelmeter.system": "கணினி உள்ளீட்டு நிலை",
|
||||||
|
|
||||||
|
"app.tagline": "உள்ளூர் · தனிப்பட்ட",
|
||||||
|
"app.note_template": "குறிப்பு டெம்ப்ளேட்",
|
||||||
|
"app.no_template": "டெம்ப்ளேட் இல்லை",
|
||||||
|
"app.add_meeting": "கூட்டத்தைச் சேர்",
|
||||||
|
"app.add_meeting_title": "ஒரு கோப்பு அல்லது URL இலிருந்து கூட்டத்தைச் சேர்",
|
||||||
|
"app.record": "பதிவு செய்",
|
||||||
|
"app.record_title": "பதிவைத் தொடங்கு (Ctrl+Shift+R)",
|
||||||
|
"app.stop": "நிறுத்து",
|
||||||
|
"app.stop_title": "பதிவை நிறுத்து (Ctrl+Shift+R)",
|
||||||
|
"app.cancel": "ரத்து செய்",
|
||||||
|
"app.cancel_title": "இந்தப் பதிவை நிராகரித்து நீக்கு",
|
||||||
|
"app.recording": "பதிவு செய்கிறது…",
|
||||||
|
"app.backend_title": "செயலில் உள்ள படியெடுப்பு பின்தளம்",
|
||||||
|
"app.retention_title": "இந்தக் கூட்டத்திற்கு ஒலியை .wav ஆகச் சேமி",
|
||||||
|
"app.saving": "சேமிக்கிறது",
|
||||||
|
"app.not_saved": "சேமிக்கப்படவில்லை",
|
||||||
|
"app.reconnecting": "ஒலி சாதனத்தை மீண்டும் இணைக்கிறது…",
|
||||||
|
"app.settings": "அமைப்புகள்",
|
||||||
|
"app.settings_title": "அமைப்புகள் (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "இந்தப் பதிவை நிராகரிக்கவா? அதன் ஒலியும் படியெடுப்பும் நீக்கப்படும்.",
|
||||||
|
"app.announce_started": "பதிவு தொடங்கியது",
|
||||||
|
"app.announce_paused": "பதிவு இடைநிறுத்தப்பட்டது",
|
||||||
|
"app.announce_stopped": "பதிவு நிறுத்தப்பட்டது",
|
||||||
|
"app.consent_dialog": "பதிவு ஒப்புதல்",
|
||||||
|
"app.vault_dialog": "மறையாக்கப் பெட்டகத்தைத் திற",
|
||||||
|
"app.vault_title": "மறையாக்கப் பெட்டகத்தைத் திற",
|
||||||
|
"app.vault_desc": "உங்கள் கூட்டங்கள் ஓய்வில் மறையாக்கப்பட்டுள்ளன. அவற்றைப் படிக்க உங்கள் கடவுச்சொல்லை உள்ளிடவும்.",
|
||||||
|
"app.vault_password": "பெட்டகக் கடவுச்சொல்",
|
||||||
|
"app.vault_incorrect": "தவறான கடவுச்சொல்",
|
||||||
|
"app.unlock": "திற",
|
||||||
|
"app.continue_locked": "பூட்டியபடி தொடர்",
|
||||||
|
"app.show_meetings": "கூட்டப் பட்டியலைக் காட்டு",
|
||||||
|
"app.hide_meetings": "கூட்டப் பட்டியலை மறை",
|
||||||
|
"app.show_summary": "சுருக்கப் பலகத்தைக் காட்டு",
|
||||||
|
"app.hide_summary": "சுருக்கப் பலகத்தை மறை",
|
||||||
|
"app.resize_meetings": "கூட்டப் பட்டியலின் அளவை மாற்று",
|
||||||
|
"app.resize_summary": "சுருக்கப் பலகத்தின் அளவை மாற்று",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "கூட்டங்களைத் தேடு…",
|
||||||
|
"meetings.search_aria": "கூட்டங்களைத் தேடு",
|
||||||
|
"meetings.filter_tag_aria": "குறிச்சொல் மூலம் வடிகட்டு",
|
||||||
|
"meetings.all_tags": "அனைத்து குறிச்சொற்கள்",
|
||||||
|
"meetings.from_date_aria": "தேதியிலிருந்து",
|
||||||
|
"meetings.to_date_aria": "தேதி வரை",
|
||||||
|
"meetings.bulk_format_aria": "மொத்த ஏற்றுமதி வடிவம்",
|
||||||
|
"meetings.bulk_export": "மொத்த ஏற்றுமதி",
|
||||||
|
"meetings.bulk_export_title": "மேலே உள்ள குறிச்சொல்/தேதி வடிகட்டிகளுடன் பொருந்தும் ஒவ்வொரு கூட்டத்தையும் ஏற்றுமதி செய்",
|
||||||
|
"meetings.exporting": "ஏற்றுமதி செய்கிறது…",
|
||||||
|
"meetings.exported_one": "{n} கூட்டம் ஏற்றுமதி செய்யப்பட்டது.",
|
||||||
|
"meetings.exported_many": "{n} கூட்டங்கள் ஏற்றுமதி செய்யப்பட்டன.",
|
||||||
|
"meetings.searching": "தேடுகிறது…",
|
||||||
|
"meetings.loading": "கூட்டங்கள் ஏற்றப்படுகின்றன…",
|
||||||
|
"meetings.no_matches": "பொருத்தங்கள் இல்லை.",
|
||||||
|
"meetings.empty": "இன்னும் கூட்டங்கள் இல்லை — தொடங்க மேலே பதிவு என்பதைக் கிளிக் செய்யவும்.",
|
||||||
|
"meetings.status.recording": "பதிவு செய்கிறது",
|
||||||
|
"meetings.status.transcribing": "படியெடுக்கிறது",
|
||||||
|
"meetings.status.recovering": "மீட்கிறது",
|
||||||
|
"meetings.status.error": "பிழை",
|
||||||
|
"meetings.resume": "படியெடுப்பைத் தொடர்",
|
||||||
|
"meetings.delete_aria": "கூட்டத்தை நீக்கு",
|
||||||
|
"meetings.delete_confirm": "இந்தக் கூட்டத்தை நீக்கவா? இது அதன் பதிவு, படியெடுப்பு மற்றும் குறிப்புகளை அகற்றும்.",
|
||||||
|
|
||||||
|
"transcript.heading": "படியெடுப்பு",
|
||||||
|
"transcript.title_aria": "கூட்டத் தலைப்பு",
|
||||||
|
"transcript.lang_title": "படியெடுப்பு மொழி",
|
||||||
|
"transcript.lang_auto": "கண்டறிகிறது…",
|
||||||
|
"transcript.show": "படியெடுப்பைக் காட்டு",
|
||||||
|
"transcript.hide": "படியெடுப்பை மறை",
|
||||||
|
"transcript.empty": "இந்தக் கூட்டத்திற்கு படியெடுப்பு இல்லை.",
|
||||||
|
"transcript.will_appear": "நீங்கள் பதிவு செய்யும்போது படியெடுப்பு இங்கே தோன்றும்.",
|
||||||
|
"transcript.play_from_here": "இங்கிருந்து இயக்கு",
|
||||||
|
"transcript.resize": "படியெடுப்பு மற்றும் குறிப்புகளின் அளவை மாற்று",
|
||||||
|
"transcript.reprocess_placeholder": "இதனுடன் மீண்டும் படியெடு…",
|
||||||
|
"transcript.reprocess_lang_aria": "மறுசெயலாக்க மொழி",
|
||||||
|
"transcript.reprocess_keep_lang": "தற்போதைய மொழியை வைத்திரு",
|
||||||
|
"transcript.reprocess_go": "செல்",
|
||||||
|
"transcript.retranscribing": "மீண்டும் படியெடுக்கிறது…",
|
||||||
|
"transcript.has_note": "ஒரு குறிப்பு உள்ளது",
|
||||||
|
"transcript.note_placeholder": "இந்தத் தருணத்திற்கு ஒரு குறிப்பைச் சேர்…",
|
||||||
|
"transcript.note_aria": "இந்தப் படியெடுப்பு வரிக்கான குறிப்பு",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Word ஆவணம்",
|
||||||
|
"transcript.filter_obsidian": "Obsidian குறிப்பு",
|
||||||
|
|
||||||
|
"notes.heading": "குறிப்புகள்",
|
||||||
|
"notes.show": "குறிப்புகளைக் காட்டு",
|
||||||
|
"notes.hide": "குறிப்புகளை மறை",
|
||||||
|
"notes.placeholder": "குறிப்புகள்…",
|
||||||
|
"notes.live_placeholder": "பேசும்போது குறிப்புகளைத் தட்டச்சு செய்யவும்…",
|
||||||
|
"notes.toolbar_aria": "குறிப்பு வடிவமைப்பு",
|
||||||
|
"notes.bold": "தடிமன்",
|
||||||
|
"notes.italic": "சாய்வு",
|
||||||
|
"notes.h1": "தலைப்பு 1",
|
||||||
|
"notes.h2": "தலைப்பு 2",
|
||||||
|
"notes.bullet": "புள்ளிப் பட்டியல்",
|
||||||
|
"notes.checkbox_title": "தேர்வுப்பெட்டி",
|
||||||
|
"notes.checkbox_aria": "தேர்வுப்பெட்டி பட்டியல் உருப்படி",
|
||||||
|
"notes.edit_raw": "மூல markdown ஐத் திருத்து",
|
||||||
|
"notes.render": "markdown ஐ வழங்கு",
|
||||||
|
"notes.editor": "திருத்தி",
|
||||||
|
"notes.preview": "முன்னோட்டம்",
|
||||||
|
"notes.export_md_title": "குறிப்புகளை .md ஆக ஏற்றுமதி செய்",
|
||||||
|
"notes.export_pdf_title": "குறிப்புகளை .pdf ஆக ஏற்றுமதி செய்",
|
||||||
|
"notes.export_docx_title": "குறிப்புகளை .docx ஆக ஏற்றுமதி செய்",
|
||||||
|
"notes.export_bundle_title": "ஒலி + படியெடுப்பு + குறிப்புகளை ஒரு கோப்புறைக்கு ஏற்றுமதி செய்",
|
||||||
|
"notes.export_obsidian_title": "ஒரு Obsidian குறிப்பை ஏற்றுமதி செய் (குறிப்புகள் + சுருக்கம் + படியெடுப்பு, ஒலி இல்லை)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "பதிவு",
|
||||||
|
"summary.loading_recording": "பதிவு ஏற்றப்படுகிறது…",
|
||||||
|
"summary.sync_heading": "ஒத்திசைவு",
|
||||||
|
"summary.uploading": "பதிவேற்றுகிறது…",
|
||||||
|
"summary.upload_now": "இப்போது பதிவேற்று",
|
||||||
|
"summary.no_uploads": "இந்தக் கூட்டத்திற்கு இன்னும் பதிவேற்றங்கள் இல்லை.",
|
||||||
|
"summary.retry": "மீண்டும் முயற்சி",
|
||||||
|
"summary.tags_heading": "குறிச்சொற்கள்",
|
||||||
|
"summary.tags_select": "ஒரு கூட்டத்தைக் குறிச்சொல்லிட அதைத் தேர்ந்தெடுக்கவும்.",
|
||||||
|
"summary.tag_add_placeholder": "குறிச்சொல்லைச் சேர்…",
|
||||||
|
"summary.tag_first_placeholder": "திட்டம், வாடிக்கையாளர், தலைப்பு…",
|
||||||
|
"summary.generating": "உருவாக்குகிறது…",
|
||||||
|
"summary.generate_tags": "குறிச்சொற்களை உருவாக்கு",
|
||||||
|
"summary.saving": "சேமிக்கிறது…",
|
||||||
|
"summary.save_tags": "குறிச்சொற்களைச் சேமி",
|
||||||
|
"summary.summary_heading": "சுருக்கம்",
|
||||||
|
"summary.ai_provider": "AI வழங்குநர்",
|
||||||
|
"summary.provider_off": "ஆஃப்",
|
||||||
|
"summary.provider_custom": "தனிப்பயன்",
|
||||||
|
"summary.local": "உள்ளூர்",
|
||||||
|
"summary.leaves_device": "இந்தச் சாதனத்தை விட்டு வெளியேறுகிறது",
|
||||||
|
"summary.this_hosted_provider": "இந்த ஹோஸ்ட் செய்யப்பட்ட வழங்குநர்",
|
||||||
|
"summary.select_generate": "ஒரு சுருக்கத்தை உருவாக்க ஒரு கூட்டத்தைத் தேர்ந்தெடுக்கவும்.",
|
||||||
|
"summary.decisions_heading": "முடிவுகள்",
|
||||||
|
"summary.regenerate": "மீண்டும் உருவாக்கு",
|
||||||
|
"summary.generated_hint": "கூட்டத்திற்குப் பிறகு உள்ளூரில் உருவாக்கப்பட்டது (ஒரு உள்ளூர் LLM வழங்குநர் தேவை).",
|
||||||
|
"summary.no_provider": "AI வழங்குநர் எதுவும் கட்டமைக்கப்படவில்லை — அமைப்புகளில் ஒன்றை இயக்கவும்.",
|
||||||
|
"summary.generate_summary": "சுருக்கத்தை உருவாக்கு",
|
||||||
|
"summary.briefs_heading": "அம்ச சுருக்கங்கள்",
|
||||||
|
"summary.briefs_select": "அம்ச சுருக்கங்களை உருவாக்க அல்லது பார்க்க ஒரு கூட்டத்தைத் தேர்ந்தெடுக்கவும்.",
|
||||||
|
"summary.briefs_desc": "இந்தக் கூட்டத்தை ஒரு முகவருக்குத் தயார் விவரக்குறிப்பாக வடிகட்டுகிறது (ஒரு உள்ளூர் LLM வழங்குநர் தேவை) — அதை ஒரு குறியீட்டு முகவருக்குக் கொடுக்கவும், அல்லது MCP சேவையகம் இயக்கப்பட்டவுடன் அதன் மூலம் வழங்கவும்.",
|
||||||
|
"summary.brief_repo_placeholder": "இலக்கு களஞ்சியம் (விருப்பமானது), எ.கா. acme/reporting-web",
|
||||||
|
"summary.distilling": "வடிகட்டுகிறது…",
|
||||||
|
"summary.create_brief": "அம்ச சுருக்கத்தை உருவாக்கு",
|
||||||
|
"summary.brief_mcp_title": "MCP சேவையகம் இயக்கப்பட்டிருக்கும்போது கிடைக்கும்",
|
||||||
|
"summary.copied": "நகலெடுக்கப்பட்டது",
|
||||||
|
"summary.copy_md": "Markdown ஆக நகலெடு",
|
||||||
|
"summary.copy_json": "JSON ஆக நகலெடு",
|
||||||
|
"summary.brief_problem": "சிக்கல்",
|
||||||
|
"summary.brief_outcome": "விரும்பிய முடிவு",
|
||||||
|
"summary.brief_criteria": "ஏற்பு அளவுகோல்கள்",
|
||||||
|
"summary.brief_none": "எதுவும் பிடிக்கப்படவில்லை.",
|
||||||
|
"summary.brief_context": "சூழல்",
|
||||||
|
"summary.action_items_heading": "செயல்-உருப்படிகள்",
|
||||||
|
"summary.ai_select": "அதன் செயல்-உருப்படிகளைப் பார்க்க ஒரு கூட்டத்தைத் தேர்ந்தெடுக்கவும்.",
|
||||||
|
"summary.ai_empty": "இன்னும் எதுவும் இல்லை — கீழே ஒன்றைச் சேர்க்கவும், அல்லது அவற்றைத் தானாகப் பிரித்தெடுக்க ஒரு சுருக்கத்தை உருவாக்கவும்.",
|
||||||
|
"summary.confirmed": "உறுதிப்படுத்தப்பட்டது",
|
||||||
|
"summary.ai_text_placeholder": "செயல்-உருப்படி…",
|
||||||
|
"summary.ai_text_aria": "செயல்-உருப்படி உரை",
|
||||||
|
"summary.owner": "உரிமையாளர்",
|
||||||
|
"summary.due_date": "காலக்கெடு",
|
||||||
|
"summary.reminder_title": "இந்தக் காலக்கெடுவுக்கு ஒரு உள்ளூர் நினைவூட்டலைத் திட்டமிடு",
|
||||||
|
"summary.ai_delete_title": "இந்தச் செயல்-உருப்படியை நீக்கு",
|
||||||
|
"summary.ai_delete_aria": "செயல்-உருப்படியை நீக்கு",
|
||||||
|
"summary.add_action_item": "செயல்-உருப்படியைச் சேர்",
|
||||||
|
"summary.save_action_items": "செயல்-உருப்படிகளைச் சேமி",
|
||||||
|
"summary.calendar_heading": "நாட்காட்டி நிகழ்வு",
|
||||||
|
"summary.cal_select": "ஒரு கூட்டத்தை நாட்காட்டி நிகழ்வுடன் இணைக்க அதைத் தேர்ந்தெடுக்கவும்.",
|
||||||
|
"summary.event_search_placeholder": "நிகழ்வுத் தலைப்பைத் தேடு…",
|
||||||
|
"summary.linked_event": "இணைக்கப்பட்ட நிகழ்வு",
|
||||||
|
"summary.change_event": "நிகழ்வை மாற்று…",
|
||||||
|
"summary.link_event": "ஒரு நிகழ்வை இணை…",
|
||||||
|
"summary.untitled_event": "(தலைப்பில்லாதது)",
|
||||||
|
"summary.no_events_1": "இன்னும் நிகழ்வுகள் எதுவும் இறக்குமதி செய்யப்படவில்லை — ஒன்றை",
|
||||||
|
"summary.no_events_2": "அமைப்புகள் → நாட்காட்டியிலிருந்து இறக்குமதி செய்யவும்.",
|
||||||
|
"summary.no_events_match": "இந்தத் தேடல்/தேதியுடன் எந்த நிகழ்வும் பொருந்தவில்லை — ஒன்றை அழிக்க முயற்சிக்கவும்.",
|
||||||
|
"summary.organizer_label": "ஏற்பாட்டாளர்: {name}",
|
||||||
|
"summary.participants_heading": "பங்கேற்பாளர்கள்",
|
||||||
|
"summary.participants_hint": "இணைக்கப்பட்ட நாட்காட்டி நிகழ்விலிருந்து நிரப்பப்படுகிறது.",
|
||||||
|
"summary.speakers_heading": "பேச்சாளர்கள்",
|
||||||
|
"summary.speakers_select": "அதன் பேச்சாளர்களுக்குப் பெயரிட ஒரு கூட்டத்தைத் தேர்ந்தெடுக்கவும்.",
|
||||||
|
"summary.no_speakers": "இன்னும் பேச்சாளர்கள் எவரும் கண்டறியப்படவில்லை.",
|
||||||
|
"summary.speaker_name_placeholder": "பேச்சாளர் பெயர்",
|
||||||
|
"summary.save": "சேமி",
|
||||||
|
"summary.cancel": "ரத்து செய்",
|
||||||
|
"summary.name_speaker": "இந்தப் பேச்சாளருக்குப் பெயரிடு…",
|
||||||
|
"summary.add_new_name": "+ புதிய பெயரைச் சேர்…",
|
||||||
|
|
||||||
|
"settings.recording.title": "பதிவு",
|
||||||
|
"settings.recording.record_default": "இயல்பாகக் கூட்டங்களைப் பதிவு செய்",
|
||||||
|
"settings.recording.save_audio_as": "ஒலியை இவ்வாறு சேமி",
|
||||||
|
"settings.recording.default_hint": "இயல்பாக ஆஃப். ஆஃப் ஆக இருக்கும்போது, ஒலி படியெடுப்பை உருவாக்கவே மட்டும் பயன்படுத்தப்பட்டு, கூட்டம் இறுதிசெய்யப்படும்போது நீக்கப்படும். ஒவ்வொரு கூட்டத்திற்கும் பதிவை நீங்கள் மாற்றலாம்.",
|
||||||
|
"settings.recording.consent_label": "ஒப்புதல்: {status}.",
|
||||||
|
"settings.recording.consent_ack": "ஒப்புக்கொள்ளப்பட்டது",
|
||||||
|
"settings.recording.consent_not": "இன்னும் ஒப்புக்கொள்ளப்படவில்லை",
|
||||||
|
"settings.recording.auto_label": "ஒரு நாட்காட்டி நிகழ்வு தொடங்கும்போது பதிவைத் தானாகத் தொடங்கு",
|
||||||
|
"settings.recording.auto_hint": "WhispAssist திறந்திருக்கும்போது மட்டும். இறக்குமதி செய்யப்பட்ட நாட்காட்டி நிகழ்வின் தொடக்க நேரம் வரும்போது, ஒரு பதிவு தானாகத் தொடங்கும் (மேலே உள்ள உங்கள் இயல்புநிலை தக்கவைப்பு அமைப்பைப் பயன்படுத்தி). பின்னணியில் எதுவும் இயங்காது — பயன்பாடு இயங்கும்போது மட்டுமே டைமர் தயார்நிலையில் இருக்கும். அமைப்புகள் → நாட்காட்டியின் கீழ் நிகழ்வுகளை இறக்குமதி செய்யவும்."
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "ریکارڈنگ",
|
||||||
|
"nav.hardware": "ہارڈ ویئر",
|
||||||
|
"nav.storage": "اسٹوریج",
|
||||||
|
"nav.calendar": "کیلنڈر",
|
||||||
|
"nav.sync": "سِنک",
|
||||||
|
"nav.ai": "اے آئی",
|
||||||
|
"nav.mcp": "MCP سرور",
|
||||||
|
"nav.privacy": "رازداری",
|
||||||
|
"nav.about": "بارے میں",
|
||||||
|
"nav.language": "زبان",
|
||||||
|
|
||||||
|
"settings.language.title": "زبان",
|
||||||
|
"settings.language.display": "ڈسپلے زبان",
|
||||||
|
"settings.language.display_hint": "ایپ انٹرفیس کی زبان۔ کوئی زبان شامل کرنا صرف ایک ترجمہ فائل ڈالنے جتنا آسان ہے۔",
|
||||||
|
|
||||||
|
"settings.transcription.title": "نقل نویسی کی زبان",
|
||||||
|
"settings.transcription.label": "زبان",
|
||||||
|
"settings.transcription.auto": "خودکار شناخت",
|
||||||
|
"settings.transcription.applies_hint": "اگلی ریکارڈنگ پر لاگو ہوتا ہے۔ حقیقت میں استعمال ہونے والی زبان بعد میں ہر میٹنگ پر دکھائی جاتی ہے۔",
|
||||||
|
"settings.transcription.english_only": "صرف انگریزی",
|
||||||
|
"settings.transcription.model_english_only": "«{model}» صرف انگریزی میں ہے۔",
|
||||||
|
"settings.transcription.no_model": "کوئی ماڈل منتخب نہیں۔",
|
||||||
|
"settings.transcription.switch_multilingual": "زبان منتخب کرنے کے لیے اوپر کسی کثیر لسانی ماڈل پر سوئچ کریں۔",
|
||||||
|
|
||||||
|
"settings.hardware.title": "ہارڈ ویئر",
|
||||||
|
"settings.hardware.active_backend": "فعال بیک اینڈ",
|
||||||
|
"settings.hardware.model_meta": "· ماڈل {size}",
|
||||||
|
"settings.hardware.preferred_backend": "ترجیحی بیک اینڈ",
|
||||||
|
"settings.hardware.auto_backend": "خودکار (بہترین دستیاب)",
|
||||||
|
"settings.hardware.not_available": "دستیاب نہیں",
|
||||||
|
"settings.hardware.fallback_hint": "اگر منتخب بیک اینڈ لوڈ نہ ہو تو خودکار طور پر واپس چلا جاتا ہے (NPU → NVIDIA → AMD → Intel → CPU)۔",
|
||||||
|
"settings.hardware.audio_devices": "آڈیو آلات",
|
||||||
|
"settings.hardware.recording_device": "ریکارڈنگ آلہ",
|
||||||
|
"settings.hardware.default_system_audio": "ڈیفالٹ سسٹم آڈیو",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist وہی ریکارڈ کرتا ہے جو یہ آلہ چلاتا ہے (لوپ بیک) — کال کا دوسرا رخ۔ اگر آپ نہیں چاہتے کہ یہ Windows کے سسٹم ڈیفالٹ کی پیروی کرے تو کوئی مخصوص آؤٹ پٹ منتخب کریں۔",
|
||||||
|
"settings.hardware.microphone": "مائیکروفون",
|
||||||
|
"settings.hardware.mic_off": "بند — میرا مائیکروفون کیپچر نہ کریں",
|
||||||
|
"settings.hardware.default_mic": "ڈیفالٹ مائیکروفون",
|
||||||
|
"settings.hardware.mic_hint": "لائیو نقل میں آپ کی اپنی آواز شامل کرتا ہے تاکہ میٹنگ کے دونوں رخ کیپچر ہوں۔ آپ کے آلے پر ہی رہتا ہے — کچھ بھی اپ لوڈ نہیں ہوتا۔ صرف اوپر والا سسٹم آڈیو نقل کرنے کے لیے «بند» منتخب کریں۔",
|
||||||
|
"settings.hardware.npu_detected": "NPU ایکسلریٹر کا پتہ چلا",
|
||||||
|
"settings.hardware.ready": "تیار",
|
||||||
|
"settings.hardware.downloading": "ڈاؤن لوڈ ہو رہا ہے…",
|
||||||
|
"settings.hardware.package_needed": "پیکیج درکار",
|
||||||
|
"settings.hardware.npu_hint": "آپ کے NPU کو ایک بار کا ایکسلریشن پیکیج (Whisper ONNX ماڈل{extra}) درکار ہے۔ یہ پس منظر میں ڈاؤن لوڈ ہوتا ہے؛ آپ اسے ابھی بھی شروع کر سکتے ہیں۔",
|
||||||
|
"settings.hardware.plus_openvino": " + OpenVINO رن ٹائم",
|
||||||
|
"settings.hardware.download_npu": "NPU پیکیج ڈاؤن لوڈ کریں",
|
||||||
|
"settings.hardware.dml_title": "GPU ایکسلریشن (DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "آپ کے GPU کو Vulkan کے بغیر DirectML کے ذریعے تیز کیا جا سکتا ہے — ایک بار کا پیکیج (Whisper ONNX ماڈل{extra})۔",
|
||||||
|
"settings.hardware.plus_directml": " + DirectML رن ٹائم",
|
||||||
|
"settings.hardware.download_dml": "DirectML پیکیج ڈاؤن لوڈ کریں",
|
||||||
|
"settings.hardware.detection_unavailable": "ہارڈ ویئر شناخت دستیاب نہیں۔",
|
||||||
|
"settings.hardware.low_overhead": "کم اوور ہیڈ پری سیٹ",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + سب سے چھوٹا ماڈل، بیٹری/پس منظر استعمال کے لیے",
|
||||||
|
"settings.hardware.models": "ماڈلز",
|
||||||
|
"settings.hardware.downloading_model": "{name} ڈاؤن لوڈ ہو رہا ہے",
|
||||||
|
"settings.hardware.active_badge": "فعال",
|
||||||
|
"settings.hardware.installed": "انسٹال شدہ",
|
||||||
|
"settings.hardware.use": "استعمال کریں",
|
||||||
|
"settings.hardware.remove": "ہٹائیں",
|
||||||
|
"settings.hardware.cant_remove_active": "فعال ماڈل کو نہیں ہٹایا جا سکتا",
|
||||||
|
"settings.hardware.download": "ڈاؤن لوڈ کریں",
|
||||||
|
"settings.hardware.diarization": "مقرر علیحدگی",
|
||||||
|
"settings.hardware.diarization_hint": "دونوں ماڈل انسٹال کریں تاکہ مکمل ریکارڈنگز ایک مسلسل نقل کے بجائے مقرر کے حساب سے (مقرر 1، مقرر 2، …) الگ ہوں۔ مکمل طور پر آف لائن چلتا ہے، رکنے کے بعد فی میٹنگ ایک بار۔ ان کے بغیر، ہر لائن ایک ہی مقرر کو منسوب کی جاتی ہے۔",
|
||||||
|
|
||||||
|
"settings.storage.title": "اسٹوریج",
|
||||||
|
"settings.storage.location_label": "مقام",
|
||||||
|
"settings.storage.location_hint": "اسٹوریج مقام تبدیل کرنا ابھی معاون نہیں — فی الحال میٹنگز، ماڈلز اور ڈیٹابیس یہیں موجود ہیں۔",
|
||||||
|
"settings.storage.retention_title": "برقراری",
|
||||||
|
"settings.storage.retention_hint": "ایک حد سے زیادہ پرانی میٹنگز کو خودکار طور پر حذف کرتا ہے۔ شروع میں ایک بار جانچا جاتا ہے؛ خالی کا مطلب کوئی حد نہیں۔ فی الحال ریکارڈ ہونے والی میٹنگ کو کبھی نہیں چھوتا۔",
|
||||||
|
"settings.storage.max_age": "زیادہ سے زیادہ عمر (دن)",
|
||||||
|
"settings.storage.max_size": "زیادہ سے زیادہ سائز (GB)",
|
||||||
|
"settings.storage.no_limit": "کوئی حد نہیں",
|
||||||
|
"settings.storage.export_import_title": "برآمد و درآمد",
|
||||||
|
"settings.storage.export_hint_1": "ریکارڈنگز کو کمپیوٹرز کے درمیان پورٹیبل بنڈل فولڈرز کے طور پر منتقل کریں (آڈیو، نقل، نوٹس، خلاصہ اور ایک",
|
||||||
|
"settings.storage.export_hint_2": "مینی فیسٹ)۔ کسی بھی فولڈر میں برآمد کریں — ایک سِنک شدہ ڈرائیو، ایک USB اسٹک، یا کسی سِنک ہدف کے مقامی ماؤنٹ میں — پھر دوسری مشین پر اسے درآمد کریں۔ درآمد شدہ میٹنگز کو نیا id ملتا ہے، اس لیے دوبارہ درآمد کبھی کسی چیز کو اوور رائٹ نہیں کرتا۔",
|
||||||
|
"settings.storage.export_dialog_title": "تمام میٹنگز یہاں برآمد کریں…",
|
||||||
|
"settings.storage.import_dialog_title": "ایک بنڈل (یا بنڈلز کا فولڈر) درآمد کریں…",
|
||||||
|
"settings.storage.exporting": "برآمد ہو رہا ہے…",
|
||||||
|
"settings.storage.export_all": "تمام میٹنگز برآمد کریں…",
|
||||||
|
"settings.storage.importing": "درآمد ہو رہا ہے…",
|
||||||
|
"settings.storage.import_all": "میٹنگز درآمد کریں…",
|
||||||
|
"settings.storage.exported_one": "{n} میٹنگ برآمد کی گئی۔",
|
||||||
|
"settings.storage.exported_many": "{n} میٹنگز برآمد کی گئیں۔",
|
||||||
|
"settings.storage.imported_one": "{n} میٹنگ درآمد کی گئی۔",
|
||||||
|
"settings.storage.imported_many": "{n} میٹنگز درآمد کی گئیں۔",
|
||||||
|
|
||||||
|
"settings.calendar.title": "کیلنڈر اور Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "ایک مقامی Outlook بیک اپ سے ایونٹس اور شرکاء درآمد کریں",
|
||||||
|
"settings.calendar.intro_2": "— صرف پڑھنے کے لیے، فائل میں کچھ بھی واپس نہیں لکھا جاتا۔ اس آلے سے کچھ بھی باہر نہیں جاتا۔",
|
||||||
|
"settings.calendar.pst_file": ".pst فائل",
|
||||||
|
"settings.calendar.no_file": "کوئی فائل منتخب نہیں",
|
||||||
|
"settings.calendar.browse": "براؤز کریں…",
|
||||||
|
"settings.calendar.password": "پاس ورڈ",
|
||||||
|
"settings.calendar.password_note": "شاذ و نادر ہی درکار",
|
||||||
|
"settings.calendar.optional": "اختیاری",
|
||||||
|
"settings.calendar.import_range": "درآمد رینج",
|
||||||
|
"settings.calendar.range_30": "گزشتہ 30 دن",
|
||||||
|
"settings.calendar.range_90": "گزشتہ 90 دن",
|
||||||
|
"settings.calendar.range_180": "گزشتہ 6 ماہ",
|
||||||
|
"settings.calendar.range_365": "گزشتہ 1 سال",
|
||||||
|
"settings.calendar.range_all": "تمام وقت",
|
||||||
|
"settings.calendar.range_hint": "ایک طویل عرصے سے موجود میل باکس میں برسوں کی بار بار آنے والی/تعطیلات کی اندراجات ہو سکتی ہیں — رینج کو تنگ کرنا درآمد شدہ کیلنڈر کو صرف اسی چیز تک محدود رکھتا ہے جو حقیقتاً متعلقہ ہے۔ یہ اس درآمد بٹن اور شروع پر خودکار دوبارہ درآمد دونوں پر لاگو ہوتا ہے۔",
|
||||||
|
"settings.calendar.importing": "درآمد ہو رہا ہے…",
|
||||||
|
"settings.calendar.import": "درآمد کریں",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} ایونٹس",
|
||||||
|
"settings.calendar.auto_reimport": "شروع پر اس فائل کو خودکار طور پر دوبارہ درآمد کریں",
|
||||||
|
"settings.calendar.auto_reimport_hint": "ٹائمر پر نہیں بلکہ شروع میں ایک بار چلتا ہے — دوبارہ درآمد کو محفوظ طریقے سے دہرایا جا سکتا ہے (موجودہ ایونٹس ملا کر اپ ڈیٹ کیے جاتے ہیں، نقل نہیں کیے جاتے)۔",
|
||||||
|
"settings.calendar.import_failed": "درآمد ناکام: {error} — فائل خود اچھوتی رہتی ہے؛ راستہ جانچیں اور دوبارہ کوشش کریں۔",
|
||||||
|
"settings.calendar.cleanup_title": "صاف کریں",
|
||||||
|
"settings.calendar.cleanup_hint": "کسی ریکارڈ شدہ میٹنگ سے پہلے سے منسلک ایونٹس ہمیشہ رکھے جاتے ہیں، چاہے نیچے کوئی بھی آپشن منتخب ہو۔",
|
||||||
|
"settings.calendar.cleanup_30": "30 دن سے پرانے",
|
||||||
|
"settings.calendar.cleanup_90": "90 دن سے پرانے",
|
||||||
|
"settings.calendar.cleanup_180": "6 ماہ سے پرانے",
|
||||||
|
"settings.calendar.cleanup_365": "1 سال سے پرانے",
|
||||||
|
"settings.calendar.cleanup_all": "سب حذف کریں",
|
||||||
|
"settings.calendar.cleaning": "صاف ہو رہا ہے…",
|
||||||
|
"settings.calendar.cleanup_btn": "کیلنڈر صاف کریں",
|
||||||
|
"settings.calendar.cleanup_result": "{deleted}{extra} حذف کیے گئے۔",
|
||||||
|
"settings.calendar.cleanup_kept": "، {n} رکھے گئے (ایک میٹنگ سے منسلک)",
|
||||||
|
"settings.calendar.cleanup_failed": "صفائی ناکام: {error}",
|
||||||
|
"settings.calendar.imported_events": "درآمد شدہ ایونٹس",
|
||||||
|
"settings.calendar.no_events": "ابھی تک کوئی ایونٹ درآمد نہیں ہوا۔",
|
||||||
|
"settings.calendar.search_title": "عنوان تلاش کریں",
|
||||||
|
"settings.calendar.search_placeholder": "میٹنگ کا نام…",
|
||||||
|
"settings.calendar.date_label": "تاریخ",
|
||||||
|
"settings.calendar.events_count": "{total} میں سے {shown} ایونٹس",
|
||||||
|
"settings.calendar.untitled": "(بلا عنوان)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "ترتیبات",
|
||||||
|
"settings.close": "بند کریں",
|
||||||
|
"settings.close_title": "بند کریں (Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "پیش نظارہ موڈ — سِنک بیک اینڈ ابھی نافذ نہیں ہوا (مرحلہ 9)۔ تبدیلیاں صرف انٹرفیس میں رکھی جاتی ہیں۔",
|
||||||
|
"settings.sync.title": "سِنک اور اپ لوڈ",
|
||||||
|
"settings.sync.enable_label": "ترتیب شدہ اہداف پر میٹنگ آرٹیفیکٹس اپ لوڈ کرنا فعال کریں",
|
||||||
|
"settings.sync.enable_hint": "بطور ڈیفالٹ بند۔ جب تک یہ آن نہ ہو اور کوئی ہدف فعال نہ ہو، کچھ اپ لوڈ نہیں ہوتا۔ خود میزبان اہداف ڈیٹا کو آپ کے اپنے سرور پر رکھتے ہیں؛ فریق ثالث کلاؤڈز واضح طور پر لیبل شدہ ہیں۔",
|
||||||
|
"settings.sync.targets_title": "اہداف",
|
||||||
|
"settings.sync.no_targets": "ابھی کوئی اہداف نہیں۔ نیچے ایک شامل کریں۔",
|
||||||
|
"settings.sync.third_party": "فریق ثالث",
|
||||||
|
"settings.sync.your_server": "آپ کا سرور",
|
||||||
|
"settings.sync.edit": "ترمیم کریں",
|
||||||
|
"settings.sync.remove": "ہٹائیں",
|
||||||
|
"settings.sync.edit_target": "ہدف میں ترمیم کریں",
|
||||||
|
"settings.sync.add_target": "ایک ہدف شامل کریں",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV، Nextcloud، ownCloud، Cloudreve، Seafile اور Synology کا احاطہ کرتا ہے۔ (Seafile: سرور کی طرف SeafDAV فعال کریں۔)",
|
||||||
|
"settings.sync.name": "نام",
|
||||||
|
"settings.sync.name_placeholder": "گھریلو Nextcloud",
|
||||||
|
"settings.sync.provider": "فراہم کنندہ",
|
||||||
|
"settings.sync.server_url": "سرور URL",
|
||||||
|
"settings.sync.server_url_note_1": "صرف آپ کا سرور URL — WhispAssist خودکار طور پر",
|
||||||
|
"settings.sync.server_url_note_2": "شامل کرتا ہے۔",
|
||||||
|
"settings.sync.remote_folder": "دور دراز فولڈر",
|
||||||
|
"settings.sync.username": "صارف نام",
|
||||||
|
"settings.sync.app_password": "ایپ پاس ورڈ",
|
||||||
|
"settings.sync.pw_keep": "موجودہ پاس ورڈ رکھنے کے لیے خالی چھوڑ دیں",
|
||||||
|
"settings.sync.pw_store": "OS کریڈنشل اسٹور میں محفوظ",
|
||||||
|
"settings.sync.upload_legend": "اپ لوڈ",
|
||||||
|
"settings.sync.artifact_transcript": "نقل",
|
||||||
|
"settings.sync.artifact_notes": "نوٹس",
|
||||||
|
"settings.sync.artifact_summary": "خلاصہ",
|
||||||
|
"settings.sync.artifact_recording": "ریکارڈنگ (.wav، اگر رکھی گئی)",
|
||||||
|
"settings.sync.allow_plaintext": "کسی LAN پتے کے لیے سادہ متن http کی اجازت دیں (تجویز کردہ نہیں)",
|
||||||
|
"settings.sync.encrypt_before": "اپ لوڈ سے پہلے خفیہ کریں (منزل صرف خفیہ متن محفوظ کرتی ہے)",
|
||||||
|
"settings.sync.requires_vault": "— ایک ان لاک شدہ والٹ درکار ہے",
|
||||||
|
"settings.sync.test_connection": "کنکشن جانچیں",
|
||||||
|
"settings.sync.save_changes": "تبدیلیاں محفوظ کریں",
|
||||||
|
"settings.sync.add_target_btn": "ہدف شامل کریں",
|
||||||
|
"settings.sync.cancel": "منسوخ کریں",
|
||||||
|
"settings.sync.third_party_banner": "{kind} ایک فریق ثالث کلاؤڈ ہے — اپ لوڈ کرنا آپ کا ڈیٹا آپ کے آلے سے باہر {kind} کو بھیجتا ہے۔",
|
||||||
|
"settings.sync.link_account": "{kind} اکاؤنٹ منسلک کریں…",
|
||||||
|
"settings.sync.oauth_hint": "ایک OAuth سائن ان کھولتا ہے (لوپ بیک ری ڈائریکٹ)؛ ٹوکن آپ کے OS کریڈنشل اسٹور میں محفوظ ہوتا ہے۔",
|
||||||
|
"settings.sync.footnote": "کریڈنشلز کبھی ترتیبات یا ڈیٹابیس میں نہیں لکھے جاتے — صرف OS کریڈنشل اسٹور میں۔ غیر LAN اہداف کے لیے TLS درکار ہے۔",
|
||||||
|
|
||||||
|
"settings.ai.title": "AI خلاصہ فراہم کنندہ",
|
||||||
|
"settings.ai.intro_1": "خلاصے بطور ڈیفالٹ ایک مقامی LLM پر چلتے ہیں۔ اسے اس PC یا اپنے LAN کی کسی اور مشین پر Ollama کی طرف اشارہ کریں (مثلاً",
|
||||||
|
"settings.ai.intro_2": "— دونوں مقامی شمار ہوتے ہیں، اس لیے آپ کے نیٹ ورک سے کچھ باہر نہیں جاتا۔ میزبان فراہم کنندگان (Anthropic) اختیاری ہیں، بطور ڈیفالٹ بند ہیں، اور جیسے ہی آپ ایک آن کرتے ہیں نقل کو کسی فریق ثالث کو بھیجتے ہیں۔",
|
||||||
|
"settings.ai.provider": "فراہم کنندہ",
|
||||||
|
"settings.ai.off": "بند",
|
||||||
|
"settings.ai.provider_ollama": "Ollama (مقامی / LAN)",
|
||||||
|
"settings.ai.provider_custom": "حسب ضرورت (OpenAI-مطابق)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic (Claude) — میزبان، یہ آلہ چھوڑ دیتا ہے",
|
||||||
|
"settings.ai.endpoint": "اینڈ پوائنٹ",
|
||||||
|
"settings.ai.model": "ماڈل",
|
||||||
|
"settings.ai.api_key": "API کلید",
|
||||||
|
"settings.ai.api_key_set_placeholder": "•••••••••••••••• (پہلے سے سیٹ — رکھنے کے لیے خالی چھوڑ دیں)",
|
||||||
|
"settings.ai.show_api_key": "API کلید دکھائیں",
|
||||||
|
"settings.ai.hide_api_key": "API کلید چھپائیں",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic ایک میزبان فریق ثالث سروس ہے — جب آپ خلاصہ تیار کرتے ہیں تو اس میٹنگ کی نقل آپ کا آلہ چھوڑ دیتی ہے۔",
|
||||||
|
"settings.ai.endpoint_banner": "یہ اینڈ پوائنٹ آپ کی مشین یا LAN پر نہیں ہے — آپ کی نقل آپ کا نیٹ ورک چھوڑ دے گی۔",
|
||||||
|
"settings.ai.saving": "محفوظ ہو رہا ہے…",
|
||||||
|
"settings.ai.save": "محفوظ کریں",
|
||||||
|
"settings.ai.test_connection": "کنکشن جانچیں",
|
||||||
|
"settings.ai.this_hosted_provider": "یہ میزبان فراہم کنندہ",
|
||||||
|
"settings.ai.reachable": "قابل رسائی",
|
||||||
|
"settings.ai.unreachable": "ناقابل رسائی",
|
||||||
|
"settings.ai.model_count_one": "· {n} ماڈل",
|
||||||
|
"settings.ai.model_count_many": "· {n} ماڈل",
|
||||||
|
"settings.ai.on_lan": "· آپ کی مشین / LAN پر",
|
||||||
|
"settings.ai.leaves_network": "· آپ کا نیٹ ورک چھوڑ دیتا ہے",
|
||||||
|
"settings.ai.advanced_title": "اعلیٰ Ollama ترتیب",
|
||||||
|
"settings.ai.changed": "{n} تبدیل شدہ",
|
||||||
|
"settings.ai.reset_all": "سب کو ڈیفالٹ پر ری سیٹ کریں",
|
||||||
|
"settings.ai.system_prompt": "سسٹم پرومپٹ",
|
||||||
|
"settings.ai.system_prompt_placeholder": "مثلاً آپ ایک مختصر میٹنگ خلاصہ ساز ہیں۔",
|
||||||
|
"settings.ai.system_prompt_hint": "WhispAssist کے مطلوبہ آؤٹ پٹ فارمیٹ سے پہلے شامل کیا جاتا ہے، تاکہ آپ کی ہدایات خلاصہ/ایکشن آئٹم پارسنگ کو توڑ نہ سکیں۔",
|
||||||
|
"settings.ai.think": "سوچیں",
|
||||||
|
"settings.ai.think_help": "استدلال کی کوشش (صرف استدلال ماڈلز)۔",
|
||||||
|
"settings.ai.keep_alive": "فعال رکھیں",
|
||||||
|
"settings.ai.keep_alive_help": "ماڈل RAM میں کتنی دیر رہتا ہے۔ مثلاً 5m، 1h، 0 (ان لوڈ)، -1 (ہمیشہ)۔",
|
||||||
|
"settings.ai.runtime_hardware": "رن ٹائم اور ہارڈ ویئر",
|
||||||
|
"settings.ai.rarely_needed": "— شاذ و نادر ہی درکار",
|
||||||
|
"settings.ai.save_advanced": "اعلیٰ محفوظ کریں",
|
||||||
|
"settings.ai.saved": "محفوظ ہو گیا",
|
||||||
|
"settings.ai.turn_off": "بند کریں",
|
||||||
|
"settings.ai.default_value": "ڈیفالٹ {value}۔",
|
||||||
|
"settings.ai.reset_default": "ڈیفالٹ پر ری سیٹ کریں",
|
||||||
|
"settings.ai.reset_field": "{name} ری سیٹ کریں",
|
||||||
|
|
||||||
|
"settings.mcp.title": "MCP سرور",
|
||||||
|
"settings.mcp.banner_1": "یہ آپ کے اپنے کوڈنگ ایجنٹ (Claude Code، Codex، Copilot، OpenCode، …) کو آپ کی مقامی مشین پر میٹنگ سیاق و سباق حاصل کرنے دیتا ہے۔ منسلک ہونے کے بعد،",
|
||||||
|
"settings.mcp.banner_agent": "وہ ایجنٹ",
|
||||||
|
"settings.mcp.banner_2": "جو کچھ پڑھتا ہے اسے اپنے ماڈل فراہم کنندہ کے کلاؤڈ کو بھیج سکتا ہے — WhispAssist کے کنٹرول سے باہر۔ WhispAssist خود یہ ڈیٹا کبھی کہیں نہیں بھیجتا؛ سرور صرف اس آلے پر سنتا ہے (",
|
||||||
|
"settings.mcp.banner_3": ")، اور ہر پڑھائی نیچے لاگ ہوتی ہے۔",
|
||||||
|
"settings.mcp.enable": "MCP سرور فعال کریں",
|
||||||
|
"settings.mcp.enable_hint": "بطور ڈیفالٹ بند۔ صرف لوپ بیک، ٹوکن سے محفوظ — نیٹ ورک سے کچھ قابل رسائی نہیں۔",
|
||||||
|
"settings.mcp.transport": "ٹرانسپورٹ",
|
||||||
|
"settings.mcp.transport_http": "اسٹریمنگ HTTP",
|
||||||
|
"settings.mcp.transport_stdio": "stdio (ایجنٹ ایک پروسیس بناتا ہے)",
|
||||||
|
"settings.mcp.port": "پورٹ",
|
||||||
|
"settings.mcp.transport_change_hint": "ٹرانسپورٹ/پورٹ تبدیل کرنے کے لیے، پہلے سرور بند کریں، پھر دوبارہ آن کریں۔",
|
||||||
|
"settings.mcp.scope_title": "دائرہ کار",
|
||||||
|
"settings.mcp.expose": "بے نقاب کریں",
|
||||||
|
"settings.mcp.scope_none": "کوئی نہیں — کچھ شیئر نہیں ہوتا",
|
||||||
|
"settings.mcp.scope_selected": "منتخب شدہ — صرف وہ فیچر بریفس جنہیں آپ نے شیئرڈ کے طور پر نشان زد کیا ہے",
|
||||||
|
"settings.mcp.scope_all": "سب — میٹنگز، نقلیں، ایکشن آئٹمز اور شیئرڈ بریفس",
|
||||||
|
"settings.mcp.expose_recordings": "محفوظ ریکارڈنگ والی میٹنگز کی بھی اجازت دیں (بطور ڈیفالٹ بند — جب تک یہ آن نہ ہو، ریکارڈ شدہ میٹنگ کی نقل «سب» دائرہ کار میں بھی روکی جاتی ہے)",
|
||||||
|
"settings.mcp.token_title": "نیا تصدیقی ٹوکن — ایک بار دکھایا جاتا ہے، اسے ابھی کاپی کریں",
|
||||||
|
"settings.mcp.token_hint": "یہ دوبارہ نہیں دکھایا جائے گا۔ یہ آپ کے OS کریڈنشل اسٹور میں محفوظ ہے؛ اگر آپ اسے کھو دیں، تو نیا بنانے کے لیے سرور بند کریں اور دوبارہ آن کریں۔",
|
||||||
|
"settings.mcp.copied": "کاپی ہو گیا",
|
||||||
|
"settings.mcp.copy": "کاپی کریں",
|
||||||
|
"settings.mcp.endpoint": "اینڈ پوائنٹ",
|
||||||
|
"settings.mcp.endpoint_hint": "اپنے ایجنٹ کی MCP کلائنٹ ترتیب کو {kind} کی طرف اشارہ کریں، اوپر والے ٹوکن کو bearer کریڈنشل کے طور پر استعمال کرتے ہوئے۔",
|
||||||
|
"settings.mcp.kind_command": "کمانڈ",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "رسائی لاگ",
|
||||||
|
"settings.mcp.access_log_hint": "ایجنٹ کی ہر ٹول پڑھائی، اجازت شدہ یا مسترد (FR-MCP-5)۔",
|
||||||
|
"settings.mcp.no_reads": "ابھی تک کسی ایجنٹ نے کچھ نہیں پڑھا۔",
|
||||||
|
"settings.mcp.log_meeting": "میٹنگ {id}",
|
||||||
|
"settings.mcp.refresh": "ریفریش کریں",
|
||||||
|
|
||||||
|
"settings.privacy.title": "رازداری",
|
||||||
|
"settings.privacy.intro": "WhispAssist کو اس وقت حقیقتاً اس آلے سے باہر کیا بھیجنے کی اجازت ہے۔ سب کچھ بند ہونے پر (ڈیفالٹ)، آلے سے کچھ بھی باہر نہیں جاتا۔",
|
||||||
|
"settings.privacy.llm_endpoint": "LLM اینڈ پوائنٹ",
|
||||||
|
"settings.privacy.off": "بند",
|
||||||
|
"settings.privacy.local_only": "صرف مقامی",
|
||||||
|
"settings.privacy.leaves_device": "یہ آلہ چھوڑ دیتا ہے",
|
||||||
|
"settings.privacy.sync_label": "سِنک",
|
||||||
|
"settings.privacy.enabled": "فعال",
|
||||||
|
"settings.privacy.mcp_label": "MCP سرور",
|
||||||
|
"settings.privacy.mcp_on": "آن · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "صرف لوپ بیک پر آنے والا — اوپر والی باہر جانے والی فہرست میں کچھ شامل نہیں کرتا۔ منسلک ایجنٹ پھر بھی جو پڑھتا ہے اسے اپنے ماڈل فراہم کنندہ کو بھیج سکتا ہے؛ MCP سرور ٹیب دیکھیں۔",
|
||||||
|
"settings.privacy.egress_title": "باہر جانے کی اجازت فہرست",
|
||||||
|
"settings.privacy.no_egress": "کوئی میزبان اجازت فہرست میں نہیں — WA کوئی مواد باہر نہیں بھیجتا۔",
|
||||||
|
"settings.privacy.sync_targets_title": "سِنک اہداف",
|
||||||
|
"settings.privacy.third_party": "فریق ثالث",
|
||||||
|
"settings.privacy.your_server": "آپ کا سرور",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "کوئی TLS نہیں",
|
||||||
|
"settings.privacy.refresh": "ریفریش کریں",
|
||||||
|
"settings.privacy.unavailable": "رازداری خود جانچ دستیاب نہیں۔",
|
||||||
|
"settings.privacy.vault_title": "خفیہ کاری والٹ",
|
||||||
|
"settings.privacy.vault_intro": "ایک پاس ورڈ کے ساتھ نوٹس، نقلیں اور خلاصے آرام کی حالت میں خفیہ کریں۔ (آڈیو فائلیں ابھی خفیہ نہیں ہوتیں۔)",
|
||||||
|
"settings.privacy.vault_password": "والٹ پاس ورڈ",
|
||||||
|
"settings.privacy.enable_vault": "والٹ فعال کریں",
|
||||||
|
"settings.privacy.vault_pw_hint": "کم از کم 8 حروف استعمال کریں۔ اگر آپ اسے بھول جائیں، تو خفیہ شدہ مواد بازیافت نہیں ہو سکتا۔",
|
||||||
|
"settings.privacy.vault_locked_1": "والٹ ",
|
||||||
|
"settings.privacy.locked_word": "مقفل",
|
||||||
|
"settings.privacy.vault_locked_2": " ہے۔ خفیہ شدہ میٹنگز پڑھنے کے لیے ان لاک کریں۔",
|
||||||
|
"settings.privacy.password": "پاس ورڈ",
|
||||||
|
"settings.privacy.unlock": "ان لاک کریں",
|
||||||
|
"settings.privacy.vault_unlocked_1": "والٹ ",
|
||||||
|
"settings.privacy.unlocked_word": "ان لاک",
|
||||||
|
"settings.privacy.vault_unlocked_2": " ہے۔ نئے نوٹس، نقلیں اور خلاصے آرام کی حالت میں خفیہ کیے جاتے ہیں۔",
|
||||||
|
"settings.privacy.lock_now": "ابھی مقفل کریں",
|
||||||
|
"settings.privacy.change_password": "پاس ورڈ تبدیل کریں",
|
||||||
|
"settings.privacy.current_password": "موجودہ پاس ورڈ",
|
||||||
|
"settings.privacy.new_password": "نیا پاس ورڈ",
|
||||||
|
|
||||||
|
"settings.about.title": "بارے میں",
|
||||||
|
"settings.about.tagline": "ایک مکمل طور پر مقامی، اوپن سورس، Windows-نیٹو میٹنگ اسسٹنٹ۔",
|
||||||
|
"settings.about.build_commit": "بلڈ کمٹ",
|
||||||
|
|
||||||
|
"consent.heading": "ریکارڈ کرنے سے پہلے",
|
||||||
|
"consent.body": "شرکاء کی رضامندی کے بغیر گفتگو ریکارڈ کرنا آپ کے علاقے میں غیر قانونی ہو سکتا ہے۔ اپنے مقامی ریکارڈنگ قوانین دیکھیں۔ یہ ایک احتیاط ہے، قانونی مشورہ نہیں۔",
|
||||||
|
"consent.accept": "میں سمجھتا ہوں — ریکارڈنگ فعال کریں",
|
||||||
|
"consent.cancel": "منسوخ کریں",
|
||||||
|
|
||||||
|
"hosted.heading": "{provider} استعمال کرنے سے پہلے",
|
||||||
|
"hosted.body_1": "کے ساتھ تیار کرنا",
|
||||||
|
"hosted.body_2": "اس میٹنگ کی نقل ان کے سرورز کو بھیجتا ہے — یہ اس آلے کو چھوڑ دیتی ہے اور ان کی رازداری پالیسی کے تابع ہے۔ ڈیٹا کے آپ کا آلہ چھوڑنے کے بعد اس کی ہینڈلنگ پر WhispAssist کا کوئی کنٹرول نہیں۔ یہ بطور ڈیفالٹ بند ہے؛ آپ اسے ابھی منتخب کر رہے ہیں۔",
|
||||||
|
"hosted.accept": "میں سمجھتا ہوں — جاری رکھیں",
|
||||||
|
"hosted.cancel": "منسوخ کریں",
|
||||||
|
|
||||||
|
"theme.group": "تھیم",
|
||||||
|
"theme.system": "سسٹم سے مطابقت",
|
||||||
|
"theme.light": "ہلکا",
|
||||||
|
"theme.dark": "گہرا",
|
||||||
|
|
||||||
|
"import.dialog_label": "کسی فائل یا URL سے میٹنگ شامل کریں",
|
||||||
|
"import.title": "میٹنگ شامل کریں",
|
||||||
|
"import.close": "بند کریں",
|
||||||
|
"import.close_title": "بند کریں (Esc)",
|
||||||
|
"import.body": "ایک موجودہ ریکارڈنگ درآمد کریں — ایک مقامی آڈیو/ویڈیو فائل، یا ایک لنک (YouTube، ایک اسٹریمنگ صفحہ، یا ایک براہ راست میڈیا URL)۔ اسے لائیو ریکارڈنگ کی طرح ہی نقل اور مقرر علیحدہ کیا جاتا ہے۔",
|
||||||
|
"import.file_or_url": "فائل یا URL",
|
||||||
|
"import.source_placeholder": "ایک URL پیسٹ کریں، یا کسی فائل کے لیے براؤز کریں…",
|
||||||
|
"import.choose_file": "ایک مقامی فائل منتخب کریں",
|
||||||
|
"import.browse": "براؤز کریں…",
|
||||||
|
"import.title_label": "عنوان",
|
||||||
|
"import.optional": "اختیاری",
|
||||||
|
"import.title_placeholder": "بطور ڈیفالٹ فائل کا نام",
|
||||||
|
"import.filter_av": "آڈیو / ویڈیو",
|
||||||
|
"import.requires_1": "درکار ہے",
|
||||||
|
"import.requires_2": "انسٹال شدہ اور آپ کے PATH میں (اور",
|
||||||
|
"import.requires_3": "URLs کے لیے)۔ WhispAssist انہیں شامل نہیں کرتا۔",
|
||||||
|
"import.importing": "درآمد ہو رہا ہے… اس میں کچھ وقت لگ سکتا ہے",
|
||||||
|
"import.import": "درآمد کریں",
|
||||||
|
"import.cancel": "منسوخ کریں",
|
||||||
|
|
||||||
|
"tagchip.filter": "«{tag}» ٹیگ والی میٹنگز فلٹر کریں",
|
||||||
|
"tagchip.remove": "ٹیگ {tag} ہٹائیں",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "سسٹم اور مائیکروفون ان پٹ سطح",
|
||||||
|
"levelmeter.system": "سسٹم ان پٹ سطح",
|
||||||
|
|
||||||
|
"app.tagline": "مقامی · نجی",
|
||||||
|
"app.note_template": "نوٹ ٹیمپلیٹ",
|
||||||
|
"app.no_template": "کوئی ٹیمپلیٹ نہیں",
|
||||||
|
"app.add_meeting": "میٹنگ شامل کریں",
|
||||||
|
"app.add_meeting_title": "کسی فائل یا URL سے میٹنگ شامل کریں",
|
||||||
|
"app.record": "ریکارڈ کریں",
|
||||||
|
"app.record_title": "ریکارڈنگ شروع کریں (Ctrl+Shift+R)",
|
||||||
|
"app.stop": "روکیں",
|
||||||
|
"app.stop_title": "ریکارڈنگ روکیں (Ctrl+Shift+R)",
|
||||||
|
"app.cancel": "منسوخ کریں",
|
||||||
|
"app.cancel_title": "اس ریکارڈنگ کو رد کریں اور حذف کریں",
|
||||||
|
"app.recording": "ریکارڈ ہو رہا ہے…",
|
||||||
|
"app.backend_title": "فعال نقل بیک اینڈ",
|
||||||
|
"app.retention_title": "اس میٹنگ کے لیے آڈیو کو .wav کے طور پر محفوظ کریں",
|
||||||
|
"app.saving": "محفوظ ہو رہا ہے",
|
||||||
|
"app.not_saved": "محفوظ نہیں",
|
||||||
|
"app.reconnecting": "آڈیو آلہ دوبارہ منسلک ہو رہا ہے…",
|
||||||
|
"app.settings": "ترتیبات",
|
||||||
|
"app.settings_title": "ترتیبات (Ctrl+,)",
|
||||||
|
"app.discard_confirm": "اس ریکارڈنگ کو رد کریں؟ اس کا آڈیو اور نقل حذف کر دی جائے گی۔",
|
||||||
|
"app.announce_started": "ریکارڈنگ شروع ہوئی",
|
||||||
|
"app.announce_paused": "ریکارڈنگ روکی گئی",
|
||||||
|
"app.announce_stopped": "ریکارڈنگ رک گئی",
|
||||||
|
"app.consent_dialog": "ریکارڈنگ رضامندی",
|
||||||
|
"app.vault_dialog": "خفیہ کاری والٹ ان لاک کریں",
|
||||||
|
"app.vault_title": "خفیہ کاری والٹ ان لاک کریں",
|
||||||
|
"app.vault_desc": "آپ کی میٹنگز آرام کی حالت میں خفیہ ہیں۔ انہیں پڑھنے کے لیے اپنا پاس ورڈ درج کریں۔",
|
||||||
|
"app.vault_password": "والٹ پاس ورڈ",
|
||||||
|
"app.vault_incorrect": "غلط پاس ورڈ",
|
||||||
|
"app.unlock": "ان لاک کریں",
|
||||||
|
"app.continue_locked": "مقفل رہتے ہوئے جاری رکھیں",
|
||||||
|
"app.show_meetings": "میٹنگ فہرست دکھائیں",
|
||||||
|
"app.hide_meetings": "میٹنگ فہرست چھپائیں",
|
||||||
|
"app.show_summary": "خلاصہ پینل دکھائیں",
|
||||||
|
"app.hide_summary": "خلاصہ پینل چھپائیں",
|
||||||
|
"app.resize_meetings": "میٹنگ فہرست کا سائز تبدیل کریں",
|
||||||
|
"app.resize_summary": "خلاصہ پینل کا سائز تبدیل کریں",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "میٹنگز تلاش کریں…",
|
||||||
|
"meetings.search_aria": "میٹنگز تلاش کریں",
|
||||||
|
"meetings.filter_tag_aria": "ٹیگ کے مطابق فلٹر کریں",
|
||||||
|
"meetings.all_tags": "تمام ٹیگز",
|
||||||
|
"meetings.from_date_aria": "تاریخ سے",
|
||||||
|
"meetings.to_date_aria": "تاریخ تک",
|
||||||
|
"meetings.bulk_format_aria": "بلک برآمد فارمیٹ",
|
||||||
|
"meetings.bulk_export": "بلک برآمد",
|
||||||
|
"meetings.bulk_export_title": "اوپر والے ٹیگ/تاریخ فلٹرز سے مماثل ہر میٹنگ برآمد کریں",
|
||||||
|
"meetings.exporting": "برآمد ہو رہا ہے…",
|
||||||
|
"meetings.exported_one": "{n} میٹنگ برآمد کی گئی۔",
|
||||||
|
"meetings.exported_many": "{n} میٹنگز برآمد کی گئیں۔",
|
||||||
|
"meetings.searching": "تلاش ہو رہا ہے…",
|
||||||
|
"meetings.loading": "میٹنگز لوڈ ہو رہی ہیں…",
|
||||||
|
"meetings.no_matches": "کوئی مماثلت نہیں۔",
|
||||||
|
"meetings.empty": "ابھی کوئی میٹنگ نہیں — شروع کرنے کے لیے اوپر ریکارڈ پر کلک کریں۔",
|
||||||
|
"meetings.status.recording": "ریکارڈ ہو رہا ہے",
|
||||||
|
"meetings.status.transcribing": "نقل ہو رہا ہے",
|
||||||
|
"meetings.status.recovering": "بحال ہو رہا ہے",
|
||||||
|
"meetings.status.error": "خرابی",
|
||||||
|
"meetings.resume": "نقل دوبارہ شروع کریں",
|
||||||
|
"meetings.delete_aria": "میٹنگ حذف کریں",
|
||||||
|
"meetings.delete_confirm": "اس میٹنگ کو حذف کریں؟ یہ اس کی ریکارڈنگ، نقل اور نوٹس ہٹا دیتا ہے۔",
|
||||||
|
|
||||||
|
"transcript.heading": "نقل",
|
||||||
|
"transcript.title_aria": "میٹنگ عنوان",
|
||||||
|
"transcript.lang_title": "نقل کی زبان",
|
||||||
|
"transcript.lang_auto": "شناخت ہو رہی ہے…",
|
||||||
|
"transcript.show": "نقل دکھائیں",
|
||||||
|
"transcript.hide": "نقل چھپائیں",
|
||||||
|
"transcript.empty": "اس میٹنگ کے لیے کوئی نقل نہیں۔",
|
||||||
|
"transcript.will_appear": "جیسے جیسے آپ ریکارڈ کریں گے نقل یہاں ظاہر ہوگی۔",
|
||||||
|
"transcript.play_from_here": "یہاں سے چلائیں",
|
||||||
|
"transcript.resize": "نقل اور نوٹس کا سائز تبدیل کریں",
|
||||||
|
"transcript.reprocess_placeholder": "اس کے ساتھ دوبارہ نقل کریں…",
|
||||||
|
"transcript.reprocess_lang_aria": "دوبارہ عمل زبان",
|
||||||
|
"transcript.reprocess_keep_lang": "موجودہ زبان رکھیں",
|
||||||
|
"transcript.reprocess_go": "چلائیں",
|
||||||
|
"transcript.retranscribing": "دوبارہ نقل ہو رہا ہے…",
|
||||||
|
"transcript.has_note": "ایک نوٹ ہے",
|
||||||
|
"transcript.note_placeholder": "اس لمحے کے لیے ایک نوٹ شامل کریں…",
|
||||||
|
"transcript.note_aria": "اس نقل لائن کے لیے نوٹ",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Word دستاویز",
|
||||||
|
"transcript.filter_obsidian": "Obsidian نوٹ",
|
||||||
|
|
||||||
|
"notes.heading": "نوٹس",
|
||||||
|
"notes.show": "نوٹس دکھائیں",
|
||||||
|
"notes.hide": "نوٹس چھپائیں",
|
||||||
|
"notes.placeholder": "نوٹس…",
|
||||||
|
"notes.live_placeholder": "بات کرتے ہوئے نوٹس ٹائپ کریں…",
|
||||||
|
"notes.toolbar_aria": "نوٹس فارمیٹنگ",
|
||||||
|
"notes.bold": "جلی",
|
||||||
|
"notes.italic": "ترچھا",
|
||||||
|
"notes.h1": "سرخی 1",
|
||||||
|
"notes.h2": "سرخی 2",
|
||||||
|
"notes.bullet": "بلٹ فہرست",
|
||||||
|
"notes.checkbox_title": "چیک باکس",
|
||||||
|
"notes.checkbox_aria": "چیک باکس فہرست آئٹم",
|
||||||
|
"notes.edit_raw": "خام markdown میں ترمیم کریں",
|
||||||
|
"notes.render": "markdown رینڈر کریں",
|
||||||
|
"notes.editor": "ایڈیٹر",
|
||||||
|
"notes.preview": "پیش نظارہ",
|
||||||
|
"notes.export_md_title": "نوٹس کو .md کے طور پر برآمد کریں",
|
||||||
|
"notes.export_pdf_title": "نوٹس کو .pdf کے طور پر برآمد کریں",
|
||||||
|
"notes.export_docx_title": "نوٹس کو .docx کے طور پر برآمد کریں",
|
||||||
|
"notes.export_bundle_title": "آڈیو + نقل + نوٹس کو ایک فولڈر میں برآمد کریں",
|
||||||
|
"notes.export_obsidian_title": "ایک Obsidian نوٹ برآمد کریں (نوٹس + خلاصہ + نقل، کوئی آڈیو نہیں)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "ریکارڈنگ",
|
||||||
|
"summary.loading_recording": "ریکارڈنگ لوڈ ہو رہی ہے…",
|
||||||
|
"summary.sync_heading": "سِنک",
|
||||||
|
"summary.uploading": "اپ لوڈ ہو رہا ہے…",
|
||||||
|
"summary.upload_now": "ابھی اپ لوڈ کریں",
|
||||||
|
"summary.no_uploads": "اس میٹنگ کے لیے ابھی کوئی اپ لوڈ نہیں۔",
|
||||||
|
"summary.retry": "دوبارہ کوشش کریں",
|
||||||
|
"summary.tags_heading": "ٹیگز",
|
||||||
|
"summary.tags_select": "کسی میٹنگ کو ٹیگ کرنے کے لیے اسے منتخب کریں۔",
|
||||||
|
"summary.tag_add_placeholder": "ٹیگ شامل کریں…",
|
||||||
|
"summary.tag_first_placeholder": "منصوبہ، کلائنٹ، موضوع…",
|
||||||
|
"summary.generating": "تیار ہو رہا ہے…",
|
||||||
|
"summary.generate_tags": "ٹیگز تیار کریں",
|
||||||
|
"summary.saving": "محفوظ ہو رہا ہے…",
|
||||||
|
"summary.save_tags": "ٹیگز محفوظ کریں",
|
||||||
|
"summary.summary_heading": "خلاصہ",
|
||||||
|
"summary.ai_provider": "AI فراہم کنندہ",
|
||||||
|
"summary.provider_off": "بند",
|
||||||
|
"summary.provider_custom": "حسب ضرورت",
|
||||||
|
"summary.local": "مقامی",
|
||||||
|
"summary.leaves_device": "یہ آلہ چھوڑ دیتا ہے",
|
||||||
|
"summary.this_hosted_provider": "یہ میزبان فراہم کنندہ",
|
||||||
|
"summary.select_generate": "خلاصہ تیار کرنے کے لیے ایک میٹنگ منتخب کریں۔",
|
||||||
|
"summary.decisions_heading": "فیصلے",
|
||||||
|
"summary.regenerate": "دوبارہ تیار کریں",
|
||||||
|
"summary.generated_hint": "میٹنگ کے بعد مقامی طور پر تیار کیا گیا (ایک مقامی LLM فراہم کنندہ درکار)۔",
|
||||||
|
"summary.no_provider": "کوئی AI فراہم کنندہ ترتیب نہیں دیا گیا — ترتیبات میں ایک فعال کریں۔",
|
||||||
|
"summary.generate_summary": "خلاصہ تیار کریں",
|
||||||
|
"summary.briefs_heading": "فیچر بریفس",
|
||||||
|
"summary.briefs_select": "فیچر بریفس بنانے یا دیکھنے کے لیے ایک میٹنگ منتخب کریں۔",
|
||||||
|
"summary.briefs_desc": "اس میٹنگ کو ایک ایجنٹ کے لیے تیار تفصیلات میں کشید کرتا ہے (ایک مقامی LLM فراہم کنندہ درکار) — اسے کسی کوڈنگ ایجنٹ کو دیں، یا MCP سرور آن ہونے پر اس کے ذریعے فراہم کریں۔",
|
||||||
|
"summary.brief_repo_placeholder": "ہدف ریپو (اختیاری)، مثلاً acme/reporting-web",
|
||||||
|
"summary.distilling": "کشید ہو رہا ہے…",
|
||||||
|
"summary.create_brief": "فیچر بریف بنائیں",
|
||||||
|
"summary.brief_mcp_title": "MCP سرور آن ہونے پر دستیاب",
|
||||||
|
"summary.copied": "کاپی ہو گیا",
|
||||||
|
"summary.copy_md": "Markdown کے طور پر کاپی کریں",
|
||||||
|
"summary.copy_json": "JSON کے طور پر کاپی کریں",
|
||||||
|
"summary.brief_problem": "مسئلہ",
|
||||||
|
"summary.brief_outcome": "مطلوبہ نتیجہ",
|
||||||
|
"summary.brief_criteria": "قبولیت کے معیار",
|
||||||
|
"summary.brief_none": "کچھ کیپچر نہیں ہوا۔",
|
||||||
|
"summary.brief_context": "سیاق و سباق",
|
||||||
|
"summary.action_items_heading": "ایکشن آئٹمز",
|
||||||
|
"summary.ai_select": "اس کے ایکشن آئٹمز دیکھنے کے لیے ایک میٹنگ منتخب کریں۔",
|
||||||
|
"summary.ai_empty": "ابھی کوئی نہیں — نیچے ایک شامل کریں، یا انہیں خودکار طور پر نکالنے کے لیے ایک خلاصہ تیار کریں۔",
|
||||||
|
"summary.confirmed": "تصدیق شدہ",
|
||||||
|
"summary.ai_text_placeholder": "ایکشن آئٹم…",
|
||||||
|
"summary.ai_text_aria": "ایکشن آئٹم متن",
|
||||||
|
"summary.owner": "مالک",
|
||||||
|
"summary.due_date": "مقررہ تاریخ",
|
||||||
|
"summary.reminder_title": "اس مقررہ تاریخ کے لیے ایک مقامی یاد دہانی شیڈول کریں",
|
||||||
|
"summary.ai_delete_title": "اس ایکشن آئٹم کو حذف کریں",
|
||||||
|
"summary.ai_delete_aria": "ایکشن آئٹم حذف کریں",
|
||||||
|
"summary.add_action_item": "ایکشن آئٹم شامل کریں",
|
||||||
|
"summary.save_action_items": "ایکشن آئٹمز محفوظ کریں",
|
||||||
|
"summary.calendar_heading": "کیلنڈر ایونٹ",
|
||||||
|
"summary.cal_select": "کسی میٹنگ کو کیلنڈر ایونٹ سے منسلک کرنے کے لیے اسے منتخب کریں۔",
|
||||||
|
"summary.event_search_placeholder": "ایونٹ عنوان تلاش کریں…",
|
||||||
|
"summary.linked_event": "منسلک ایونٹ",
|
||||||
|
"summary.change_event": "ایونٹ تبدیل کریں…",
|
||||||
|
"summary.link_event": "ایک ایونٹ منسلک کریں…",
|
||||||
|
"summary.untitled_event": "(بلا عنوان)",
|
||||||
|
"summary.no_events_1": "ابھی تک کوئی ایونٹ درآمد نہیں ہوا — ایک",
|
||||||
|
"summary.no_events_2": "ترتیبات → کیلنڈر سے درآمد کریں۔",
|
||||||
|
"summary.no_events_match": "کوئی ایونٹ اس تلاش/تاریخ سے مماثل نہیں — ایک صاف کرنے کی کوشش کریں۔",
|
||||||
|
"summary.organizer_label": "منتظم: {name}",
|
||||||
|
"summary.participants_heading": "شرکاء",
|
||||||
|
"summary.participants_hint": "منسلک کیلنڈر ایونٹ سے بھرا جاتا ہے۔",
|
||||||
|
"summary.speakers_heading": "مقررین",
|
||||||
|
"summary.speakers_select": "اس کے مقررین کو نام دینے کے لیے ایک میٹنگ منتخب کریں۔",
|
||||||
|
"summary.no_speakers": "ابھی تک کوئی مقرر شناخت نہیں ہوا۔",
|
||||||
|
"summary.speaker_name_placeholder": "مقرر کا نام",
|
||||||
|
"summary.save": "محفوظ کریں",
|
||||||
|
"summary.cancel": "منسوخ کریں",
|
||||||
|
"summary.name_speaker": "اس مقرر کو نام دیں…",
|
||||||
|
"summary.add_new_name": "+ نیا نام شامل کریں…",
|
||||||
|
|
||||||
|
"settings.recording.title": "ریکارڈنگ",
|
||||||
|
"settings.recording.record_default": "بطور ڈیفالٹ میٹنگز ریکارڈ کریں",
|
||||||
|
"settings.recording.save_audio_as": "آڈیو کو اس طرح محفوظ کریں",
|
||||||
|
"settings.recording.default_hint": "بطور ڈیفالٹ بند۔ جب بند ہو، آڈیو صرف نقل تیار کرنے کے لیے استعمال ہوتا ہے اور میٹنگ کے حتمی ہونے پر حذف کر دیا جاتا ہے۔ آپ فی میٹنگ ریکارڈنگ بھی ٹوگل کر سکتے ہیں۔",
|
||||||
|
"settings.recording.consent_label": "رضامندی: {status}۔",
|
||||||
|
"settings.recording.consent_ack": "تسلیم شدہ",
|
||||||
|
"settings.recording.consent_not": "ابھی تسلیم نہیں",
|
||||||
|
"settings.recording.auto_label": "جب کوئی کیلنڈر ایونٹ شروع ہو تو خودکار طور پر ریکارڈنگ شروع کریں",
|
||||||
|
"settings.recording.auto_hint": "صرف جب WhispAssist کھلا ہو۔ جب کسی درآمد شدہ کیلنڈر ایونٹ کا آغاز وقت آتا ہے، تو ریکارڈنگ خودکار طور پر شروع ہو جاتی ہے (اوپر والی آپ کی ڈیفالٹ برقراری ترتیب استعمال کرتے ہوئے)۔ پس منظر میں کچھ نہیں چلتا — ٹائمر صرف ایپ چلنے کے دوران مسلح ہوتا ہے۔ ترتیبات → کیلنڈر کے تحت ایونٹس درآمد کریں۔"
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
{
|
||||||
|
"nav.recording": "录音",
|
||||||
|
"nav.hardware": "硬件",
|
||||||
|
"nav.storage": "存储",
|
||||||
|
"nav.calendar": "日历",
|
||||||
|
"nav.sync": "同步",
|
||||||
|
"nav.ai": "AI",
|
||||||
|
"nav.mcp": "MCP 服务器",
|
||||||
|
"nav.privacy": "隐私",
|
||||||
|
"nav.about": "关于",
|
||||||
|
"nav.language": "语言",
|
||||||
|
|
||||||
|
"settings.language.title": "语言",
|
||||||
|
"settings.language.display": "显示语言",
|
||||||
|
"settings.language.display_hint": "应用界面的语言。添加一种语言就像放入一个翻译文件那样简单。",
|
||||||
|
|
||||||
|
"settings.transcription.title": "转录语言",
|
||||||
|
"settings.transcription.label": "语言",
|
||||||
|
"settings.transcription.auto": "自动检测",
|
||||||
|
"settings.transcription.applies_hint": "适用于下一次录音。实际使用的语言之后会显示在每次会议上。",
|
||||||
|
"settings.transcription.english_only": "仅英语",
|
||||||
|
"settings.transcription.model_english_only": "“{model}”仅支持英语。",
|
||||||
|
"settings.transcription.no_model": "未选择模型。",
|
||||||
|
"settings.transcription.switch_multilingual": "切换到上方的多语言模型即可选择语言。",
|
||||||
|
|
||||||
|
"settings.hardware.title": "硬件",
|
||||||
|
"settings.hardware.active_backend": "活动后端",
|
||||||
|
"settings.hardware.model_meta": "· 模型 {size}",
|
||||||
|
"settings.hardware.preferred_backend": "首选后端",
|
||||||
|
"settings.hardware.auto_backend": "自动(最佳可用)",
|
||||||
|
"settings.hardware.not_available": "不可用",
|
||||||
|
"settings.hardware.fallback_hint": "如果所选后端加载失败,将自动回退(NPU → NVIDIA → AMD → Intel → CPU)。",
|
||||||
|
"settings.hardware.audio_devices": "音频设备",
|
||||||
|
"settings.hardware.recording_device": "录音设备",
|
||||||
|
"settings.hardware.default_system_audio": "默认系统音频",
|
||||||
|
"settings.hardware.recording_device_hint": "WhispAssist 会录制此设备播放的内容(环回)——通话的另一方。如果你不希望它跟随 Windows 系统默认设备,请选择特定的输出。",
|
||||||
|
"settings.hardware.microphone": "麦克风",
|
||||||
|
"settings.hardware.mic_off": "关闭——不捕获我的麦克风",
|
||||||
|
"settings.hardware.default_mic": "默认麦克风",
|
||||||
|
"settings.hardware.mic_hint": "将你自己的声音加入实时转录,从而捕获会议的双方。内容留在你的设备上——不会上传任何内容。选择“关闭”即可仅转录上方的系统音频。",
|
||||||
|
"settings.hardware.npu_detected": "检测到 NPU 加速器",
|
||||||
|
"settings.hardware.ready": "就绪",
|
||||||
|
"settings.hardware.downloading": "正在下载…",
|
||||||
|
"settings.hardware.package_needed": "需要软件包",
|
||||||
|
"settings.hardware.npu_hint": "你的 NPU 需要一个一次性加速软件包(Whisper ONNX 模型{extra})。它会在后台下载;你也可以立即开始。",
|
||||||
|
"settings.hardware.plus_openvino": " + OpenVINO 运行时",
|
||||||
|
"settings.hardware.download_npu": "下载 NPU 软件包",
|
||||||
|
"settings.hardware.dml_title": "GPU 加速(DirectML)",
|
||||||
|
"settings.hardware.dml_hint": "你的 GPU 可以通过 DirectML 在没有 Vulkan 的情况下加速——一个一次性软件包(Whisper ONNX 模型{extra})。",
|
||||||
|
"settings.hardware.plus_directml": " + DirectML 运行时",
|
||||||
|
"settings.hardware.download_dml": "下载 DirectML 软件包",
|
||||||
|
"settings.hardware.detection_unavailable": "硬件检测不可用。",
|
||||||
|
"settings.hardware.low_overhead": "低开销预设",
|
||||||
|
"settings.hardware.low_overhead_note": "CPU + 最小模型,适用于电池/后台使用",
|
||||||
|
"settings.hardware.models": "模型",
|
||||||
|
"settings.hardware.downloading_model": "正在下载 {name}",
|
||||||
|
"settings.hardware.active_badge": "活动",
|
||||||
|
"settings.hardware.installed": "已安装",
|
||||||
|
"settings.hardware.use": "使用",
|
||||||
|
"settings.hardware.remove": "移除",
|
||||||
|
"settings.hardware.cant_remove_active": "无法移除活动模型",
|
||||||
|
"settings.hardware.download": "下载",
|
||||||
|
"settings.hardware.diarization": "说话人分离",
|
||||||
|
"settings.hardware.diarization_hint": "安装两个模型后,完成的录音将按说话人分离(说话人 1、说话人 2、…),而不是一段连续转录。完全离线运行,会议停止后每次会议运行一次。若没有它们,每一行都会归属于单一说话人。",
|
||||||
|
|
||||||
|
"settings.storage.title": "存储",
|
||||||
|
"settings.storage.location_label": "位置",
|
||||||
|
"settings.storage.location_hint": "尚不支持更改存储位置——这里是当前会议、模型和数据库所在的位置。",
|
||||||
|
"settings.storage.retention_title": "保留",
|
||||||
|
"settings.storage.retention_hint": "自动删除超出限制的最旧会议。启动时检查一次;留空表示无限制。绝不会触及正在录制的会议。",
|
||||||
|
"settings.storage.max_age": "最长保留天数",
|
||||||
|
"settings.storage.max_size": "最大大小(GB)",
|
||||||
|
"settings.storage.no_limit": "无限制",
|
||||||
|
"settings.storage.export_import_title": "导出与导入",
|
||||||
|
"settings.storage.export_hint_1": "以便携的包文件夹形式在计算机之间移动录音(音频、转录、笔记、摘要以及一个",
|
||||||
|
"settings.storage.export_hint_2": "清单)。导出到任意文件夹——同步的驱动器、U 盘或同步目标的本地挂载点——然后在另一台机器上导入。导入的会议会获得新的 id,因此重新导入绝不会覆盖任何内容。",
|
||||||
|
"settings.storage.export_dialog_title": "将所有会议导出到…",
|
||||||
|
"settings.storage.import_dialog_title": "导入一个包(或一个包文件夹)…",
|
||||||
|
"settings.storage.exporting": "正在导出…",
|
||||||
|
"settings.storage.export_all": "导出所有会议…",
|
||||||
|
"settings.storage.importing": "正在导入…",
|
||||||
|
"settings.storage.import_all": "导入会议…",
|
||||||
|
"settings.storage.exported_one": "已导出 {n} 个会议。",
|
||||||
|
"settings.storage.exported_many": "已导出 {n} 个会议。",
|
||||||
|
"settings.storage.imported_one": "已导入 {n} 个会议。",
|
||||||
|
"settings.storage.imported_many": "已导入 {n} 个会议。",
|
||||||
|
|
||||||
|
"settings.calendar.title": "日历与 Outlook .pst",
|
||||||
|
"settings.calendar.intro_1": "从本地 Outlook 备份导入事件和参与者",
|
||||||
|
"settings.calendar.intro_2": "——只读,不会向文件写回任何内容。没有任何内容离开此设备。",
|
||||||
|
"settings.calendar.pst_file": ".pst 文件",
|
||||||
|
"settings.calendar.no_file": "未选择文件",
|
||||||
|
"settings.calendar.browse": "浏览…",
|
||||||
|
"settings.calendar.password": "密码",
|
||||||
|
"settings.calendar.password_note": "很少需要",
|
||||||
|
"settings.calendar.optional": "可选",
|
||||||
|
"settings.calendar.import_range": "导入范围",
|
||||||
|
"settings.calendar.range_30": "最近 30 天",
|
||||||
|
"settings.calendar.range_90": "最近 90 天",
|
||||||
|
"settings.calendar.range_180": "最近 6 个月",
|
||||||
|
"settings.calendar.range_365": "最近 1 年",
|
||||||
|
"settings.calendar.range_all": "所有时间",
|
||||||
|
"settings.calendar.range_hint": "长期存在的邮箱可能包含多年的重复/节假日条目——缩小范围可将导入的日历限定为真正相关的内容。适用于此导入按钮以及启动时的自动重新导入。",
|
||||||
|
"settings.calendar.importing": "正在导入…",
|
||||||
|
"settings.calendar.import": "导入",
|
||||||
|
"settings.calendar.import_progress": "{processed}/{total} 个事件",
|
||||||
|
"settings.calendar.auto_reimport": "启动时自动重新导入此文件",
|
||||||
|
"settings.calendar.auto_reimport_hint": "在启动时运行一次,而非按计时器——重新导入可以安全地重复(现有事件会被匹配并更新,而非重复)。",
|
||||||
|
"settings.calendar.import_failed": "导入失败:{error}——文件本身未被改动;请检查路径并重试。",
|
||||||
|
"settings.calendar.cleanup_title": "清理",
|
||||||
|
"settings.calendar.cleanup_hint": "已附加到已录制会议的事件将始终保留,无论下方选择哪个选项。",
|
||||||
|
"settings.calendar.cleanup_30": "早于 30 天",
|
||||||
|
"settings.calendar.cleanup_90": "早于 90 天",
|
||||||
|
"settings.calendar.cleanup_180": "早于 6 个月",
|
||||||
|
"settings.calendar.cleanup_365": "早于 1 年",
|
||||||
|
"settings.calendar.cleanup_all": "全部删除",
|
||||||
|
"settings.calendar.cleaning": "正在清理…",
|
||||||
|
"settings.calendar.cleanup_btn": "清理日历",
|
||||||
|
"settings.calendar.cleanup_result": "已删除 {deleted}{extra}。",
|
||||||
|
"settings.calendar.cleanup_kept": ",保留了 {n} 个(已关联到会议)",
|
||||||
|
"settings.calendar.cleanup_failed": "清理失败:{error}",
|
||||||
|
"settings.calendar.imported_events": "已导入的事件",
|
||||||
|
"settings.calendar.no_events": "尚未导入任何事件。",
|
||||||
|
"settings.calendar.search_title": "搜索标题",
|
||||||
|
"settings.calendar.search_placeholder": "会议名称…",
|
||||||
|
"settings.calendar.date_label": "日期",
|
||||||
|
"settings.calendar.events_count": "{total} 个事件中的 {shown} 个",
|
||||||
|
"settings.calendar.untitled": "(无标题)",
|
||||||
|
|
||||||
|
"settings.dialog_title": "设置",
|
||||||
|
"settings.close": "关闭",
|
||||||
|
"settings.close_title": "关闭(Esc)",
|
||||||
|
|
||||||
|
"settings.sync.stub": "预览模式——同步后端尚未实现(第 9 阶段)。更改仅保留在界面中。",
|
||||||
|
"settings.sync.title": "同步与上传",
|
||||||
|
"settings.sync.enable_label": "启用将会议构件上传到已配置的目标",
|
||||||
|
"settings.sync.enable_hint": "默认关闭。除非启用此项并启用某个目标,否则不会上传任何内容。自托管目标会将数据保存在你自己的服务器上;第三方云会明确标注。",
|
||||||
|
"settings.sync.targets_title": "目标",
|
||||||
|
"settings.sync.no_targets": "尚无目标。在下方添加一个。",
|
||||||
|
"settings.sync.third_party": "第三方",
|
||||||
|
"settings.sync.your_server": "你的服务器",
|
||||||
|
"settings.sync.edit": "编辑",
|
||||||
|
"settings.sync.remove": "移除",
|
||||||
|
"settings.sync.edit_target": "编辑目标",
|
||||||
|
"settings.sync.add_target": "添加目标",
|
||||||
|
"settings.sync.webdav_hint": "WebDAV 涵盖 Nextcloud、ownCloud、Cloudreve、Seafile 和 Synology。(Seafile:在服务器端启用 SeafDAV。)",
|
||||||
|
"settings.sync.name": "名称",
|
||||||
|
"settings.sync.name_placeholder": "家庭 Nextcloud",
|
||||||
|
"settings.sync.provider": "提供商",
|
||||||
|
"settings.sync.server_url": "服务器 URL",
|
||||||
|
"settings.sync.server_url_note_1": "只需你的服务器 URL——WhispAssist 会自动添加",
|
||||||
|
"settings.sync.server_url_note_2": "。",
|
||||||
|
"settings.sync.remote_folder": "远程文件夹",
|
||||||
|
"settings.sync.username": "用户名",
|
||||||
|
"settings.sync.app_password": "应用密码",
|
||||||
|
"settings.sync.pw_keep": "留空以保留当前密码",
|
||||||
|
"settings.sync.pw_store": "存储在操作系统凭据存储中",
|
||||||
|
"settings.sync.upload_legend": "上传",
|
||||||
|
"settings.sync.artifact_transcript": "转录",
|
||||||
|
"settings.sync.artifact_notes": "笔记",
|
||||||
|
"settings.sync.artifact_summary": "摘要",
|
||||||
|
"settings.sync.artifact_recording": "录音(.wav,如已保留)",
|
||||||
|
"settings.sync.allow_plaintext": "允许对 LAN 地址使用明文 http(不推荐)",
|
||||||
|
"settings.sync.encrypt_before": "上传前加密(目标仅存储密文)",
|
||||||
|
"settings.sync.requires_vault": "——需要已解锁的保险库",
|
||||||
|
"settings.sync.test_connection": "测试连接",
|
||||||
|
"settings.sync.save_changes": "保存更改",
|
||||||
|
"settings.sync.add_target_btn": "添加目标",
|
||||||
|
"settings.sync.cancel": "取消",
|
||||||
|
"settings.sync.third_party_banner": "{kind} 是第三方云——上传会将你的数据发送到设备之外的 {kind}。",
|
||||||
|
"settings.sync.link_account": "关联 {kind} 账户…",
|
||||||
|
"settings.sync.oauth_hint": "打开 OAuth 登录(环回重定向);令牌存储在操作系统凭据存储中。",
|
||||||
|
"settings.sync.footnote": "凭据绝不会写入设置或数据库——只写入操作系统凭据存储。非 LAN 目标需要 TLS。",
|
||||||
|
|
||||||
|
"settings.ai.title": "AI 摘要提供商",
|
||||||
|
"settings.ai.intro_1": "摘要默认在本地 LLM 上运行。将此指向本机或你 LAN 上另一台机器上的 Ollama(例如",
|
||||||
|
"settings.ai.intro_2": "——两者都算作本地,因此没有任何内容离开你的网络。托管提供商(Anthropic)为可选,默认关闭,一旦你启用其中之一,就会将转录发送给第三方。",
|
||||||
|
"settings.ai.provider": "提供商",
|
||||||
|
"settings.ai.off": "关闭",
|
||||||
|
"settings.ai.provider_ollama": "Ollama(本地 / LAN)",
|
||||||
|
"settings.ai.provider_custom": "自定义(兼容 OpenAI)",
|
||||||
|
"settings.ai.provider_anthropic": "Anthropic(Claude)——托管,会离开此设备",
|
||||||
|
"settings.ai.endpoint": "端点",
|
||||||
|
"settings.ai.model": "模型",
|
||||||
|
"settings.ai.api_key": "API 密钥",
|
||||||
|
"settings.ai.api_key_set_placeholder": "••••••••••••••••(已设置——留空以保留)",
|
||||||
|
"settings.ai.show_api_key": "显示 API 密钥",
|
||||||
|
"settings.ai.hide_api_key": "隐藏 API 密钥",
|
||||||
|
"settings.ai.anthropic_banner": "Anthropic 是一项托管的第三方服务——当你生成摘要时,本次会议的转录会离开你的设备。",
|
||||||
|
"settings.ai.endpoint_banner": "此端点不在你的机器或 LAN 上——你的转录将离开你的网络。",
|
||||||
|
"settings.ai.saving": "正在保存…",
|
||||||
|
"settings.ai.save": "保存",
|
||||||
|
"settings.ai.test_connection": "测试连接",
|
||||||
|
"settings.ai.this_hosted_provider": "此托管提供商",
|
||||||
|
"settings.ai.reachable": "可访问",
|
||||||
|
"settings.ai.unreachable": "无法访问",
|
||||||
|
"settings.ai.model_count_one": "· {n} 个模型",
|
||||||
|
"settings.ai.model_count_many": "· {n} 个模型",
|
||||||
|
"settings.ai.on_lan": "· 在你的机器 / LAN 上",
|
||||||
|
"settings.ai.leaves_network": "· 离开你的网络",
|
||||||
|
"settings.ai.advanced_title": "Ollama 高级配置",
|
||||||
|
"settings.ai.changed": "已更改 {n} 项",
|
||||||
|
"settings.ai.reset_all": "全部重置为默认值",
|
||||||
|
"settings.ai.system_prompt": "系统提示词",
|
||||||
|
"settings.ai.system_prompt_placeholder": "例如:你是一个简洁的会议摘要生成器。",
|
||||||
|
"settings.ai.system_prompt_hint": "会添加在 WhispAssist 所需的输出格式之前,因此你的指令无法破坏摘要/行动项的解析。",
|
||||||
|
"settings.ai.think": "思考",
|
||||||
|
"settings.ai.think_help": "推理力度(仅限推理模型)。",
|
||||||
|
"settings.ai.keep_alive": "保持活动",
|
||||||
|
"settings.ai.keep_alive_help": "模型在 RAM 中保留多久。例如 5m、1h、0(卸载)、-1(永久)。",
|
||||||
|
"settings.ai.runtime_hardware": "运行时与硬件",
|
||||||
|
"settings.ai.rarely_needed": "——很少需要",
|
||||||
|
"settings.ai.save_advanced": "保存高级设置",
|
||||||
|
"settings.ai.saved": "已保存",
|
||||||
|
"settings.ai.turn_off": "关闭",
|
||||||
|
"settings.ai.default_value": "默认 {value}。",
|
||||||
|
"settings.ai.reset_default": "重置为默认值",
|
||||||
|
"settings.ai.reset_field": "重置 {name}",
|
||||||
|
|
||||||
|
"settings.mcp.title": "MCP 服务器",
|
||||||
|
"settings.mcp.banner_1": "这让你自己的编码代理(Claude Code、Codex、Copilot、OpenCode 等)在你的本地机器上拉取会议上下文。连接后,",
|
||||||
|
"settings.mcp.banner_agent": "该代理",
|
||||||
|
"settings.mcp.banner_2": "可能会将其读取的内容转发到其自身模型提供商的云端——超出 WhispAssist 的控制范围。WhispAssist 本身绝不会将这些数据发送到任何地方;服务器仅在此设备上监听(",
|
||||||
|
"settings.mcp.banner_3": "),且每次读取都会记录在下方。",
|
||||||
|
"settings.mcp.enable": "启用 MCP 服务器",
|
||||||
|
"settings.mcp.enable_hint": "默认关闭。仅限环回、令牌保护——从网络无法访问任何内容。",
|
||||||
|
"settings.mcp.transport": "传输",
|
||||||
|
"settings.mcp.transport_http": "流式 HTTP",
|
||||||
|
"settings.mcp.transport_stdio": "stdio(代理生成一个进程)",
|
||||||
|
"settings.mcp.port": "端口",
|
||||||
|
"settings.mcp.transport_change_hint": "要更改传输/端口,请先关闭服务器,然后再重新打开。",
|
||||||
|
"settings.mcp.scope_title": "范围",
|
||||||
|
"settings.mcp.expose": "公开",
|
||||||
|
"settings.mcp.scope_none": "无——不共享任何内容",
|
||||||
|
"settings.mcp.scope_selected": "已选择——仅限你标记为共享的功能简报",
|
||||||
|
"settings.mcp.scope_all": "全部——会议、转录、行动项和共享简报",
|
||||||
|
"settings.mcp.expose_recordings": "也允许带有已保存录音的会议(默认关闭——在此项启用之前,即使在“全部”范围内,已录制会议的转录也会被扣留)",
|
||||||
|
"settings.mcp.token_title": "新的身份验证令牌——仅显示一次,请立即复制",
|
||||||
|
"settings.mcp.token_hint": "此内容不会再次显示。它存储在操作系统凭据存储中;如果丢失,请关闭并重新打开服务器以生成新的。",
|
||||||
|
"settings.mcp.copied": "已复制",
|
||||||
|
"settings.mcp.copy": "复制",
|
||||||
|
"settings.mcp.endpoint": "端点",
|
||||||
|
"settings.mcp.endpoint_hint": "将你代理的 MCP 客户端配置指向 {kind},以上方的令牌作为 bearer 凭据。",
|
||||||
|
"settings.mcp.kind_command": "命令",
|
||||||
|
"settings.mcp.kind_url": "URL",
|
||||||
|
"settings.mcp.access_log": "访问日志",
|
||||||
|
"settings.mcp.access_log_hint": "代理进行的每次工具读取,无论允许还是拒绝(FR-MCP-5)。",
|
||||||
|
"settings.mcp.no_reads": "尚无代理读取任何内容。",
|
||||||
|
"settings.mcp.log_meeting": "会议 {id}",
|
||||||
|
"settings.mcp.refresh": "刷新",
|
||||||
|
|
||||||
|
"settings.privacy.title": "隐私",
|
||||||
|
"settings.privacy.intro": "WhispAssist 现在实际获准从此设备发送出去的内容。当一切都关闭时(默认),没有任何内容离开设备。",
|
||||||
|
"settings.privacy.llm_endpoint": "LLM 端点",
|
||||||
|
"settings.privacy.off": "关闭",
|
||||||
|
"settings.privacy.local_only": "仅本地",
|
||||||
|
"settings.privacy.leaves_device": "离开此设备",
|
||||||
|
"settings.privacy.sync_label": "同步",
|
||||||
|
"settings.privacy.enabled": "已启用",
|
||||||
|
"settings.privacy.mcp_label": "MCP 服务器",
|
||||||
|
"settings.privacy.mcp_on": "开启 · {scope}",
|
||||||
|
"settings.privacy.mcp_loopback_note": "仅在环回上入站——不会向上方的出站列表添加任何内容。已连接的代理仍可能将其读取的内容转发到其自身的模型提供商;请参阅 MCP 服务器选项卡。",
|
||||||
|
"settings.privacy.egress_title": "出站允许列表",
|
||||||
|
"settings.privacy.no_egress": "没有主机被列入允许列表——WA 不进行任何内容出站。",
|
||||||
|
"settings.privacy.sync_targets_title": "同步目标",
|
||||||
|
"settings.privacy.third_party": "第三方",
|
||||||
|
"settings.privacy.your_server": "你的服务器",
|
||||||
|
"settings.privacy.tls": "TLS",
|
||||||
|
"settings.privacy.no_tls": "无 TLS",
|
||||||
|
"settings.privacy.refresh": "刷新",
|
||||||
|
"settings.privacy.unavailable": "隐私自检不可用。",
|
||||||
|
"settings.privacy.vault_title": "加密保险库",
|
||||||
|
"settings.privacy.vault_intro": "使用密码对静态的笔记、转录和摘要进行加密。(音频文件尚未加密。)",
|
||||||
|
"settings.privacy.vault_password": "保险库密码",
|
||||||
|
"settings.privacy.enable_vault": "启用保险库",
|
||||||
|
"settings.privacy.vault_pw_hint": "请至少使用 8 个字符。如果忘记,加密内容将无法恢复。",
|
||||||
|
"settings.privacy.vault_locked_1": "保险库已",
|
||||||
|
"settings.privacy.locked_word": "锁定",
|
||||||
|
"settings.privacy.vault_locked_2": "。解锁以读取加密的会议。",
|
||||||
|
"settings.privacy.password": "密码",
|
||||||
|
"settings.privacy.unlock": "解锁",
|
||||||
|
"settings.privacy.vault_unlocked_1": "保险库已",
|
||||||
|
"settings.privacy.unlocked_word": "解锁",
|
||||||
|
"settings.privacy.vault_unlocked_2": "。新的笔记、转录和摘要将在静态时加密。",
|
||||||
|
"settings.privacy.lock_now": "立即锁定",
|
||||||
|
"settings.privacy.change_password": "更改密码",
|
||||||
|
"settings.privacy.current_password": "当前密码",
|
||||||
|
"settings.privacy.new_password": "新密码",
|
||||||
|
|
||||||
|
"settings.about.title": "关于",
|
||||||
|
"settings.about.tagline": "一款完全本地、开源、Windows 原生的会议助手。",
|
||||||
|
"settings.about.build_commit": "构建提交",
|
||||||
|
|
||||||
|
"consent.heading": "录音之前",
|
||||||
|
"consent.body": "未经参与者同意录制对话在你所在地区可能是违法的。请查阅当地的录音法律。这是一项提醒,而非法律建议。",
|
||||||
|
"consent.accept": "我明白——启用录音",
|
||||||
|
"consent.cancel": "取消",
|
||||||
|
|
||||||
|
"hosted.heading": "使用 {provider} 之前",
|
||||||
|
"hosted.body_1": "使用",
|
||||||
|
"hosted.body_2": "进行生成会将本次会议的转录发送到他们的服务器——它会离开此设备,并受其隐私政策约束。一旦数据离开你的设备,WhispAssist 对其处理方式没有任何控制权。此项默认关闭;你正在现在选择启用它。",
|
||||||
|
"hosted.accept": "我明白——继续",
|
||||||
|
"hosted.cancel": "取消",
|
||||||
|
|
||||||
|
"theme.group": "主题",
|
||||||
|
"theme.system": "跟随系统",
|
||||||
|
"theme.light": "浅色",
|
||||||
|
"theme.dark": "深色",
|
||||||
|
|
||||||
|
"import.dialog_label": "从文件或 URL 添加会议",
|
||||||
|
"import.title": "添加会议",
|
||||||
|
"import.close": "关闭",
|
||||||
|
"import.close_title": "关闭(Esc)",
|
||||||
|
"import.body": "导入现有录音——本地音频/视频文件,或一个链接(YouTube、流媒体页面或直接媒体 URL)。它会像实时录音一样被转录和说话人分离。",
|
||||||
|
"import.file_or_url": "文件或 URL",
|
||||||
|
"import.source_placeholder": "粘贴 URL,或浏览文件…",
|
||||||
|
"import.choose_file": "选择本地文件",
|
||||||
|
"import.browse": "浏览…",
|
||||||
|
"import.title_label": "标题",
|
||||||
|
"import.optional": "可选",
|
||||||
|
"import.title_placeholder": "默认为文件名",
|
||||||
|
"import.filter_av": "音频 / 视频",
|
||||||
|
"import.requires_1": "需要",
|
||||||
|
"import.requires_2": "已安装并在你的 PATH 中(以及",
|
||||||
|
"import.requires_3": "用于 URL)。WhispAssist 不捆绑它们。",
|
||||||
|
"import.importing": "正在导入…这可能需要一段时间",
|
||||||
|
"import.import": "导入",
|
||||||
|
"import.cancel": "取消",
|
||||||
|
|
||||||
|
"tagchip.filter": "筛选带有标签“{tag}”的会议",
|
||||||
|
"tagchip.remove": "移除标签 {tag}",
|
||||||
|
|
||||||
|
"levelmeter.system_mic": "系统和麦克风输入电平",
|
||||||
|
"levelmeter.system": "系统输入电平",
|
||||||
|
|
||||||
|
"app.tagline": "本地 · 私密",
|
||||||
|
"app.note_template": "笔记模板",
|
||||||
|
"app.no_template": "无模板",
|
||||||
|
"app.add_meeting": "添加会议",
|
||||||
|
"app.add_meeting_title": "从文件或 URL 添加会议",
|
||||||
|
"app.record": "录音",
|
||||||
|
"app.record_title": "开始录音(Ctrl+Shift+R)",
|
||||||
|
"app.stop": "停止",
|
||||||
|
"app.stop_title": "停止录音(Ctrl+Shift+R)",
|
||||||
|
"app.cancel": "取消",
|
||||||
|
"app.cancel_title": "放弃此录音并删除它",
|
||||||
|
"app.recording": "正在录音…",
|
||||||
|
"app.backend_title": "活动转录后端",
|
||||||
|
"app.retention_title": "为此会议将音频保存为 .wav",
|
||||||
|
"app.saving": "正在保存",
|
||||||
|
"app.not_saved": "未保存",
|
||||||
|
"app.reconnecting": "正在重新连接音频设备…",
|
||||||
|
"app.settings": "设置",
|
||||||
|
"app.settings_title": "设置(Ctrl+,)",
|
||||||
|
"app.discard_confirm": "放弃此录音?它的音频和转录将被删除。",
|
||||||
|
"app.announce_started": "录音已开始",
|
||||||
|
"app.announce_paused": "录音已暂停",
|
||||||
|
"app.announce_stopped": "录音已停止",
|
||||||
|
"app.consent_dialog": "录音同意",
|
||||||
|
"app.vault_dialog": "解锁加密保险库",
|
||||||
|
"app.vault_title": "解锁加密保险库",
|
||||||
|
"app.vault_desc": "你的会议在静态时已加密。请输入密码以读取它们。",
|
||||||
|
"app.vault_password": "保险库密码",
|
||||||
|
"app.vault_incorrect": "密码不正确",
|
||||||
|
"app.unlock": "解锁",
|
||||||
|
"app.continue_locked": "保持锁定继续",
|
||||||
|
"app.show_meetings": "显示会议列表",
|
||||||
|
"app.hide_meetings": "隐藏会议列表",
|
||||||
|
"app.show_summary": "显示摘要面板",
|
||||||
|
"app.hide_summary": "隐藏摘要面板",
|
||||||
|
"app.resize_meetings": "调整会议列表大小",
|
||||||
|
"app.resize_summary": "调整摘要面板大小",
|
||||||
|
|
||||||
|
"meetings.search_placeholder": "搜索会议…",
|
||||||
|
"meetings.search_aria": "搜索会议",
|
||||||
|
"meetings.filter_tag_aria": "按标签筛选",
|
||||||
|
"meetings.all_tags": "所有标签",
|
||||||
|
"meetings.from_date_aria": "起始日期",
|
||||||
|
"meetings.to_date_aria": "结束日期",
|
||||||
|
"meetings.bulk_format_aria": "批量导出格式",
|
||||||
|
"meetings.bulk_export": "批量导出",
|
||||||
|
"meetings.bulk_export_title": "导出与上方标签/日期筛选条件匹配的每个会议",
|
||||||
|
"meetings.exporting": "正在导出…",
|
||||||
|
"meetings.exported_one": "已导出 {n} 个会议。",
|
||||||
|
"meetings.exported_many": "已导出 {n} 个会议。",
|
||||||
|
"meetings.searching": "正在搜索…",
|
||||||
|
"meetings.loading": "正在加载会议…",
|
||||||
|
"meetings.no_matches": "无匹配项。",
|
||||||
|
"meetings.empty": "尚无会议——点击上方的录音开始。",
|
||||||
|
"meetings.status.recording": "录音中",
|
||||||
|
"meetings.status.transcribing": "转录中",
|
||||||
|
"meetings.status.recovering": "恢复中",
|
||||||
|
"meetings.status.error": "错误",
|
||||||
|
"meetings.resume": "恢复转录",
|
||||||
|
"meetings.delete_aria": "删除会议",
|
||||||
|
"meetings.delete_confirm": "删除此会议?这将移除它的录音、转录和笔记。",
|
||||||
|
|
||||||
|
"transcript.heading": "转录",
|
||||||
|
"transcript.title_aria": "会议标题",
|
||||||
|
"transcript.lang_title": "转录语言",
|
||||||
|
"transcript.lang_auto": "正在自动检测…",
|
||||||
|
"transcript.show": "显示转录",
|
||||||
|
"transcript.hide": "隐藏转录",
|
||||||
|
"transcript.empty": "此会议没有转录。",
|
||||||
|
"transcript.will_appear": "转录将在你录音时显示在此处。",
|
||||||
|
"transcript.play_from_here": "从此处播放",
|
||||||
|
"transcript.resize": "调整转录和笔记大小",
|
||||||
|
"transcript.reprocess_placeholder": "重新转录,使用…",
|
||||||
|
"transcript.reprocess_lang_aria": "重新处理语言",
|
||||||
|
"transcript.reprocess_keep_lang": "保持当前语言",
|
||||||
|
"transcript.reprocess_go": "执行",
|
||||||
|
"transcript.retranscribing": "正在重新转录…",
|
||||||
|
"transcript.has_note": "有一条笔记",
|
||||||
|
"transcript.note_placeholder": "为这一刻添加笔记…",
|
||||||
|
"transcript.note_aria": "此转录行的笔记",
|
||||||
|
"transcript.filter_md": "Markdown",
|
||||||
|
"transcript.filter_pdf": "PDF",
|
||||||
|
"transcript.filter_docx": "Word 文档",
|
||||||
|
"transcript.filter_obsidian": "Obsidian 笔记",
|
||||||
|
|
||||||
|
"notes.heading": "笔记",
|
||||||
|
"notes.show": "显示笔记",
|
||||||
|
"notes.hide": "隐藏笔记",
|
||||||
|
"notes.placeholder": "笔记…",
|
||||||
|
"notes.live_placeholder": "边说边记笔记…",
|
||||||
|
"notes.toolbar_aria": "笔记格式",
|
||||||
|
"notes.bold": "粗体",
|
||||||
|
"notes.italic": "斜体",
|
||||||
|
"notes.h1": "标题 1",
|
||||||
|
"notes.h2": "标题 2",
|
||||||
|
"notes.bullet": "项目符号列表",
|
||||||
|
"notes.checkbox_title": "复选框",
|
||||||
|
"notes.checkbox_aria": "复选框列表项",
|
||||||
|
"notes.edit_raw": "编辑原始 markdown",
|
||||||
|
"notes.render": "渲染 markdown",
|
||||||
|
"notes.editor": "编辑器",
|
||||||
|
"notes.preview": "预览",
|
||||||
|
"notes.export_md_title": "将笔记导出为 .md",
|
||||||
|
"notes.export_pdf_title": "将笔记导出为 .pdf",
|
||||||
|
"notes.export_docx_title": "将笔记导出为 .docx",
|
||||||
|
"notes.export_bundle_title": "将音频 + 转录 + 笔记导出到文件夹",
|
||||||
|
"notes.export_obsidian_title": "导出一个 Obsidian 笔记(笔记 + 摘要 + 转录,不含音频)",
|
||||||
|
|
||||||
|
"summary.recording_heading": "录音",
|
||||||
|
"summary.loading_recording": "正在加载录音…",
|
||||||
|
"summary.sync_heading": "同步",
|
||||||
|
"summary.uploading": "正在上传…",
|
||||||
|
"summary.upload_now": "立即上传",
|
||||||
|
"summary.no_uploads": "此会议尚无上传。",
|
||||||
|
"summary.retry": "重试",
|
||||||
|
"summary.tags_heading": "标签",
|
||||||
|
"summary.tags_select": "选择一个会议来为其添加标签。",
|
||||||
|
"summary.tag_add_placeholder": "添加标签…",
|
||||||
|
"summary.tag_first_placeholder": "项目、客户、主题…",
|
||||||
|
"summary.generating": "正在生成…",
|
||||||
|
"summary.generate_tags": "生成标签",
|
||||||
|
"summary.saving": "正在保存…",
|
||||||
|
"summary.save_tags": "保存标签",
|
||||||
|
"summary.summary_heading": "摘要",
|
||||||
|
"summary.ai_provider": "AI 提供商",
|
||||||
|
"summary.provider_off": "关闭",
|
||||||
|
"summary.provider_custom": "自定义",
|
||||||
|
"summary.local": "本地",
|
||||||
|
"summary.leaves_device": "离开此设备",
|
||||||
|
"summary.this_hosted_provider": "此托管提供商",
|
||||||
|
"summary.select_generate": "选择一个会议来生成摘要。",
|
||||||
|
"summary.decisions_heading": "决策",
|
||||||
|
"summary.regenerate": "重新生成",
|
||||||
|
"summary.generated_hint": "会议结束后在本地生成(需要本地 LLM 提供商)。",
|
||||||
|
"summary.no_provider": "未配置 AI 提供商——请在设置中启用一个。",
|
||||||
|
"summary.generate_summary": "生成摘要",
|
||||||
|
"summary.briefs_heading": "功能简报",
|
||||||
|
"summary.briefs_select": "选择一个会议来创建或查看功能简报。",
|
||||||
|
"summary.briefs_desc": "将本次会议提炼为代理可用的规范(需要本地 LLM 提供商)——将其交给编码代理,或在 MCP 服务器开启后通过它提供。",
|
||||||
|
"summary.brief_repo_placeholder": "目标仓库(可选),例如 acme/reporting-web",
|
||||||
|
"summary.distilling": "正在提炼…",
|
||||||
|
"summary.create_brief": "创建功能简报",
|
||||||
|
"summary.brief_mcp_title": "MCP 服务器开启时可用",
|
||||||
|
"summary.copied": "已复制",
|
||||||
|
"summary.copy_md": "复制为 Markdown",
|
||||||
|
"summary.copy_json": "复制为 JSON",
|
||||||
|
"summary.brief_problem": "问题",
|
||||||
|
"summary.brief_outcome": "期望结果",
|
||||||
|
"summary.brief_criteria": "验收标准",
|
||||||
|
"summary.brief_none": "未捕获任何内容。",
|
||||||
|
"summary.brief_context": "上下文",
|
||||||
|
"summary.action_items_heading": "行动项",
|
||||||
|
"summary.ai_select": "选择一个会议来查看其行动项。",
|
||||||
|
"summary.ai_empty": "尚无——在下方添加一项,或生成摘要以自动提取它们。",
|
||||||
|
"summary.confirmed": "已确认",
|
||||||
|
"summary.ai_text_placeholder": "行动项…",
|
||||||
|
"summary.ai_text_aria": "行动项文本",
|
||||||
|
"summary.owner": "负责人",
|
||||||
|
"summary.due_date": "截止日期",
|
||||||
|
"summary.reminder_title": "为此截止日期安排本地提醒",
|
||||||
|
"summary.ai_delete_title": "删除此行动项",
|
||||||
|
"summary.ai_delete_aria": "删除行动项",
|
||||||
|
"summary.add_action_item": "添加行动项",
|
||||||
|
"summary.save_action_items": "保存行动项",
|
||||||
|
"summary.calendar_heading": "日历事件",
|
||||||
|
"summary.cal_select": "选择一个会议以将其关联到日历事件。",
|
||||||
|
"summary.event_search_placeholder": "搜索事件标题…",
|
||||||
|
"summary.linked_event": "已关联的事件",
|
||||||
|
"summary.change_event": "更改事件…",
|
||||||
|
"summary.link_event": "关联一个事件…",
|
||||||
|
"summary.untitled_event": "(无标题)",
|
||||||
|
"summary.no_events_1": "尚未导入任何事件——请",
|
||||||
|
"summary.no_events_2": "从设置 → 日历导入。",
|
||||||
|
"summary.no_events_match": "没有事件与此搜索/日期匹配——请尝试清除其中一个。",
|
||||||
|
"summary.organizer_label": "组织者:{name}",
|
||||||
|
"summary.participants_heading": "参与者",
|
||||||
|
"summary.participants_hint": "根据已关联的日历事件填充。",
|
||||||
|
"summary.speakers_heading": "说话人",
|
||||||
|
"summary.speakers_select": "选择一个会议来为其说话人命名。",
|
||||||
|
"summary.no_speakers": "尚未检测到说话人。",
|
||||||
|
"summary.speaker_name_placeholder": "说话人姓名",
|
||||||
|
"summary.save": "保存",
|
||||||
|
"summary.cancel": "取消",
|
||||||
|
"summary.name_speaker": "为此说话人命名…",
|
||||||
|
"summary.add_new_name": "+ 添加新姓名…",
|
||||||
|
|
||||||
|
"settings.recording.title": "录音",
|
||||||
|
"settings.recording.record_default": "默认录制会议",
|
||||||
|
"settings.recording.save_audio_as": "将音频保存为",
|
||||||
|
"settings.recording.default_hint": "默认关闭。关闭时,音频仅用于生成转录,并在会议最终确定时删除。你也可以按会议切换录音。",
|
||||||
|
"settings.recording.consent_label": "同意:{status}。",
|
||||||
|
"settings.recording.consent_ack": "已确认",
|
||||||
|
"settings.recording.consent_not": "尚未确认",
|
||||||
|
"settings.recording.auto_label": "当日历事件开始时自动开始录音",
|
||||||
|
"settings.recording.auto_hint": "仅在 WhispAssist 打开时。当导入的日历事件的开始时间到达时,会自动开始录音(使用上方的默认保留设置)。没有任何内容在后台运行——计时器仅在应用运行时启用。请在设置 → 日历下导入事件。"
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// Live per-meeting progress of background media imports (feeds ImportTracker).
|
||||||
|
// Fed entirely by `import://progress` events emitted by `import_media`; kept in
|
||||||
|
// memory only (the meeting's `status` badge is the persistent story after a
|
||||||
|
// restart). See commands.rs `run_import_pipeline`.
|
||||||
|
|
||||||
|
import { events, type ImportPhase, type ImportProgress, type MeetingId } from "../api";
|
||||||
|
|
||||||
|
export type PhaseState = "pending" | "active" | "done" | "error";
|
||||||
|
|
||||||
|
// The four phases in the order the backend runs (and the tracker renders) them.
|
||||||
|
export const IMPORT_PHASES: ImportPhase[] = ["prepare", "transcribe", "diarize", "finalize"];
|
||||||
|
|
||||||
|
export interface PhaseInfo {
|
||||||
|
phase: ImportPhase;
|
||||||
|
state: PhaseState;
|
||||||
|
elapsedMs: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportRun {
|
||||||
|
meetingId: MeetingId;
|
||||||
|
phases: PhaseInfo[];
|
||||||
|
error: string | null;
|
||||||
|
done: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function freshRun(meetingId: MeetingId): ImportRun {
|
||||||
|
return {
|
||||||
|
meetingId,
|
||||||
|
phases: IMPORT_PHASES.map((phase) => ({ phase, state: "pending", elapsedMs: null })),
|
||||||
|
error: null,
|
||||||
|
done: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImportsStore {
|
||||||
|
runs = $state<Record<MeetingId, ImportRun>>({});
|
||||||
|
|
||||||
|
get(meetingId: MeetingId): ImportRun | undefined {
|
||||||
|
return this.runs[meetingId];
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await events.onImportProgress((p) => this.apply(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
private apply(p: ImportProgress) {
|
||||||
|
// Re-read through the record after inserting so we mutate the $state proxy,
|
||||||
|
// not the raw object (Svelte 5 deep reactivity only tracks the proxy).
|
||||||
|
if (!this.runs[p.meetingId]) this.runs[p.meetingId] = freshRun(p.meetingId);
|
||||||
|
const run = this.runs[p.meetingId];
|
||||||
|
const info = run.phases.find((x) => x.phase === p.phase);
|
||||||
|
if (!info) return;
|
||||||
|
info.state = p.state;
|
||||||
|
if (p.state === "done") info.elapsedMs = p.elapsedMs;
|
||||||
|
if (p.state === "error") {
|
||||||
|
run.error = p.error;
|
||||||
|
run.done = true;
|
||||||
|
}
|
||||||
|
if (p.phase === "finalize" && p.state === "done") run.done = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const imports = new ImportsStore();
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Shared recording-playback state so the transcript (center pane,
|
||||||
|
// TranscriptNotes) and the <audio> element (side pane, SummaryPanel) can talk
|
||||||
|
// to each other across the App layout: click a transcript segment to seek the
|
||||||
|
// player, and highlight/auto-scroll the segment that's currently playing.
|
||||||
|
// Pure UI ephemera — no persistence, reset per meeting.
|
||||||
|
|
||||||
|
class PlayerStore {
|
||||||
|
/** Playback position in ms, pushed from the <audio> element's timeupdate. */
|
||||||
|
currentMs = $state(0);
|
||||||
|
/** Target of the latest seek request, in ms. */
|
||||||
|
seekMs = $state<number | null>(null);
|
||||||
|
/** Bumped on every seek() so clicking the *same* segment twice re-seeks
|
||||||
|
* (a plain seekMs assignment wouldn't fire when the value is unchanged). */
|
||||||
|
seekNonce = $state(0);
|
||||||
|
|
||||||
|
/** Ask the player to jump to `ms` and play (SummaryPanel watches seekNonce). */
|
||||||
|
seek(ms: number) {
|
||||||
|
this.seekMs = ms;
|
||||||
|
this.seekNonce++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear when the selected meeting changes so a new recording starts at 0. */
|
||||||
|
reset() {
|
||||||
|
this.currentMs = 0;
|
||||||
|
this.seekMs = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const player = new PlayerStore();
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Recording state store (Svelte 5 runes-friendly via a small class).
|
// Recording state store (Svelte 5 runes-friendly via a small class).
|
||||||
// Subscribes to recording/transcript events and exposes reactive state.
|
// Subscribes to recording/transcript events and exposes reactive state.
|
||||||
|
|
||||||
import { api, events, type TranscriptSegment, type MeetingId } from "../api";
|
import { api, events, type TranscriptSegment, type SpeakerInfo, type MeetingId } from "../api";
|
||||||
import { settings } from "./settings.svelte";
|
import { settings } from "./settings.svelte";
|
||||||
import { SvelteMap } from "svelte/reactivity";
|
import { SvelteMap } from "svelte/reactivity";
|
||||||
|
|
||||||
@@ -10,6 +10,9 @@ class RecordingStore {
|
|||||||
state = $state<"idle" | "recording" | "paused">("idle");
|
state = $state<"idle" | "recording" | "paused">("idle");
|
||||||
elapsedMs = $state(0);
|
elapsedMs = $state(0);
|
||||||
segments = $state<TranscriptSegment[]>([]);
|
segments = $state<TranscriptSegment[]>([]);
|
||||||
|
/** Live speaker list from provisional diarization (FR-SPK): resolves labels
|
||||||
|
* to display names ("You" for the mic speaker) for the live transcript. */
|
||||||
|
speakers = $state<SpeakerInfo[]>([]);
|
||||||
/** Whether audio is being retained as .wav for the in-flight meeting (ADR-0009). */
|
/** Whether audio is being retained as .wav for the in-flight meeting (ADR-0009). */
|
||||||
retention = $state(false);
|
retention = $state(false);
|
||||||
/** Live system/loopback level for the waveform/meter (FR-CAP-5); 0 when not recording. */
|
/** Live system/loopback level for the waveform/meter (FR-CAP-5); 0 when not recording. */
|
||||||
@@ -19,6 +22,9 @@ class RecordingStore {
|
|||||||
* (FR-CAP-7); stays 0 when the mic is disabled or not recording. */
|
* (FR-CAP-7); stays 0 when the mic is disabled or not recording. */
|
||||||
levelRmsMic = $state(0);
|
levelRmsMic = $state(0);
|
||||||
levelPeakMic = $state(0);
|
levelPeakMic = $state(0);
|
||||||
|
/** Mic muted for the in-flight meeting (FR-CAP-7): mic channel goes silent
|
||||||
|
* while loopback keeps recording. Toggled by the "M" key / mute button. */
|
||||||
|
micMuted = $state(false);
|
||||||
/** Set while a capture-device reconnect is in progress; cleared on recovery (FR-CAP-6). */
|
/** Set while a capture-device reconnect is in progress; cleared on recovery (FR-CAP-6). */
|
||||||
deviceNotice = $state<string | null>(null);
|
deviceNotice = $state<string | null>(null);
|
||||||
/** Live notes redesign: freeform text typed in the Notes pane while recording. */
|
/** Live notes redesign: freeform text typed in the Notes pane while recording. */
|
||||||
@@ -47,11 +53,16 @@ class RecordingStore {
|
|||||||
this.retention = p.record;
|
this.retention = p.record;
|
||||||
});
|
});
|
||||||
await events.onSegment(({ segment }) => {
|
await events.onSegment(({ segment }) => {
|
||||||
// Replace an interim segment with the same id, else append.
|
// Replace an interim segment with the same id, else append. Live
|
||||||
|
// diarization also re-emits committed segments here with a refined
|
||||||
|
// speaker (FR-SPK) — same replace-by-id path applies.
|
||||||
const i = this.segments.findIndex((s) => s.id === segment.id);
|
const i = this.segments.findIndex((s) => s.id === segment.id);
|
||||||
if (i >= 0) this.segments[i] = segment;
|
if (i >= 0) this.segments[i] = segment;
|
||||||
else this.segments.push(segment);
|
else this.segments.push(segment);
|
||||||
});
|
});
|
||||||
|
await events.onDiarizationUpdated(({ speakers }) => {
|
||||||
|
this.speakers = speakers;
|
||||||
|
});
|
||||||
await events.onLevel(({ rms, peak, mic }) => {
|
await events.onLevel(({ rms, peak, mic }) => {
|
||||||
if (mic) {
|
if (mic) {
|
||||||
this.levelRmsMic = rms;
|
this.levelRmsMic = rms;
|
||||||
@@ -64,12 +75,30 @@ class RecordingStore {
|
|||||||
await events.onDeviceChanged(({ recovered, message }) => {
|
await events.onDeviceChanged(({ recovered, message }) => {
|
||||||
this.deviceNotice = recovered ? null : message;
|
this.deviceNotice = recovered ? null : message;
|
||||||
});
|
});
|
||||||
|
// Keep mute state in sync even if it was toggled elsewhere (e.g. a future
|
||||||
|
// tray control), not just from this store's toggleMute().
|
||||||
|
await events.onMicMuted(({ muted }) => {
|
||||||
|
this.micMuted = muted;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toggle mic mute for the active recording (FR-CAP-7); no-op if not
|
||||||
|
* recording. Optimistically flips, then reconciles with the backend result. */
|
||||||
|
async toggleMute() {
|
||||||
|
if (!this.meetingId || this.state === "idle") return;
|
||||||
|
try {
|
||||||
|
this.micMuted = await api.toggleMicrophoneMute(this.meetingId);
|
||||||
|
} catch {
|
||||||
|
// Mic off for this meeting (or capture gone) — nothing to mute.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(title?: string, record = false, templateId?: string, calendarEventId?: string) {
|
async start(title?: string, record = false, templateId?: string, calendarEventId?: string) {
|
||||||
this.segments = [];
|
this.segments = [];
|
||||||
|
this.speakers = [];
|
||||||
this.retention = record;
|
this.retention = record;
|
||||||
this.deviceNotice = null;
|
this.deviceNotice = null;
|
||||||
|
this.micMuted = false;
|
||||||
this.notesText = "";
|
this.notesText = "";
|
||||||
this.segmentNotes.clear();
|
this.segmentNotes.clear();
|
||||||
// T8.7/FR-TRX-4: whatever language is currently configured in Settings
|
// T8.7/FR-TRX-4: whatever language is currently configured in Settings
|
||||||
@@ -89,6 +118,7 @@ class RecordingStore {
|
|||||||
this.levelPeak = 0;
|
this.levelPeak = 0;
|
||||||
this.levelRmsMic = 0;
|
this.levelRmsMic = 0;
|
||||||
this.levelPeakMic = 0;
|
this.levelPeakMic = 0;
|
||||||
|
this.micMuted = false;
|
||||||
this.deviceNotice = null;
|
this.deviceNotice = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,11 +131,13 @@ class RecordingStore {
|
|||||||
if (this.meetingId) await api.cancelRecording(this.meetingId);
|
if (this.meetingId) await api.cancelRecording(this.meetingId);
|
||||||
this.meetingId = null;
|
this.meetingId = null;
|
||||||
this.segments = [];
|
this.segments = [];
|
||||||
|
this.speakers = [];
|
||||||
this.state = "idle";
|
this.state = "idle";
|
||||||
this.levelRms = 0;
|
this.levelRms = 0;
|
||||||
this.levelPeak = 0;
|
this.levelPeak = 0;
|
||||||
this.levelRmsMic = 0;
|
this.levelRmsMic = 0;
|
||||||
this.levelPeakMic = 0;
|
this.levelPeakMic = 0;
|
||||||
|
this.micMuted = false;
|
||||||
this.deviceNotice = null;
|
this.deviceNotice = null;
|
||||||
this.notesText = "";
|
this.notesText = "";
|
||||||
this.segmentNotes.clear();
|
this.segmentNotes.clear();
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
audio_output_device: null, // system default render device (FR-CAP-1)
|
audio_output_device: null, // system default render device (FR-CAP-1)
|
||||||
microphone_enabled: true, // capture the user's mic into the transcript (FR-CAP-7)
|
microphone_enabled: true, // capture the user's mic into the transcript (FR-CAP-7)
|
||||||
audio_input_device: null, // system default capture device
|
audio_input_device: null, // system default capture device
|
||||||
|
auto_start: false, // launch at login — opt-in, off by default (NFR-RES-4)
|
||||||
|
close_to_tray: true, // closing the window hides to tray; on by default
|
||||||
};
|
};
|
||||||
|
|
||||||
class SettingsStore {
|
class SettingsStore {
|
||||||
@@ -383,6 +385,18 @@ class SettingsStore {
|
|||||||
return this.patch({ default_record: on });
|
return this.patch({ default_record: on });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Launch-at-login toggle (NFR-RES-4). Goes through its own command (not
|
||||||
|
* patch) since the backend also writes the per-user OS Run entry; that
|
||||||
|
* command persists auto_start itself, so we just mirror it locally. */
|
||||||
|
async setAutoStart(on: boolean) {
|
||||||
|
this.settings = { ...this.settings, auto_start: on };
|
||||||
|
try {
|
||||||
|
await api.setAutoStart(on);
|
||||||
|
} catch {
|
||||||
|
this.backendStub = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setRetentionPolicy(maxAgeDays: number | null, maxSizeGb: number | null) {
|
setRetentionPolicy(maxAgeDays: number | null, maxSizeGb: number | null) {
|
||||||
return this.patch({ retention_max_age_days: maxAgeDays, retention_max_size_gb: maxSizeGb });
|
return this.patch({ retention_max_age_days: maxAgeDays, retention_max_size_gb: maxSizeGb });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,12 @@
|
|||||||
import { api, type MeetingListItem, type SearchHit } from "../api";
|
import { api, type MeetingListItem, type SearchHit } from "../api";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { Search, Trash2, Download, RotateCcw } from "@lucide/svelte";
|
import { Search, Trash2, Download, RotateCcw } from "@lucide/svelte";
|
||||||
|
import { t } from "../i18n/index.svelte";
|
||||||
|
|
||||||
|
// Meeting status ("recording"/"transcribing"/"recovering"/"error") → its
|
||||||
|
// localized badge label. The status value itself is app vocabulary, not user
|
||||||
|
// content, so it gets keyed.
|
||||||
|
const statusLabel = (s: string) => t(`meetings.status.${s}`);
|
||||||
|
|
||||||
let query = $state("");
|
let query = $state("");
|
||||||
let searchTimer: ReturnType<typeof setTimeout> | undefined;
|
let searchTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
@@ -55,7 +61,10 @@
|
|||||||
from: toUnix(fromFilter),
|
from: toUnix(fromFilter),
|
||||||
to: toUnix(toFilter),
|
to: toUnix(toFilter),
|
||||||
});
|
});
|
||||||
bulkResult = `Exported ${count} meeting${count === 1 ? "" : "s"}.`;
|
bulkResult =
|
||||||
|
count === 1
|
||||||
|
? t("meetings.exported_one", { n: count })
|
||||||
|
: t("meetings.exported_many", { n: count });
|
||||||
} finally {
|
} finally {
|
||||||
bulkExporting = false;
|
bulkExporting = false;
|
||||||
}
|
}
|
||||||
@@ -93,7 +102,7 @@
|
|||||||
|
|
||||||
async function removeMeeting(e: Event, id: string) {
|
async function removeMeeting(e: Event, id: string) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!confirm("Delete this meeting? This removes its recording, transcript, and notes.")) return;
|
if (!confirm(t("meetings.delete_confirm"))) return;
|
||||||
await meetings.remove(id);
|
await meetings.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,24 +116,38 @@
|
|||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<Search size={15} aria-hidden="true" />
|
<Search size={15} aria-hidden="true" />
|
||||||
<input
|
<input
|
||||||
placeholder="Search meetings…"
|
placeholder={t("meetings.search_placeholder")}
|
||||||
aria-label="Search meetings"
|
aria-label={t("meetings.search_aria")}
|
||||||
bind:value={query}
|
bind:value={query}
|
||||||
oninput={onSearchInput}
|
oninput={onSearchInput}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<select aria-label="Filter by tag" bind:value={tagFilter} onchange={onFilterChange}>
|
<select
|
||||||
<option value="">All tags</option>
|
aria-label={t("meetings.filter_tag_aria")}
|
||||||
{#each meetings.allTags as t (t)}
|
bind:value={tagFilter}
|
||||||
<option value={t}>{t}</option>
|
onchange={onFilterChange}
|
||||||
|
>
|
||||||
|
<option value="">{t("meetings.all_tags")}</option>
|
||||||
|
{#each meetings.allTags as tag (tag)}
|
||||||
|
<option value={tag}>{tag}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<input type="date" aria-label="From date" bind:value={fromFilter} onchange={onFilterChange} />
|
<input
|
||||||
<input type="date" aria-label="To date" bind:value={toFilter} onchange={onFilterChange} />
|
type="date"
|
||||||
|
aria-label={t("meetings.from_date_aria")}
|
||||||
|
bind:value={fromFilter}
|
||||||
|
onchange={onFilterChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
aria-label={t("meetings.to_date_aria")}
|
||||||
|
bind:value={toFilter}
|
||||||
|
onchange={onFilterChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<select aria-label="Bulk export format" bind:value={bulkFormat}>
|
<select aria-label={t("meetings.bulk_format_aria")} bind:value={bulkFormat}>
|
||||||
<option value="md">.md</option>
|
<option value="md">.md</option>
|
||||||
<option value="pdf">.pdf</option>
|
<option value="pdf">.pdf</option>
|
||||||
<option value="docx">.docx</option>
|
<option value="docx">.docx</option>
|
||||||
@@ -134,23 +157,23 @@
|
|||||||
class="bulk-btn"
|
class="bulk-btn"
|
||||||
onclick={bulkExport}
|
onclick={bulkExport}
|
||||||
disabled={bulkExporting}
|
disabled={bulkExporting}
|
||||||
title="Export every meeting matching the tag/date filters above"
|
title={t("meetings.bulk_export_title")}
|
||||||
>
|
>
|
||||||
<Download size={13} aria-hidden="true" />
|
<Download size={13} aria-hidden="true" />
|
||||||
{bulkExporting ? "Exporting…" : "Bulk export"}
|
{bulkExporting ? t("meetings.exporting") : t("meetings.bulk_export")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if bulkResult}
|
{#if bulkResult}
|
||||||
<p class="muted small">{bulkResult}</p>
|
<p class="muted small">{bulkResult}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if meetings.searching}
|
{#if meetings.searching}
|
||||||
<p class="muted">Searching…</p>
|
<p class="muted">{t("meetings.searching")}</p>
|
||||||
{:else if displayItems.length === 0 && meetings.loading}
|
{:else if displayItems.length === 0 && meetings.loading}
|
||||||
<p class="muted">Loading meetings…</p>
|
<p class="muted">{t("meetings.loading")}</p>
|
||||||
{:else if displayItems.length === 0 && meetings.searchResults !== null}
|
{:else if displayItems.length === 0 && meetings.searchResults !== null}
|
||||||
<p class="muted">No matches.</p>
|
<p class="muted">{t("meetings.no_matches")}</p>
|
||||||
{:else if displayItems.length === 0}
|
{:else if displayItems.length === 0}
|
||||||
<p class="muted">No meetings yet — click Record above to start.</p>
|
<p class="muted">{t("meetings.empty")}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<ul>
|
<ul>
|
||||||
{#each displayItems as m (m.id)}
|
{#each displayItems as m (m.id)}
|
||||||
@@ -159,11 +182,11 @@
|
|||||||
<span class="row">
|
<span class="row">
|
||||||
<span class="title">{m.title}</span>
|
<span class="title">{m.title}</span>
|
||||||
{#if m.status === "recording" || m.status === "transcribing"}
|
{#if m.status === "recording" || m.status === "transcribing"}
|
||||||
<span class="badge live">{m.status}</span>
|
<span class="badge live">{statusLabel(m.status)}</span>
|
||||||
{:else if m.status === "recovering"}
|
{:else if m.status === "recovering"}
|
||||||
<span class="badge recovering">recovering</span>
|
<span class="badge recovering">{statusLabel("recovering")}</span>
|
||||||
{:else if m.status === "error"}
|
{:else if m.status === "error"}
|
||||||
<span class="badge error">error</span>
|
<span class="badge error">{statusLabel("error")}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<span class="row muted small">
|
<span class="row muted small">
|
||||||
@@ -175,8 +198,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if m.tags.length > 0}
|
{#if m.tags.length > 0}
|
||||||
<span class="row tags">
|
<span class="row tags">
|
||||||
{#each m.tags as t (t)}
|
{#each m.tags as tag (tag)}
|
||||||
<span class="chip">{t}</span>
|
<span class="chip">{tag}</span>
|
||||||
{/each}
|
{/each}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -185,14 +208,14 @@
|
|||||||
{#if m.status === "recovering"}
|
{#if m.status === "recovering"}
|
||||||
<button class="link" onclick={(e) => resumeMeeting(e, m.id)}>
|
<button class="link" onclick={(e) => resumeMeeting(e, m.id)}>
|
||||||
<RotateCcw size={12} aria-hidden="true" />
|
<RotateCcw size={12} aria-hidden="true" />
|
||||||
Resume transcription
|
{t("meetings.resume")}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
class="link danger"
|
class="link danger"
|
||||||
onclick={(e) => removeMeeting(e, m.id)}
|
onclick={(e) => removeMeeting(e, m.id)}
|
||||||
aria-label="Delete meeting"
|
aria-label={t("meetings.delete_aria")}
|
||||||
title="Delete meeting"
|
title={t("meetings.delete_aria")}
|
||||||
>
|
>
|
||||||
<Trash2 size={14} aria-hidden="true" />
|
<Trash2 size={14} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+934
-387
File diff suppressed because it is too large
Load Diff
+259
-104
@@ -4,6 +4,7 @@
|
|||||||
import { meetings } from "../stores/meetings.svelte";
|
import { meetings } from "../stores/meetings.svelte";
|
||||||
import { calendar } from "../stores/calendar.svelte";
|
import { calendar } from "../stores/calendar.svelte";
|
||||||
import { settings } from "../stores/settings.svelte";
|
import { settings } from "../stores/settings.svelte";
|
||||||
|
import { player } from "../stores/player.svelte";
|
||||||
import {
|
import {
|
||||||
api,
|
api,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
type LlmStatus,
|
type LlmStatus,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
import { renderMarkdown } from "../markdown";
|
import { renderMarkdown } from "../markdown";
|
||||||
|
import { t } from "../i18n/index.svelte";
|
||||||
import TagChip from "../components/TagChip.svelte";
|
import TagChip from "../components/TagChip.svelte";
|
||||||
import HostedAiBanner from "../components/HostedAiBanner.svelte";
|
import HostedAiBanner from "../components/HostedAiBanner.svelte";
|
||||||
import {
|
import {
|
||||||
@@ -30,6 +32,8 @@
|
|||||||
Check,
|
Check,
|
||||||
Cpu,
|
Cpu,
|
||||||
Globe,
|
Globe,
|
||||||
|
Plus,
|
||||||
|
X,
|
||||||
} from "@lucide/svelte";
|
} from "@lucide/svelte";
|
||||||
|
|
||||||
onMount(() => calendar.load());
|
onMount(() => calendar.load());
|
||||||
@@ -37,10 +41,12 @@
|
|||||||
// ---- Recording playback (FR-REC-5) ----
|
// ---- Recording playback (FR-REC-5) ----
|
||||||
let audioSrc = $state<string | null>(null);
|
let audioSrc = $state<string | null>(null);
|
||||||
let audioError = $state<string | null>(null);
|
let audioError = $state<string | null>(null);
|
||||||
|
let audioEl = $state<HTMLAudioElement | null>(null);
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const m = meetings.selected;
|
const m = meetings.selected;
|
||||||
audioSrc = null;
|
audioSrc = null;
|
||||||
audioError = null;
|
audioError = null;
|
||||||
|
player.reset();
|
||||||
if (m?.recorded) {
|
if (m?.recorded) {
|
||||||
const id = m.id;
|
const id = m.id;
|
||||||
api
|
api
|
||||||
@@ -54,6 +60,17 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Seek + play when a transcript segment is clicked (player.seek bumps the
|
||||||
|
// nonce so re-clicking the same segment still jumps). Reading seekNonce is
|
||||||
|
// what makes this effect re-run.
|
||||||
|
$effect(() => {
|
||||||
|
void player.seekNonce;
|
||||||
|
const ms = player.seekMs;
|
||||||
|
if (ms == null || !audioEl) return;
|
||||||
|
audioEl.currentTime = ms / 1000;
|
||||||
|
void audioEl.play().catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
// ---- Summary + action items (T5.4/T5.5/T5.6, FR-LLM-2/3/4) ----
|
// ---- Summary + action items (T5.4/T5.5/T5.6, FR-LLM-2/3/4) ----
|
||||||
let llmStatus = $state<LlmStatus | null>(null);
|
let llmStatus = $state<LlmStatus | null>(null);
|
||||||
async function refreshLlmStatus() {
|
async function refreshLlmStatus() {
|
||||||
@@ -75,12 +92,24 @@
|
|||||||
// the exact same set_llm_provider command (and hence the same "anthropic's
|
// the exact same set_llm_provider command (and hence the same "anthropic's
|
||||||
// endpoint is fixed" / "switching away resets a stale hosted endpoint"
|
// endpoint is fixed" / "switching away resets a stale hosted endpoint"
|
||||||
// guarantees — see apply_llm_provider_args in commands.rs).
|
// guarantees — see apply_llm_provider_args in commands.rs).
|
||||||
const PROVIDER_LABELS: Record<string, string> = {
|
// Provider dropdown values. Labels resolve through `providerLabel` (a
|
||||||
off: "Off",
|
// function, not a static map, so a language switch re-renders them);
|
||||||
ollama: "Ollama",
|
// brand names stay literal, only the generic "Off"/"Custom" are translated.
|
||||||
custom: "Custom",
|
const PROVIDER_IDS = ["off", "ollama", "custom", "anthropic"];
|
||||||
anthropic: "Anthropic (Claude)",
|
function providerLabel(id: string): string {
|
||||||
};
|
switch (id) {
|
||||||
|
case "off":
|
||||||
|
return t("summary.provider_off");
|
||||||
|
case "custom":
|
||||||
|
return t("summary.provider_custom");
|
||||||
|
case "ollama":
|
||||||
|
return "Ollama";
|
||||||
|
case "anthropic":
|
||||||
|
return "Anthropic (Claude)";
|
||||||
|
default:
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
function isHostedStatus(status: LlmStatus | null): boolean {
|
function isHostedStatus(status: LlmStatus | null): boolean {
|
||||||
return !!status && status.provider !== "off" && !status.isLocal;
|
return !!status && status.provider !== "off" && !status.isLocal;
|
||||||
}
|
}
|
||||||
@@ -92,7 +121,7 @@
|
|||||||
// gate that matters even if the quick switch's own check is skipped, e.g.
|
// gate that matters even if the quick switch's own check is skipped, e.g.
|
||||||
// "custom" resolving to a hosted endpoint that was configured elsewhere).
|
// "custom" resolving to a hosted endpoint that was configured elsewhere).
|
||||||
let showHostedBanner = $state(false);
|
let showHostedBanner = $state(false);
|
||||||
let hostedBannerLabel = $state("this hosted provider");
|
let hostedBannerLabel = $state(t("summary.this_hosted_provider"));
|
||||||
let pendingHostedAction = $state<(() => void) | null>(null);
|
let pendingHostedAction = $state<(() => void) | null>(null);
|
||||||
function requireHostedAck(providerLabel: string, action: () => void) {
|
function requireHostedAck(providerLabel: string, action: () => void) {
|
||||||
if (!settings.settings.hosted_ai_acknowledged) {
|
if (!settings.settings.hosted_ai_acknowledged) {
|
||||||
@@ -129,13 +158,13 @@
|
|||||||
if (next === "anthropic") {
|
if (next === "anthropic") {
|
||||||
// Deterministically hosted — worth confirming before even switching to
|
// Deterministically hosted — worth confirming before even switching to
|
||||||
// it, not just before the next generate.
|
// it, not just before the next generate.
|
||||||
requireHostedAck(PROVIDER_LABELS[next], () => void switchProvider(next));
|
requireHostedAck(providerLabel(next), () => void switchProvider(next));
|
||||||
} else {
|
} else {
|
||||||
void switchProvider(next);
|
void switchProvider(next);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onGenerateClick() {
|
function onGenerateClick() {
|
||||||
const label = llmStatus ? (PROVIDER_LABELS[llmStatus.provider] ?? llmStatus.provider) : "";
|
const label = llmStatus ? providerLabel(llmStatus.provider) : "";
|
||||||
if (isHostedStatus(llmStatus)) {
|
if (isHostedStatus(llmStatus)) {
|
||||||
requireHostedAck(label, generateSummary);
|
requireHostedAck(label, generateSummary);
|
||||||
} else {
|
} else {
|
||||||
@@ -143,20 +172,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Editable copy so toggling a checkbox doesn't persist until "Save" is
|
// Editable working copy so add/delete/edit don't persist until "Save" is
|
||||||
// pressed; recomputes whenever a different meeting (or a freshly generated
|
// pressed. Resynced from the meeting's table-backed action_items whenever a
|
||||||
// summary) comes in. bind:checked mutates each item in place, which a plain
|
// *different* meeting is selected (same guard pattern as pendingTags), so
|
||||||
// $derived array tolerates fine — only the array identity is recomputed.
|
// it isn't clobbered mid-edit.
|
||||||
let editableItems = $derived(
|
let editableItems = $state<ActionItem[]>([]);
|
||||||
(meetings.selected?.summary?.action_items ?? []).map((i) => ({ ...i }) as ActionItem),
|
let loadedItemsForId: string | null = null;
|
||||||
);
|
$effect(() => {
|
||||||
|
const m = meetings.selected;
|
||||||
|
if (m && m.id !== loadedItemsForId) {
|
||||||
|
editableItems = m.action_items.map((i) => ({ ...i }));
|
||||||
|
loadedItemsForId = m.id;
|
||||||
|
} else if (!m) {
|
||||||
|
loadedItemsForId = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function addActionItem() {
|
||||||
|
editableItems = [
|
||||||
|
...editableItems,
|
||||||
|
{ id: null, text: "", owner: null, due_at: null, confirmed: false, reminder_set: false },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function removeActionItem(i: number) {
|
||||||
|
editableItems = editableItems.filter((_, idx) => idx !== i);
|
||||||
|
}
|
||||||
|
|
||||||
let savingItems = $state(false);
|
let savingItems = $state(false);
|
||||||
async function saveActionItems() {
|
async function saveActionItems() {
|
||||||
const m = meetings.selected;
|
const m = meetings.selected;
|
||||||
if (!m) return;
|
if (!m) return;
|
||||||
savingItems = true;
|
savingItems = true;
|
||||||
try {
|
try {
|
||||||
await meetings.confirmActionItems(m.id, editableItems);
|
// Drop blank rows (an added-but-never-typed item) rather than persisting
|
||||||
|
// empty tasks; keep null owner rather than "".
|
||||||
|
const items = editableItems
|
||||||
|
.filter((i) => i.text.trim())
|
||||||
|
.map((i) => ({ ...i, owner: i.owner?.trim() || null }));
|
||||||
|
await meetings.confirmActionItems(m.id, items);
|
||||||
|
// confirmActionItems reloaded the meeting — resync so freshly inserted
|
||||||
|
// rows carry their new ids (a second save would otherwise re-insert them).
|
||||||
|
editableItems = (meetings.selected?.action_items ?? []).map((i) => ({ ...i }));
|
||||||
} finally {
|
} finally {
|
||||||
savingItems = false;
|
savingItems = false;
|
||||||
}
|
}
|
||||||
@@ -278,8 +334,8 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function addTag(raw: string) {
|
function addTag(raw: string) {
|
||||||
const t = raw.trim().toLowerCase();
|
const tag = raw.trim().toLowerCase();
|
||||||
if (t && !pendingTags.includes(t)) pendingTags.push(t);
|
if (tag && !pendingTags.includes(tag)) pendingTags.push(tag);
|
||||||
}
|
}
|
||||||
// GitHub-topics-style input: a comma commits everything before it as its
|
// GitHub-topics-style input: a comma commits everything before it as its
|
||||||
// own chip immediately, leaving whatever's after as the live draft.
|
// own chip immediately, leaving whatever's after as the live draft.
|
||||||
@@ -299,8 +355,8 @@
|
|||||||
if (tagDraft.trim()) addTag(tagDraft);
|
if (tagDraft.trim()) addTag(tagDraft);
|
||||||
tagDraft = "";
|
tagDraft = "";
|
||||||
}
|
}
|
||||||
function removeTag(t: string) {
|
function removeTag(tag: string) {
|
||||||
pendingTags = pendingTags.filter((x) => x !== t);
|
pendingTags = pendingTags.filter((x) => x !== tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
let savingTags = $state(false);
|
let savingTags = $state(false);
|
||||||
@@ -450,7 +506,7 @@
|
|||||||
|
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
{#if meetings.selected?.recorded}
|
{#if meetings.selected?.recorded}
|
||||||
<h3><Mic2 size={14} aria-hidden="true" /> Recording</h3>
|
<h3><Mic2 size={14} aria-hidden="true" /> {t("summary.recording_heading")}</h3>
|
||||||
{#if audioSrc}
|
{#if audioSrc}
|
||||||
<!-- a meeting recording has no caption track — no svelte-ignore needed,
|
<!-- a meeting recording has no caption track — no svelte-ignore needed,
|
||||||
the current eslint-plugin-svelte doesn't flag this element. -->
|
the current eslint-plugin-svelte doesn't flag this element. -->
|
||||||
@@ -461,17 +517,19 @@
|
|||||||
controls
|
controls
|
||||||
controlsList="nodownload noplaybackrate"
|
controlsList="nodownload noplaybackrate"
|
||||||
oncontextmenu={(e) => e.preventDefault()}
|
oncontextmenu={(e) => e.preventDefault()}
|
||||||
|
bind:this={audioEl}
|
||||||
|
ontimeupdate={() => audioEl && (player.currentMs = audioEl.currentTime * 1000)}
|
||||||
src={audioSrc}
|
src={audioSrc}
|
||||||
></audio>
|
></audio>
|
||||||
{:else if audioError}
|
{:else if audioError}
|
||||||
<p class="muted">{audioError}</p>
|
<p class="muted">{audioError}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="muted">Loading recording…</p>
|
<p class="muted">{t("summary.loading_recording")}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if meetings.selected && settings.settings.sync_enabled}
|
{#if meetings.selected && settings.settings.sync_enabled}
|
||||||
<h3><UploadCloud size={14} aria-hidden="true" /> Sync</h3>
|
<h3><UploadCloud size={14} aria-hidden="true" /> {t("summary.sync_heading")}</h3>
|
||||||
<button
|
<button
|
||||||
class="primary upload-now"
|
class="primary upload-now"
|
||||||
disabled={meetings.uploading}
|
disabled={meetings.uploading}
|
||||||
@@ -481,7 +539,7 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UploadCloud size={14} aria-hidden="true" />
|
<UploadCloud size={14} aria-hidden="true" />
|
||||||
{meetings.uploading ? "Uploading…" : "Upload now"}
|
{meetings.uploading ? t("summary.uploading") : t("summary.upload_now")}
|
||||||
</button>
|
</button>
|
||||||
{#if meetings.syncJobs.length}
|
{#if meetings.syncJobs.length}
|
||||||
<ul class="jobs">
|
<ul class="jobs">
|
||||||
@@ -500,34 +558,36 @@
|
|||||||
onclick={() => {
|
onclick={() => {
|
||||||
const m = meetings.selected;
|
const m = meetings.selected;
|
||||||
if (m) meetings.retryJob(job.jobId, m.id);
|
if (m) meetings.retryJob(job.jobId, m.id);
|
||||||
}}>retry</button
|
}}>{t("summary.retry")}</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="muted">No uploads yet for this meeting.</p>
|
<p class="muted">{t("summary.no_uploads")}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<h3><Tags size={14} aria-hidden="true" /> Tags</h3>
|
<h3><Tags size={14} aria-hidden="true" /> {t("summary.tags_heading")}</h3>
|
||||||
{#if !meetings.selected}
|
{#if !meetings.selected}
|
||||||
<p class="muted">Select a meeting to tag it.</p>
|
<p class="muted">{t("summary.tags_select")}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="tag-editor">
|
<div class="tag-editor">
|
||||||
{#each pendingTags as t (t)}
|
{#each pendingTags as tag (tag)}
|
||||||
<TagChip
|
<TagChip
|
||||||
tag={t}
|
{tag}
|
||||||
removable
|
removable
|
||||||
onRemove={() => removeTag(t)}
|
onRemove={() => removeTag(tag)}
|
||||||
onClick={() => meetings.filterByTag(t)}
|
onClick={() => meetings.filterByTag(tag)}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
<input
|
<input
|
||||||
class="tag-input"
|
class="tag-input"
|
||||||
list="known-tags"
|
list="known-tags"
|
||||||
placeholder={pendingTags.length ? "Add tag…" : "project, client, topic…"}
|
placeholder={pendingTags.length
|
||||||
|
? t("summary.tag_add_placeholder")
|
||||||
|
: t("summary.tag_first_placeholder")}
|
||||||
bind:value={tagDraft}
|
bind:value={tagDraft}
|
||||||
oninput={onTagInput}
|
oninput={onTagInput}
|
||||||
onkeydown={onTagInputKeydown}
|
onkeydown={onTagInputKeydown}
|
||||||
@@ -535,17 +595,17 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<datalist id="known-tags">
|
<datalist id="known-tags">
|
||||||
{#each meetings.allTags as t (t)}
|
{#each meetings.allTags as tag (tag)}
|
||||||
<option value={t}></option>
|
<option value={tag}></option>
|
||||||
{/each}
|
{/each}
|
||||||
</datalist>
|
</datalist>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="primary" onclick={generateTagsNow} disabled={generatingTags}>
|
<button class="primary" onclick={generateTagsNow} disabled={generatingTags}>
|
||||||
<Sparkles size={14} aria-hidden="true" />
|
<Sparkles size={14} aria-hidden="true" />
|
||||||
{generatingTags ? "Generating…" : "Generate tags"}
|
{generatingTags ? t("summary.generating") : t("summary.generate_tags")}
|
||||||
</button>
|
</button>
|
||||||
<button class="link" onclick={saveTags} disabled={savingTags}>
|
<button class="link" onclick={saveTags} disabled={savingTags}>
|
||||||
{savingTags ? "Saving…" : "Save tags"}
|
{savingTags ? t("summary.saving") : t("summary.save_tags")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if tagGenError}
|
{#if tagGenError}
|
||||||
@@ -553,18 +613,18 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<h3><Sparkles size={14} aria-hidden="true" /> Summary</h3>
|
<h3><Sparkles size={14} aria-hidden="true" /> {t("summary.summary_heading")}</h3>
|
||||||
|
|
||||||
<div class="provider-row">
|
<div class="provider-row">
|
||||||
<label class="provider-select">
|
<label class="provider-select">
|
||||||
<span class="sr-only">AI provider</span>
|
<span class="sr-only">{t("summary.ai_provider")}</span>
|
||||||
<select
|
<select
|
||||||
value={llmStatus?.provider ?? settings.settings.llm_provider}
|
value={llmStatus?.provider ?? settings.settings.llm_provider}
|
||||||
onchange={onProviderChange}
|
onchange={onProviderChange}
|
||||||
disabled={switchingProvider}
|
disabled={switchingProvider}
|
||||||
>
|
>
|
||||||
{#each Object.entries(PROVIDER_LABELS) as [id, label] (id)}
|
{#each PROVIDER_IDS as id (id)}
|
||||||
<option value={id}>{label}</option>
|
<option value={id}>{providerLabel(id)}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -574,7 +634,7 @@
|
|||||||
size={12}
|
size={12}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>{/if}
|
/>{/if}
|
||||||
{llmStatus.isLocal ? "local" : "leaves this device"}
|
{llmStatus.isLocal ? t("summary.local") : t("summary.leaves_device")}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -587,9 +647,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !meetings.selected}
|
{#if !meetings.selected}
|
||||||
<p class="muted">Select a meeting to generate a summary.</p>
|
<p class="muted">{t("summary.select_generate")}</p>
|
||||||
{:else if meetings.summarizingId === meetings.selected.id}
|
{:else if meetings.summarizingId === meetings.selected.id}
|
||||||
<p class="muted">Generating…</p>
|
<p class="muted">{t("summary.generating")}</p>
|
||||||
{#if meetings.summaryStream}
|
{#if meetings.summaryStream}
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized via renderMarkdown() -->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized via renderMarkdown() -->
|
||||||
<div class="summary-md">{@html renderMarkdown(meetings.summaryStream)}</div>
|
<div class="summary-md">{@html renderMarkdown(meetings.summaryStream)}</div>
|
||||||
@@ -598,22 +658,22 @@
|
|||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized via renderMarkdown() -->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized via renderMarkdown() -->
|
||||||
<div class="summary-md">{@html renderMarkdown(meetings.selected.summary.summary_md)}</div>
|
<div class="summary-md">{@html renderMarkdown(meetings.selected.summary.summary_md)}</div>
|
||||||
{#if meetings.selected.summary.decisions.length > 0}
|
{#if meetings.selected.summary.decisions.length > 0}
|
||||||
<h4>Decisions</h4>
|
<h4>{t("summary.decisions_heading")}</h4>
|
||||||
<ul class="decisions">
|
<ul class="decisions">
|
||||||
{#each meetings.selected.summary.decisions as d, i (i)}
|
{#each meetings.selected.summary.decisions as d, i (i)}
|
||||||
<li>{d}</li>
|
<li>{d}</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
<button class="link" onclick={onGenerateClick}>Regenerate</button>
|
<button class="link" onclick={onGenerateClick}>{t("summary.regenerate")}</button>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="muted">Generated locally after the meeting (requires a local LLM provider).</p>
|
<p class="muted">{t("summary.generated_hint")}</p>
|
||||||
{#if llmStatus && llmStatus.provider === "off"}
|
{#if llmStatus && llmStatus.provider === "off"}
|
||||||
<p class="muted small">No AI provider configured — enable one in Settings.</p>
|
<p class="muted small">{t("summary.no_provider")}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<button class="primary" onclick={onGenerateClick}>
|
<button class="primary" onclick={onGenerateClick}>
|
||||||
<Sparkles size={14} aria-hidden="true" />
|
<Sparkles size={14} aria-hidden="true" />
|
||||||
Generate summary
|
{t("summary.generate_summary")}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -621,24 +681,21 @@
|
|||||||
<p class="error">{meetings.summaryError}</p>
|
<p class="error">{meetings.summaryError}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<h3><FileText size={14} aria-hidden="true" /> Feature briefs</h3>
|
<h3><FileText size={14} aria-hidden="true" /> {t("summary.briefs_heading")}</h3>
|
||||||
{#if !meetings.selected}
|
{#if !meetings.selected}
|
||||||
<p class="muted">Select a meeting to create or view feature briefs.</p>
|
<p class="muted">{t("summary.briefs_select")}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="muted small">
|
<p class="muted small">{t("summary.briefs_desc")}</p>
|
||||||
Distills this meeting into an agent-ready spec (requires a local LLM provider) — hand it to a
|
|
||||||
coding agent, or serve it over the MCP server once that's on.
|
|
||||||
</p>
|
|
||||||
<div class="brief-create">
|
<div class="brief-create">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Target repo (optional), e.g. acme/reporting-web"
|
placeholder={t("summary.brief_repo_placeholder")}
|
||||||
bind:value={briefTargetRepo}
|
bind:value={briefTargetRepo}
|
||||||
disabled={creatingBrief}
|
disabled={creatingBrief}
|
||||||
/>
|
/>
|
||||||
<button class="primary" onclick={createBrief} disabled={creatingBrief}>
|
<button class="primary" onclick={createBrief} disabled={creatingBrief}>
|
||||||
<Sparkles size={14} aria-hidden="true" />
|
<Sparkles size={14} aria-hidden="true" />
|
||||||
{creatingBrief ? "Distilling…" : "Create feature brief"}
|
{creatingBrief ? t("summary.distilling") : t("summary.create_brief")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if briefError}
|
{#if briefError}
|
||||||
@@ -653,7 +710,7 @@
|
|||||||
{b.title}
|
{b.title}
|
||||||
{#if b.target_repo}<span class="muted small">{b.target_repo}</span>{/if}
|
{#if b.target_repo}<span class="muted small">{b.target_repo}</span>{/if}
|
||||||
</button>
|
</button>
|
||||||
<label class="expose" title="Available when the MCP server is on">
|
<label class="expose" title={t("summary.brief_mcp_title")}>
|
||||||
<input type="checkbox" checked={b.exposed} onchange={() => toggleBriefExposed(b)} />
|
<input type="checkbox" checked={b.exposed} onchange={() => toggleBriefExposed(b)} />
|
||||||
<span class="muted small">MCP</span>
|
<span class="muted small">MCP</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -667,24 +724,24 @@
|
|||||||
<div class="brief-actions">
|
<div class="brief-actions">
|
||||||
<button class="link" onclick={copyBriefMarkdown}>
|
<button class="link" onclick={copyBriefMarkdown}>
|
||||||
{#if copiedFormat === "md"}
|
{#if copiedFormat === "md"}
|
||||||
<Check size={13} aria-hidden="true" /> Copied
|
<Check size={13} aria-hidden="true" /> {t("summary.copied")}
|
||||||
{:else}
|
{:else}
|
||||||
<Copy size={13} aria-hidden="true" /> Copy as Markdown
|
<Copy size={13} aria-hidden="true" /> {t("summary.copy_md")}
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
<button class="link" onclick={copyBriefJson}>
|
<button class="link" onclick={copyBriefJson}>
|
||||||
{#if copiedFormat === "json"}
|
{#if copiedFormat === "json"}
|
||||||
<Check size={13} aria-hidden="true" /> Copied
|
<Check size={13} aria-hidden="true" /> {t("summary.copied")}
|
||||||
{:else}
|
{:else}
|
||||||
<Copy size={13} aria-hidden="true" /> Copy as JSON
|
<Copy size={13} aria-hidden="true" /> {t("summary.copy_json")}
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h4>Problem</h4>
|
<h4>{t("summary.brief_problem")}</h4>
|
||||||
<p class="brief-text">{selectedBrief.problem}</p>
|
<p class="brief-text">{selectedBrief.problem}</p>
|
||||||
<h4>Desired outcome</h4>
|
<h4>{t("summary.brief_outcome")}</h4>
|
||||||
<p class="brief-text">{selectedBrief.desired_outcome}</p>
|
<p class="brief-text">{selectedBrief.desired_outcome}</p>
|
||||||
<h4>Acceptance criteria</h4>
|
<h4>{t("summary.brief_criteria")}</h4>
|
||||||
{#if selectedBrief.acceptance_criteria.length}
|
{#if selectedBrief.acceptance_criteria.length}
|
||||||
<ul class="decisions">
|
<ul class="decisions">
|
||||||
{#each selectedBrief.acceptance_criteria as c, i (i)}
|
{#each selectedBrief.acceptance_criteria as c, i (i)}
|
||||||
@@ -692,10 +749,10 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="muted small">None captured.</p>
|
<p class="muted small">{t("summary.brief_none")}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if selectedBrief.context_excerpts.length}
|
{#if selectedBrief.context_excerpts.length}
|
||||||
<h4>Context</h4>
|
<h4>{t("summary.brief_context")}</h4>
|
||||||
<ul class="excerpts">
|
<ul class="excerpts">
|
||||||
{#each selectedBrief.context_excerpts as e, i (i)}
|
{#each selectedBrief.context_excerpts as e, i (i)}
|
||||||
<li><span class="speaker">{e.speaker}:</span> "{e.text}"</li>
|
<li><span class="speaker">{e.speaker}:</span> "{e.text}"</li>
|
||||||
@@ -706,72 +763,118 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<h3><ListTodo size={14} aria-hidden="true" /> Action items</h3>
|
<h3><ListTodo size={14} aria-hidden="true" /> {t("summary.action_items_heading")}</h3>
|
||||||
{#if !meetings.selected}
|
{#if !meetings.selected}
|
||||||
<p class="muted">Select a meeting to see its action items.</p>
|
<p class="muted">{t("summary.ai_select")}</p>
|
||||||
{:else if editableItems.length === 0}
|
|
||||||
<p class="muted">Parsed from the summary; edit and confirm before they become tasks.</p>
|
|
||||||
{:else}
|
{:else}
|
||||||
|
{#if editableItems.length === 0}
|
||||||
|
<p class="muted">{t("summary.ai_empty")}</p>
|
||||||
|
{:else}
|
||||||
|
<!-- Two-row card per item: the action text owns the full first row (it
|
||||||
|
was unreadable when six controls shared one row in this narrow
|
||||||
|
pane); owner/due/reminder are a secondary meta row beneath it. -->
|
||||||
<ul class="action-items">
|
<ul class="action-items">
|
||||||
{#each editableItems as item, i (i)}
|
{#each editableItems as item, i (i)}
|
||||||
<li>
|
<li>
|
||||||
<label>
|
<div class="ai-main">
|
||||||
<input type="checkbox" bind:checked={item.confirmed} />
|
<input
|
||||||
<span>{item.text}</span>
|
type="checkbox"
|
||||||
</label>
|
bind:checked={item.confirmed}
|
||||||
{#if item.owner}<span class="muted small">{item.owner}</span>{/if}
|
aria-label={t("summary.confirmed")}
|
||||||
|
title={t("summary.confirmed")}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="ai-text"
|
||||||
|
bind:value={item.text}
|
||||||
|
placeholder={t("summary.ai_text_placeholder")}
|
||||||
|
aria-label={t("summary.ai_text_aria")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="ai-del"
|
||||||
|
onclick={() => removeActionItem(i)}
|
||||||
|
title={t("summary.ai_delete_title")}
|
||||||
|
aria-label={t("summary.ai_delete_aria")}
|
||||||
|
>
|
||||||
|
<X size={14} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="ai-meta">
|
||||||
|
<input
|
||||||
|
class="ai-owner"
|
||||||
|
value={item.owner ?? ""}
|
||||||
|
oninput={(e) => (item.owner = (e.target as HTMLInputElement).value || null)}
|
||||||
|
placeholder={t("summary.owner")}
|
||||||
|
aria-label={t("summary.owner")}
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
class="due-date"
|
class="due-date"
|
||||||
aria-label="Due date"
|
aria-label={t("summary.due_date")}
|
||||||
value={dueDateInput(item.due_at)}
|
value={dueDateInput(item.due_at)}
|
||||||
onchange={(e) => onDueDateChange(item, (e.target as HTMLInputElement).value)}
|
onchange={(e) => onDueDateChange(item, (e.target as HTMLInputElement).value)}
|
||||||
/>
|
/>
|
||||||
<label class="remind" title="Schedule a local reminder for this due date">
|
<label class="remind" title={t("summary.reminder_title")}>
|
||||||
<input type="checkbox" bind:checked={item.reminder_set} disabled={!item.due_at} />
|
<input type="checkbox" bind:checked={item.reminder_set} disabled={!item.due_at} />
|
||||||
<Bell size={14} aria-hidden="true" />
|
<Bell size={14} aria-hidden="true" />
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
{/if}
|
||||||
|
<div class="ai-actions">
|
||||||
|
<button class="link" onclick={addActionItem}>
|
||||||
|
<Plus size={14} aria-hidden="true" />
|
||||||
|
{t("summary.add_action_item")}
|
||||||
|
</button>
|
||||||
<button class="primary" onclick={saveActionItems} disabled={savingItems}>
|
<button class="primary" onclick={saveActionItems} disabled={savingItems}>
|
||||||
<Check size={14} aria-hidden="true" />
|
<Check size={14} aria-hidden="true" />
|
||||||
{savingItems ? "Saving…" : "Save action items"}
|
{savingItems ? t("summary.saving") : t("summary.save_action_items")}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<h3><CalendarDays size={14} aria-hidden="true" /> Calendar event</h3>
|
<h3><CalendarDays size={14} aria-hidden="true" /> {t("summary.calendar_heading")}</h3>
|
||||||
{#if !meetings.selected}
|
{#if !meetings.selected}
|
||||||
<p class="muted">Select a meeting to link it to a calendar event.</p>
|
<p class="muted">{t("summary.cal_select")}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<input type="text" bind:value={eventLinkSearch} placeholder="Search event title…" />
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={eventLinkSearch}
|
||||||
|
placeholder={t("summary.event_search_placeholder")}
|
||||||
|
/>
|
||||||
<input type="date" bind:value={eventLinkDateFilter} />
|
<input type="date" bind:value={eventLinkDateFilter} />
|
||||||
</div>
|
</div>
|
||||||
<label class="pick"
|
<label class="pick"
|
||||||
>Linked event
|
>{t("summary.linked_event")}
|
||||||
<select value={meetings.selected.calendar_event_id ?? ""} onchange={onPickEvent}>
|
<select value={meetings.selected.calendar_event_id ?? ""} onchange={onPickEvent}>
|
||||||
<option value="" disabled>{eventDetail ? "Change event…" : "Link an event…"}</option>
|
<option value="" disabled
|
||||||
|
>{eventDetail ? t("summary.change_event") : t("summary.link_event")}</option
|
||||||
|
>
|
||||||
{#each filteredLinkEvents as ev (ev.id)}
|
{#each filteredLinkEvents as ev (ev.id)}
|
||||||
<option value={ev.id}
|
<option value={ev.id}
|
||||||
>{ev.subject ?? "(untitled)"} — {formatEventTime(ev.starts_at)}</option
|
>{ev.subject ?? t("summary.untitled_event")} — {formatEventTime(ev.starts_at)}</option
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
{#if calendar.events.length === 0}
|
{#if calendar.events.length === 0}
|
||||||
<p class="muted small">
|
<p class="muted small">
|
||||||
No events imported yet — import a <code>.pst</code> from Settings → Calendar.
|
{t("summary.no_events_1")} <code>.pst</code>
|
||||||
|
{t("summary.no_events_2")}
|
||||||
</p>
|
</p>
|
||||||
{:else if filteredLinkEvents.length === 0}
|
{:else if filteredLinkEvents.length === 0}
|
||||||
<p class="muted small">No events match this search/date — try clearing one.</p>
|
<p class="muted small">{t("summary.no_events_match")}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if eventDetail}
|
{#if eventDetail}
|
||||||
<div class="event-card">
|
<div class="event-card">
|
||||||
<div class="event-title">{eventDetail.event.subject ?? "(untitled)"}</div>
|
<div class="event-title">{eventDetail.event.subject ?? t("summary.untitled_event")}</div>
|
||||||
{#if eventDetail.event.organizer}
|
{#if eventDetail.event.organizer}
|
||||||
<div class="muted small">Organizer: {eventDetail.event.organizer}</div>
|
<div class="muted small">
|
||||||
|
{t("summary.organizer_label", { name: eventDetail.event.organizer })}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if eventDetail.event.starts_at}
|
{#if eventDetail.event.starts_at}
|
||||||
<div class="muted small">{formatEventTime(eventDetail.event.starts_at)}</div>
|
<div class="muted small">{formatEventTime(eventDetail.event.starts_at)}</div>
|
||||||
@@ -780,9 +883,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<h3><Users size={14} aria-hidden="true" /> Participants</h3>
|
<h3><Users size={14} aria-hidden="true" /> {t("summary.participants_heading")}</h3>
|
||||||
{#if !eventDetail || eventDetail.participants.length === 0}
|
{#if !eventDetail || eventDetail.participants.length === 0}
|
||||||
<p class="muted">Populated from the linked calendar event.</p>
|
<p class="muted">{t("summary.participants_hint")}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<ul class="participants">
|
<ul class="participants">
|
||||||
{#each eventDetail.participants as p (p.id)}
|
{#each eventDetail.participants as p (p.id)}
|
||||||
@@ -794,11 +897,11 @@
|
|||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<h3><Mic2 size={14} aria-hidden="true" /> Speakers</h3>
|
<h3><Mic2 size={14} aria-hidden="true" /> {t("summary.speakers_heading")}</h3>
|
||||||
{#if !meetings.selected}
|
{#if !meetings.selected}
|
||||||
<p class="muted">Select a meeting to name its speakers.</p>
|
<p class="muted">{t("summary.speakers_select")}</p>
|
||||||
{:else if meetings.selected.speakers.length === 0}
|
{:else if meetings.selected.speakers.length === 0}
|
||||||
<p class="muted">No speakers detected yet.</p>
|
<p class="muted">{t("summary.no_speakers")}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<ul class="speakers">
|
<ul class="speakers">
|
||||||
{#each meetings.selected.speakers as s (s.label)}
|
{#each meetings.selected.speakers as s (s.label)}
|
||||||
@@ -807,23 +910,26 @@
|
|||||||
{#if addingNameFor === s.label}
|
{#if addingNameFor === s.label}
|
||||||
<input
|
<input
|
||||||
class="grow"
|
class="grow"
|
||||||
placeholder="Speaker name"
|
placeholder={t("summary.speaker_name_placeholder")}
|
||||||
bind:value={newNameDraft}
|
bind:value={newNameDraft}
|
||||||
onkeydown={(e) => e.key === "Enter" && confirmNewName(s.label)}
|
onkeydown={(e) => e.key === "Enter" && confirmNewName(s.label)}
|
||||||
/>
|
/>
|
||||||
<button class="link" onclick={() => confirmNewName(s.label)}>Save</button>
|
<button class="link" onclick={() => confirmNewName(s.label)}>{t("summary.save")}</button
|
||||||
<button class="link" onclick={() => (addingNameFor = null)}>Cancel</button>
|
>
|
||||||
|
<button class="link" onclick={() => (addingNameFor = null)}
|
||||||
|
>{t("summary.cancel")}</button
|
||||||
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<select
|
<select
|
||||||
class="grow"
|
class="grow"
|
||||||
value={s.participant_id ?? ""}
|
value={s.participant_id ?? ""}
|
||||||
onchange={(e) => onPickSpeakerOption(e, s.label)}
|
onchange={(e) => onPickSpeakerOption(e, s.label)}
|
||||||
>
|
>
|
||||||
<option value="" disabled>{s.display_name ?? "Name this speaker…"}</option>
|
<option value="" disabled>{s.display_name ?? t("summary.name_speaker")}</option>
|
||||||
{#each eventDetail?.participants ?? [] as p (p.id)}
|
{#each eventDetail?.participants ?? [] as p (p.id)}
|
||||||
<option value={p.id}>{p.name}</option>
|
<option value={p.id}>{p.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
<option value={NEW_NAME}>+ Add new name…</option>
|
<option value={NEW_NAME}>{t("summary.add_new_name")}</option>
|
||||||
</select>
|
</select>
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
@@ -981,10 +1087,9 @@
|
|||||||
}
|
}
|
||||||
ul.action-items li {
|
ul.action-items li {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
gap: 0.35rem;
|
||||||
gap: 0.4rem;
|
padding: 0.45rem 0;
|
||||||
padding: 0.25rem 0;
|
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
ul.action-items label {
|
ul.action-items label {
|
||||||
@@ -992,6 +1097,56 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
}
|
}
|
||||||
|
.ai-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
.ai-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
/* Meta row indented under the text (past the confirm checkbox), wrapping
|
||||||
|
rather than crushing its inputs when the pane is narrow. */
|
||||||
|
.ai-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding-left: 1.4rem;
|
||||||
|
}
|
||||||
|
.ai-owner {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
.ai-del {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.15rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
.ai-del:hover {
|
||||||
|
color: var(--danger);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
.ai-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.ai-actions .link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
.due-date {
|
.due-date {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
|
|||||||
@@ -5,18 +5,28 @@
|
|||||||
import { recording } from "../stores/recording.svelte";
|
import { recording } from "../stores/recording.svelte";
|
||||||
import { meetings } from "../stores/meetings.svelte";
|
import { meetings } from "../stores/meetings.svelte";
|
||||||
import { settings } from "../stores/settings.svelte";
|
import { settings } from "../stores/settings.svelte";
|
||||||
import { api, type SpeakerInfo } from "../api";
|
import { player } from "../stores/player.svelte";
|
||||||
|
import { api, errorMessage, type SpeakerInfo } from "../api";
|
||||||
|
import { t } from "../i18n/index.svelte";
|
||||||
import { renderMarkdown } from "../markdown";
|
import { renderMarkdown } from "../markdown";
|
||||||
import { save, open } from "@tauri-apps/plugin-dialog";
|
import { save, open } from "@tauri-apps/plugin-dialog";
|
||||||
import { layout, clamp } from "../stores/layout.svelte";
|
import { layout, clamp } from "../stores/layout.svelte";
|
||||||
|
import { imports } from "../stores/imports.svelte";
|
||||||
import Splitter from "../components/Splitter.svelte";
|
import Splitter from "../components/Splitter.svelte";
|
||||||
|
import ImportTracker from "../components/ImportTracker.svelte";
|
||||||
import {
|
import {
|
||||||
Bold,
|
Bold,
|
||||||
Italic,
|
Italic,
|
||||||
Heading1,
|
Heading1,
|
||||||
Heading2,
|
Heading2,
|
||||||
|
Heading3,
|
||||||
List,
|
List,
|
||||||
|
ListOrdered,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
|
Quote,
|
||||||
|
Minus,
|
||||||
|
Sparkles,
|
||||||
|
Undo2,
|
||||||
FileDown,
|
FileDown,
|
||||||
FileText,
|
FileText,
|
||||||
FolderOutput,
|
FolderOutput,
|
||||||
@@ -35,11 +45,22 @@
|
|||||||
return speakers.find((s) => s.label === label)?.display_name ?? label;
|
return speakers.find((s) => s.label === label)?.display_name ?? label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transcript timestamps: segment start_ms → "m:ss" (or "h:mm:ss" past an
|
||||||
|
// hour). Shown as a quiet monospace prefix so a line reads "0:42 Alice: …".
|
||||||
|
function fmtTs(ms: number): string {
|
||||||
|
const total = Math.floor(ms / 1000);
|
||||||
|
const h = Math.floor(total / 3600);
|
||||||
|
const m = Math.floor((total % 3600) / 60);
|
||||||
|
const s = total % 60;
|
||||||
|
const mm = h ? String(m).padStart(2, "0") : String(m);
|
||||||
|
return `${h ? `${h}:` : ""}${mm}:${String(s).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
// T8.7/FR-TRX-4: the meeting view shows the language actually used, not
|
// T8.7/FR-TRX-4: the meeting view shows the language actually used, not
|
||||||
// just the raw ISO code — falls back to the code itself if it's not in
|
// just the raw ISO code — falls back to the code itself if it's not in
|
||||||
// the (curated) catalog, and to "auto-detecting…" before any is known.
|
// the (curated) catalog, and to "auto-detecting…" before any is known.
|
||||||
function languageLabel(code: string | null): string {
|
function languageLabel(code: string | null): string {
|
||||||
if (!code) return "auto-detecting…";
|
if (!code) return t("transcript.lang_auto");
|
||||||
return settings.languages.find((l) => l.code === code)?.label ?? code;
|
return settings.languages.find((l) => l.code === code)?.label ?? code;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +74,10 @@
|
|||||||
let editorEl: HTMLTextAreaElement | undefined = $state();
|
let editorEl: HTMLTextAreaElement | undefined = $state();
|
||||||
let saveTimer: ReturnType<typeof setTimeout> | undefined;
|
let saveTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
let loadedForId: string | null = null;
|
let loadedForId: string | null = null;
|
||||||
|
// The server copy the buffer was last synced against — lets the effect below
|
||||||
|
// tell a server-side notes change (speaker rename, reprocess) apart from the
|
||||||
|
// user's own unsaved edits.
|
||||||
|
let lastServerNotes: string | null = null;
|
||||||
|
|
||||||
// Transcript/notes split (FR-UX-1): resizable (drag the Splitter) and
|
// Transcript/notes split (FR-UX-1): resizable (drag the Splitter) and
|
||||||
// each side independently hideable, shared across the finalized-meeting
|
// each side independently hideable, shared across the finalized-meeting
|
||||||
@@ -90,14 +115,23 @@
|
|||||||
selectedSegmentMs = null;
|
selectedSegmentMs = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sync the editor buffer whenever a different meeting is selected.
|
// Sync the editor buffer whenever a different meeting is selected — and when
|
||||||
|
// the *server* copy of the same meeting's notes changes underneath us (a
|
||||||
|
// speaker rename rewrites notes.md's dialogue tags, reprocess regenerates it).
|
||||||
|
// A buffer with unsaved local edits is never clobbered: it only adopts the
|
||||||
|
// server copy when it still equals the last-synced one.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const m = meetings.selected;
|
const m = meetings.selected;
|
||||||
if (m && m.id !== loadedForId) {
|
if (m && m.id !== loadedForId) {
|
||||||
notesText = m.notes_markdown;
|
notesText = m.notes_markdown;
|
||||||
|
lastServerNotes = m.notes_markdown;
|
||||||
loadedForId = m.id;
|
loadedForId = m.id;
|
||||||
|
} else if (m && m.id === loadedForId && m.notes_markdown !== lastServerNotes) {
|
||||||
|
if (notesText === lastServerNotes) notesText = m.notes_markdown;
|
||||||
|
lastServerNotes = m.notes_markdown;
|
||||||
} else if (!m) {
|
} else if (!m) {
|
||||||
loadedForId = null;
|
loadedForId = null;
|
||||||
|
lastServerNotes = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,12 +179,76 @@
|
|||||||
scheduleSave();
|
scheduleSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slash commands: typing "/todo" (etc.) at the start of a line and pressing
|
||||||
|
// Space/Enter swaps it for the matching Markdown prefix. Reuses the same
|
||||||
|
// line-prefix model as the toolbar buttons — no rich inline menu.
|
||||||
|
// ponytail: line-prefix slash only; add a picker popover if users ask.
|
||||||
|
const SLASH_COMMANDS: Record<string, string> = {
|
||||||
|
h1: "# ",
|
||||||
|
h2: "## ",
|
||||||
|
h3: "### ",
|
||||||
|
todo: "- [ ] ",
|
||||||
|
bullet: "- ",
|
||||||
|
num: "1. ",
|
||||||
|
quote: "> ",
|
||||||
|
divider: "---\n",
|
||||||
|
};
|
||||||
|
function handleNotesKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key !== "Enter" && e.key !== " ") return;
|
||||||
|
const el = editorEl;
|
||||||
|
if (!el) return;
|
||||||
|
const { selectionStart: s, value } = el;
|
||||||
|
const lineStart = value.lastIndexOf("\n", s - 1) + 1;
|
||||||
|
const match = /^\/(\w+)$/.exec(value.slice(lineStart, s));
|
||||||
|
if (!match) return;
|
||||||
|
const prefix = SLASH_COMMANDS[match[1].toLowerCase()];
|
||||||
|
if (prefix === undefined) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const head = value.slice(0, lineStart) + prefix;
|
||||||
|
notesText = head + value.slice(s);
|
||||||
|
queueMicrotask(() => {
|
||||||
|
el.focus();
|
||||||
|
el.selectionStart = el.selectionEnd = head.length;
|
||||||
|
});
|
||||||
|
scheduleSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI-enhance (Granola-style): expand the user's rough notes into structured
|
||||||
|
// Markdown grounded in the transcript, via the configured LlmProvider (local
|
||||||
|
// by default, no new egress). Keeps a one-step Undo so we never silently lose
|
||||||
|
// what the user typed.
|
||||||
|
let enhancing = $state(false);
|
||||||
|
let enhanceError = $state<string | null>(null);
|
||||||
|
let notesBeforeEnhance = $state<string | null>(null);
|
||||||
|
async function enhanceNotes() {
|
||||||
|
const m = meetings.selected;
|
||||||
|
if (!m || enhancing) return;
|
||||||
|
enhancing = true;
|
||||||
|
enhanceError = null;
|
||||||
|
try {
|
||||||
|
const enhanced = await api.enhanceNotes(m.id, notesText);
|
||||||
|
notesBeforeEnhance = notesText;
|
||||||
|
notesText = enhanced;
|
||||||
|
scheduleSave();
|
||||||
|
} catch (e) {
|
||||||
|
enhanceError = errorMessage(e);
|
||||||
|
} finally {
|
||||||
|
enhancing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function undoEnhance() {
|
||||||
|
if (notesBeforeEnhance === null) return;
|
||||||
|
notesText = notesBeforeEnhance;
|
||||||
|
notesBeforeEnhance = null;
|
||||||
|
scheduleSave();
|
||||||
|
}
|
||||||
|
|
||||||
async function exportMd() {
|
async function exportMd() {
|
||||||
const m = meetings.selected;
|
const m = meetings.selected;
|
||||||
if (!m) return;
|
if (!m) return;
|
||||||
const path = await save({
|
const path = await save({
|
||||||
defaultPath: `${m.title}.md`,
|
defaultPath: `${m.title}.md`,
|
||||||
filters: [{ name: "Markdown", extensions: ["md"] }],
|
filters: [{ name: t("transcript.filter_md"), extensions: ["md"] }],
|
||||||
});
|
});
|
||||||
if (path) await api.exportMeeting(m.id, path, "md");
|
if (path) await api.exportMeeting(m.id, path, "md");
|
||||||
}
|
}
|
||||||
@@ -167,7 +265,7 @@
|
|||||||
if (!m) return;
|
if (!m) return;
|
||||||
const path = await save({
|
const path = await save({
|
||||||
defaultPath: `${m.title}.pdf`,
|
defaultPath: `${m.title}.pdf`,
|
||||||
filters: [{ name: "PDF", extensions: ["pdf"] }],
|
filters: [{ name: t("transcript.filter_pdf"), extensions: ["pdf"] }],
|
||||||
});
|
});
|
||||||
if (path) await api.exportMeeting(m.id, path, "pdf");
|
if (path) await api.exportMeeting(m.id, path, "pdf");
|
||||||
}
|
}
|
||||||
@@ -177,11 +275,52 @@
|
|||||||
if (!m) return;
|
if (!m) return;
|
||||||
const path = await save({
|
const path = await save({
|
||||||
defaultPath: `${m.title}.docx`,
|
defaultPath: `${m.title}.docx`,
|
||||||
filters: [{ name: "Word document", extensions: ["docx"] }],
|
filters: [{ name: t("transcript.filter_docx"), extensions: ["docx"] }],
|
||||||
});
|
});
|
||||||
if (path) await api.exportMeeting(m.id, path, "docx");
|
if (path) await api.exportMeeting(m.id, path, "docx");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export a single self-contained Obsidian note (frontmatter + notes + summary
|
||||||
|
// + action items + timestamped transcript, no audio) — save it into a vault
|
||||||
|
// folder from the dialog. FR-STORE-4 sibling of the bundle export.
|
||||||
|
async function exportObsidian() {
|
||||||
|
const m = meetings.selected;
|
||||||
|
if (!m) return;
|
||||||
|
const path = await save({
|
||||||
|
defaultPath: `${m.title}.md`,
|
||||||
|
filters: [{ name: t("transcript.filter_obsidian"), extensions: ["md"] }],
|
||||||
|
});
|
||||||
|
if (path) await api.exportMeeting(m.id, path, "obsidian");
|
||||||
|
}
|
||||||
|
|
||||||
|
// FR-REC-5: the finalized segment currently playing (greatest start_ms at or
|
||||||
|
// before the playhead), for highlight + auto-scroll. currentMs ticks ~4x/s
|
||||||
|
// but this only changes value at a segment boundary, so the effect below is
|
||||||
|
// quiet between boundaries.
|
||||||
|
const activeSegId = $derived.by(() => {
|
||||||
|
const ms = player.currentMs;
|
||||||
|
let id: number | null = null;
|
||||||
|
for (const s of meetings.selected?.segments ?? []) {
|
||||||
|
if (s.start_ms <= ms) id = s.id;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-scroll the playing segment into view — unless the user scrolled the
|
||||||
|
// transcript by hand recently, so playback doesn't yank them back.
|
||||||
|
// ponytail: 4s manual-scroll grace; widen if it still feels grabby.
|
||||||
|
let transcriptEl = $state<HTMLElement | null>(null);
|
||||||
|
let lastManualScroll = 0;
|
||||||
|
$effect(() => {
|
||||||
|
const id = activeSegId;
|
||||||
|
if (id == null || !transcriptEl) return;
|
||||||
|
if (Date.now() - lastManualScroll < 4000) return;
|
||||||
|
transcriptEl
|
||||||
|
.querySelector<HTMLElement>(".seg.active")
|
||||||
|
?.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||||
|
});
|
||||||
|
|
||||||
let reprocessModel = $state("");
|
let reprocessModel = $state("");
|
||||||
// T8.7/FR-TRX-4: "" reuses the meeting's current language (backend default
|
// T8.7/FR-TRX-4: "" reuses the meeting's current language (backend default
|
||||||
// when `language` is omitted) rather than resetting it to auto.
|
// when `language` is omitted) rather than resetting it to auto.
|
||||||
@@ -193,6 +332,15 @@
|
|||||||
reprocessing = true;
|
reprocessing = true;
|
||||||
try {
|
try {
|
||||||
await meetings.reprocess(m.id, reprocessModel, reprocessLanguage || undefined);
|
await meetings.reprocess(m.id, reprocessModel, reprocessLanguage || undefined);
|
||||||
|
// Re-transcribe rebuilds notes.md server-side (merging saved manual notes),
|
||||||
|
// but the meeting stays selected (same id), so the buffer-sync $effect —
|
||||||
|
// which only fires on an id change — won't pick it up. Resync explicitly so
|
||||||
|
// the notes pane updates in place instead of only after a restart.
|
||||||
|
const updated = meetings.selected;
|
||||||
|
if (updated && updated.id === m.id) {
|
||||||
|
notesText = updated.notes_markdown;
|
||||||
|
loadedForId = updated.id;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
reprocessing = false;
|
reprocessing = false;
|
||||||
}
|
}
|
||||||
@@ -206,34 +354,53 @@
|
|||||||
class="meeting-title"
|
class="meeting-title"
|
||||||
value={m.title}
|
value={m.title}
|
||||||
onchange={onTitleChange}
|
onchange={onTitleChange}
|
||||||
aria-label="Meeting title"
|
aria-label={t("transcript.title_aria")}
|
||||||
/>
|
/>
|
||||||
|
{#if m.status === "transcribing" || imports.get(m.id)}
|
||||||
|
<div class="import-strip"><ImportTracker meetingId={m.id} /></div>
|
||||||
|
{:else if m.model_used}
|
||||||
|
<p class="engine-meta" title={t("transcript.transcribed_with")}>
|
||||||
|
{t("transcript.transcribed_with")}
|
||||||
|
<strong>{m.model_used}</strong>{#if m.backend_used}
|
||||||
|
· {m.backend_used}{/if}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
<div
|
<div
|
||||||
class="split"
|
class="split"
|
||||||
style="grid-template-columns: {splitColumns()};"
|
style="grid-template-columns: {splitColumns()};"
|
||||||
bind:clientWidth={splitWidth}
|
bind:clientWidth={splitWidth}
|
||||||
>
|
>
|
||||||
<div class="pane transcript" class:collapsed={layout.transcriptCollapsed}>
|
<!-- wheel/touchmove here only note "the user scrolled by hand" to pause
|
||||||
|
playback auto-scroll; the pane isn't an interactive control. -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="pane transcript"
|
||||||
|
class:collapsed={layout.transcriptCollapsed}
|
||||||
|
bind:this={transcriptEl}
|
||||||
|
onwheel={() => (lastManualScroll = Date.now())}
|
||||||
|
ontouchmove={() => (lastManualScroll = Date.now())}
|
||||||
|
>
|
||||||
{#if layout.transcriptCollapsed}
|
{#if layout.transcriptCollapsed}
|
||||||
<button
|
<button
|
||||||
class="pane-toggle"
|
class="pane-toggle"
|
||||||
onclick={toggleTranscript}
|
onclick={toggleTranscript}
|
||||||
title="Show transcript"
|
title={t("transcript.show")}
|
||||||
aria-label="Show transcript"
|
aria-label={t("transcript.show")}
|
||||||
>
|
>
|
||||||
<PanelLeftOpen size={16} aria-hidden="true" />
|
<PanelLeftOpen size={16} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<h4>
|
<h4>
|
||||||
<MessageSquareText size={14} aria-hidden="true" /> Transcript
|
<MessageSquareText size={14} aria-hidden="true" />
|
||||||
<span class="badge lang" title="Transcription language"
|
{t("transcript.heading")}
|
||||||
|
<span class="badge lang" title={t("transcript.lang_title")}
|
||||||
>{languageLabel(m.language)}</span
|
>{languageLabel(m.language)}</span
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="pane-toggle inline"
|
class="pane-toggle inline"
|
||||||
onclick={toggleTranscript}
|
onclick={toggleTranscript}
|
||||||
title="Hide transcript"
|
title={t("transcript.hide")}
|
||||||
aria-label="Hide transcript"
|
aria-label={t("transcript.hide")}
|
||||||
>
|
>
|
||||||
<PanelLeftClose size={13} aria-hidden="true" />
|
<PanelLeftClose size={13} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@@ -242,15 +409,18 @@
|
|||||||
{@const reprocessModelInfo = settings.models.find((mo) => mo.id === reprocessModel)}
|
{@const reprocessModelInfo = settings.models.find((mo) => mo.id === reprocessModel)}
|
||||||
<div class="reprocess">
|
<div class="reprocess">
|
||||||
<select bind:value={reprocessModel}>
|
<select bind:value={reprocessModel}>
|
||||||
<option value="">Re-transcribe with…</option>
|
<option value="">{t("transcript.reprocess_placeholder")}</option>
|
||||||
{#each settings.models.filter((mo) => mo.installed) as mo (mo.id)}
|
{#each settings.models.filter((mo) => mo.installed) as mo (mo.id)}
|
||||||
<option value={mo.id}>{mo.label}</option>
|
<option value={mo.id}>{mo.label}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
{#if reprocessModelInfo?.multilingual}
|
{#if reprocessModelInfo?.multilingual}
|
||||||
<select bind:value={reprocessLanguage} aria-label="Reprocess language">
|
<select
|
||||||
<option value="">Keep current language</option>
|
bind:value={reprocessLanguage}
|
||||||
<option value="auto">Auto-detect</option>
|
aria-label={t("transcript.reprocess_lang_aria")}
|
||||||
|
>
|
||||||
|
<option value="">{t("transcript.reprocess_keep_lang")}</option>
|
||||||
|
<option value="auto">{t("settings.transcription.auto")}</option>
|
||||||
{#each settings.languages as l (l.code)}
|
{#each settings.languages as l (l.code)}
|
||||||
<option value={l.code}>{l.label}</option>
|
<option value={l.code}>{l.label}</option>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -258,22 +428,32 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<button disabled={!reprocessModel || reprocessing} onclick={reprocess}>
|
<button disabled={!reprocessModel || reprocessing} onclick={reprocess}>
|
||||||
<RefreshCw size={13} aria-hidden="true" class={reprocessing ? "spin" : ""} />
|
<RefreshCw size={13} aria-hidden="true" class={reprocessing ? "spin" : ""} />
|
||||||
{reprocessing ? "Re-transcribing…" : "Go"}
|
{reprocessing ? t("transcript.retranscribing") : t("transcript.reprocess_go")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if m.segments.length === 0}
|
{#if m.segments.length === 0}
|
||||||
<p class="muted">No transcript for this meeting.</p>
|
<p class="muted">{t("transcript.empty")}</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#each m.segments as s (s.id)}
|
{#each m.segments as s (s.id)}
|
||||||
<p><strong>{speakerName(s.speaker, m.speakers)}:</strong> {s.text}</p>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="seg"
|
||||||
|
class:active={s.id === activeSegId}
|
||||||
|
onclick={() => player.seek(s.start_ms)}
|
||||||
|
title={t("transcript.play_from_here")}
|
||||||
|
>
|
||||||
|
<span class="ts">{fmtTs(s.start_ms)}</span>
|
||||||
|
<strong>{speakerName(s.speaker, m.speakers)}:</strong>
|
||||||
|
{s.text}
|
||||||
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if !layout.transcriptCollapsed && !layout.notesCollapsed}
|
{#if !layout.transcriptCollapsed && !layout.notesCollapsed}
|
||||||
<Splitter
|
<Splitter
|
||||||
label="Resize transcript and notes"
|
label={t("transcript.resize")}
|
||||||
onResize={onSplitResize}
|
onResize={onSplitResize}
|
||||||
onResizeEnd={() => layout.persist()}
|
onResizeEnd={() => layout.persist()}
|
||||||
/>
|
/>
|
||||||
@@ -285,86 +465,151 @@
|
|||||||
<button
|
<button
|
||||||
class="pane-toggle"
|
class="pane-toggle"
|
||||||
onclick={toggleNotes}
|
onclick={toggleNotes}
|
||||||
title="Show notes"
|
title={t("notes.show")}
|
||||||
aria-label="Show notes"
|
aria-label={t("notes.show")}
|
||||||
>
|
>
|
||||||
<PanelRightOpen size={16} aria-hidden="true" />
|
<PanelRightOpen size={16} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<h4>
|
<h4>
|
||||||
<NotebookPen size={14} aria-hidden="true" /> Notes
|
<NotebookPen size={14} aria-hidden="true" />
|
||||||
|
{t("notes.heading")}
|
||||||
<button
|
<button
|
||||||
class="pane-toggle inline"
|
class="pane-toggle inline"
|
||||||
onclick={toggleNotes}
|
onclick={toggleNotes}
|
||||||
title="Hide notes"
|
title={t("notes.hide")}
|
||||||
aria-label="Hide notes"
|
aria-label={t("notes.hide")}
|
||||||
>
|
>
|
||||||
<PanelRightClose size={13} aria-hidden="true" />
|
<PanelRightClose size={13} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="toolbar" role="toolbar" aria-label="Notes formatting">
|
<div class="toolbar" role="toolbar" aria-label={t("notes.toolbar_aria")}>
|
||||||
<button onclick={() => wrapSelection("**")} title="Bold" aria-label="Bold">
|
<button
|
||||||
|
onclick={() => wrapSelection("**")}
|
||||||
|
title={t("notes.bold")}
|
||||||
|
aria-label={t("notes.bold")}
|
||||||
|
>
|
||||||
<Bold size={14} aria-hidden="true" />
|
<Bold size={14} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => wrapSelection("_")} title="Italic" aria-label="Italic">
|
<button
|
||||||
|
onclick={() => wrapSelection("_")}
|
||||||
|
title={t("notes.italic")}
|
||||||
|
aria-label={t("notes.italic")}
|
||||||
|
>
|
||||||
<Italic size={14} aria-hidden="true" />
|
<Italic size={14} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => insertLinePrefix("# ")} title="Heading 1" aria-label="Heading 1">
|
<button
|
||||||
|
onclick={() => insertLinePrefix("# ")}
|
||||||
|
title={t("notes.h1")}
|
||||||
|
aria-label={t("notes.h1")}
|
||||||
|
>
|
||||||
<Heading1 size={14} aria-hidden="true" />
|
<Heading1 size={14} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => insertLinePrefix("## ")}
|
onclick={() => insertLinePrefix("## ")}
|
||||||
title="Heading 2"
|
title={t("notes.h2")}
|
||||||
aria-label="Heading 2"
|
aria-label={t("notes.h2")}
|
||||||
>
|
>
|
||||||
<Heading2 size={14} aria-hidden="true" />
|
<Heading2 size={14} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => insertLinePrefix("- ")}
|
onclick={() => insertLinePrefix("- ")}
|
||||||
title="Bullet list"
|
title={t("notes.bullet")}
|
||||||
aria-label="Bullet list"
|
aria-label={t("notes.bullet")}
|
||||||
>
|
>
|
||||||
<List size={14} aria-hidden="true" />
|
<List size={14} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => insertLinePrefix("- [ ] ")}
|
onclick={() => insertLinePrefix("- [ ] ")}
|
||||||
title="Checkbox"
|
title={t("notes.checkbox_title")}
|
||||||
aria-label="Checkbox list item"
|
aria-label={t("notes.checkbox_aria")}
|
||||||
>
|
>
|
||||||
<ListChecks size={14} aria-hidden="true" />
|
<ListChecks size={14} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => insertLinePrefix("### ")}
|
||||||
|
title={t("notes.h3")}
|
||||||
|
aria-label={t("notes.h3")}
|
||||||
|
>
|
||||||
|
<Heading3 size={14} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => insertLinePrefix("1. ")}
|
||||||
|
title={t("notes.numbered")}
|
||||||
|
aria-label={t("notes.numbered")}
|
||||||
|
>
|
||||||
|
<ListOrdered size={14} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => insertLinePrefix("> ")}
|
||||||
|
title={t("notes.quote")}
|
||||||
|
aria-label={t("notes.quote")}
|
||||||
|
>
|
||||||
|
<Quote size={14} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => insertLinePrefix("---\n")}
|
||||||
|
title={t("notes.divider")}
|
||||||
|
aria-label={t("notes.divider")}
|
||||||
|
>
|
||||||
|
<Minus size={14} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="enhance"
|
||||||
|
onclick={enhanceNotes}
|
||||||
|
disabled={enhancing}
|
||||||
|
title={t("notes.enhance_title")}
|
||||||
|
>
|
||||||
|
<Sparkles size={14} aria-hidden="true" class={enhancing ? "spin" : ""} />
|
||||||
|
{enhancing ? t("notes.enhancing") : t("notes.enhance")}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="toggle"
|
class="toggle"
|
||||||
onclick={() => (notesPreview = !notesPreview)}
|
onclick={() => (notesPreview = !notesPreview)}
|
||||||
title={notesPreview ? "Edit the raw markdown" : "Render the markdown"}
|
title={notesPreview ? t("notes.edit_raw") : t("notes.render")}
|
||||||
aria-pressed={notesPreview}
|
aria-pressed={notesPreview}
|
||||||
>
|
>
|
||||||
{#if notesPreview}
|
{#if notesPreview}
|
||||||
<Pencil size={14} aria-hidden="true" />
|
<Pencil size={14} aria-hidden="true" />
|
||||||
Editor
|
{t("notes.editor")}
|
||||||
{:else}
|
{:else}
|
||||||
<Eye size={14} aria-hidden="true" />
|
<Eye size={14} aria-hidden="true" />
|
||||||
Preview
|
{t("notes.preview")}
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<button onclick={exportMd} title="Export notes as .md">
|
<button onclick={exportMd} title={t("notes.export_md_title")}>
|
||||||
<FileText size={13} aria-hidden="true" />
|
<FileText size={13} aria-hidden="true" />
|
||||||
.md
|
.md
|
||||||
</button>
|
</button>
|
||||||
<button onclick={exportPdf} title="Export notes as .pdf">
|
<button onclick={exportPdf} title={t("notes.export_pdf_title")}>
|
||||||
<FileDown size={13} aria-hidden="true" />
|
<FileDown size={13} aria-hidden="true" />
|
||||||
PDF
|
PDF
|
||||||
</button>
|
</button>
|
||||||
<button onclick={exportDocx} title="Export notes as .docx">
|
<button onclick={exportDocx} title={t("notes.export_docx_title")}>
|
||||||
<FileDown size={13} aria-hidden="true" />
|
<FileDown size={13} aria-hidden="true" />
|
||||||
Word
|
Word
|
||||||
</button>
|
</button>
|
||||||
<button onclick={exportBundle} title="Export audio + transcript + notes to a folder">
|
<button onclick={exportBundle} title={t("notes.export_bundle_title")}>
|
||||||
<FolderOutput size={13} aria-hidden="true" />
|
<FolderOutput size={13} aria-hidden="true" />
|
||||||
Bundle
|
Bundle
|
||||||
</button>
|
</button>
|
||||||
|
<button onclick={exportObsidian} title={t("notes.export_obsidian_title")}>
|
||||||
|
<NotebookPen size={13} aria-hidden="true" />
|
||||||
|
Obsidian
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{#if enhanceError}
|
||||||
|
<p class="enhance-bar error" role="alert">{enhanceError}</p>
|
||||||
|
{:else if notesBeforeEnhance !== null}
|
||||||
|
<div class="enhance-bar">
|
||||||
|
<span>{t("notes.enhanced_note")}</span>
|
||||||
|
<button class="link" onclick={undoEnhance}>
|
||||||
|
<Undo2 size={13} aria-hidden="true" />
|
||||||
|
{t("notes.undo_enhance")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="editor-preview">
|
<div class="editor-preview">
|
||||||
{#if notesPreview}
|
{#if notesPreview}
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized via renderMarkdown() -->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized via renderMarkdown() -->
|
||||||
@@ -374,7 +619,8 @@
|
|||||||
bind:this={editorEl}
|
bind:this={editorEl}
|
||||||
bind:value={notesText}
|
bind:value={notesText}
|
||||||
oninput={scheduleSave}
|
oninput={scheduleSave}
|
||||||
placeholder="Notes…"
|
onkeydown={handleNotesKeydown}
|
||||||
|
placeholder={t("notes.placeholder")}
|
||||||
></textarea>
|
></textarea>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -382,7 +628,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if recording.state === "idle"}
|
{:else if recording.state === "idle"}
|
||||||
<p class="muted pad">Transcript will appear here as you record.</p>
|
<p class="muted pad">{t("transcript.will_appear")}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Granola-style redesign: the Notes pane is open and typable while
|
<!-- Granola-style redesign: the Notes pane is open and typable while
|
||||||
recording, and clicking a transcript line attaches a note to that
|
recording, and clicking a transcript line attaches a note to that
|
||||||
@@ -397,25 +643,26 @@
|
|||||||
<button
|
<button
|
||||||
class="pane-toggle"
|
class="pane-toggle"
|
||||||
onclick={toggleTranscript}
|
onclick={toggleTranscript}
|
||||||
title="Show transcript"
|
title={t("transcript.show")}
|
||||||
aria-label="Show transcript"
|
aria-label={t("transcript.show")}
|
||||||
>
|
>
|
||||||
<PanelLeftOpen size={16} aria-hidden="true" />
|
<PanelLeftOpen size={16} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<h4>
|
<h4>
|
||||||
<MessageSquareText size={14} aria-hidden="true" /> Transcript
|
<MessageSquareText size={14} aria-hidden="true" />
|
||||||
|
{t("transcript.heading")}
|
||||||
<button
|
<button
|
||||||
class="pane-toggle inline"
|
class="pane-toggle inline"
|
||||||
onclick={toggleTranscript}
|
onclick={toggleTranscript}
|
||||||
title="Hide transcript"
|
title={t("transcript.hide")}
|
||||||
aria-label="Hide transcript"
|
aria-label={t("transcript.hide")}
|
||||||
>
|
>
|
||||||
<PanelLeftClose size={13} aria-hidden="true" />
|
<PanelLeftClose size={13} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
{#if recording.segments.length === 0}
|
{#if recording.segments.length === 0}
|
||||||
<p class="muted">Transcript will appear here as you record.</p>
|
<p class="muted">{t("transcript.will_appear")}</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#each recording.segments as s (s.id)}
|
{#each recording.segments as s (s.id)}
|
||||||
{@const hasNote = recording.segmentNotes.has(s.start_ms)}
|
{@const hasNote = recording.segmentNotes.has(s.start_ms)}
|
||||||
@@ -428,21 +675,22 @@
|
|||||||
class:active={open}
|
class:active={open}
|
||||||
onclick={() => (selectedSegmentMs = open ? null : s.start_ms)}
|
onclick={() => (selectedSegmentMs = open ? null : s.start_ms)}
|
||||||
>
|
>
|
||||||
<strong>{speakerName(s.speaker)}:</strong>
|
<span class="ts">{fmtTs(s.start_ms)}</span>
|
||||||
|
<strong>{speakerName(s.speaker, recording.speakers)}:</strong>
|
||||||
{s.text}
|
{s.text}
|
||||||
{#if hasNote}
|
{#if hasNote}
|
||||||
<span class="note-badge" title="Has a note">📝</span>
|
<span class="note-badge" title={t("transcript.has_note")}>📝</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{#if open}
|
{#if open}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="segment-note-input"
|
class="segment-note-input"
|
||||||
placeholder="Add a note for this moment…"
|
placeholder={t("transcript.note_placeholder")}
|
||||||
value={recording.segmentNotes.get(s.start_ms) ?? ""}
|
value={recording.segmentNotes.get(s.start_ms) ?? ""}
|
||||||
onchange={(e) =>
|
onchange={(e) =>
|
||||||
recording.setSegmentNote(s.start_ms, (e.target as HTMLInputElement).value)}
|
recording.setSegmentNote(s.start_ms, (e.target as HTMLInputElement).value)}
|
||||||
aria-label="Note for this transcript line"
|
aria-label={t("transcript.note_aria")}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -452,7 +700,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if !layout.transcriptCollapsed && !layout.notesCollapsed}
|
{#if !layout.transcriptCollapsed && !layout.notesCollapsed}
|
||||||
<Splitter
|
<Splitter
|
||||||
label="Resize transcript and notes"
|
label={t("transcript.resize")}
|
||||||
onResize={onSplitResize}
|
onResize={onSplitResize}
|
||||||
onResizeEnd={() => layout.persist()}
|
onResizeEnd={() => layout.persist()}
|
||||||
/>
|
/>
|
||||||
@@ -464,19 +712,20 @@
|
|||||||
<button
|
<button
|
||||||
class="pane-toggle"
|
class="pane-toggle"
|
||||||
onclick={toggleNotes}
|
onclick={toggleNotes}
|
||||||
title="Show notes"
|
title={t("notes.show")}
|
||||||
aria-label="Show notes"
|
aria-label={t("notes.show")}
|
||||||
>
|
>
|
||||||
<PanelRightOpen size={16} aria-hidden="true" />
|
<PanelRightOpen size={16} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<h4>
|
<h4>
|
||||||
<NotebookPen size={14} aria-hidden="true" /> Notes
|
<NotebookPen size={14} aria-hidden="true" />
|
||||||
|
{t("notes.heading")}
|
||||||
<button
|
<button
|
||||||
class="pane-toggle inline"
|
class="pane-toggle inline"
|
||||||
onclick={toggleNotes}
|
onclick={toggleNotes}
|
||||||
title="Hide notes"
|
title={t("notes.hide")}
|
||||||
aria-label="Hide notes"
|
aria-label={t("notes.hide")}
|
||||||
>
|
>
|
||||||
<PanelRightClose size={13} aria-hidden="true" />
|
<PanelRightClose size={13} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@@ -485,7 +734,7 @@
|
|||||||
class="live-notes"
|
class="live-notes"
|
||||||
value={recording.notesText}
|
value={recording.notesText}
|
||||||
oninput={(e) => recording.setNotesText((e.target as HTMLTextAreaElement).value)}
|
oninput={(e) => recording.setNotesText((e.target as HTMLTextAreaElement).value)}
|
||||||
placeholder="Type notes while you talk…"
|
placeholder={t("notes.live_placeholder")}
|
||||||
></textarea>
|
></textarea>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -513,6 +762,21 @@
|
|||||||
background: var(--border);
|
background: var(--border);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
.import-strip {
|
||||||
|
flex: none;
|
||||||
|
padding: 0 1rem 0.5rem;
|
||||||
|
}
|
||||||
|
.engine-meta {
|
||||||
|
flex: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 1rem 0.4rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.engine-meta strong {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
.pad {
|
.pad {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
max-width: 760px;
|
max-width: 760px;
|
||||||
@@ -561,6 +825,38 @@
|
|||||||
.note-badge {
|
.note-badge {
|
||||||
margin-left: 0.3rem;
|
margin-left: 0.3rem;
|
||||||
}
|
}
|
||||||
|
/* Finalized-transcript line: a click-to-seek button styled to read as plain
|
||||||
|
transcript text, highlighted while it's the segment currently playing. */
|
||||||
|
.seg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 120ms ease-out;
|
||||||
|
}
|
||||||
|
.seg:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
.seg.active {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
border-left-color: var(--accent);
|
||||||
|
}
|
||||||
|
/* Transcript timestamp: a quiet monospace prefix, not competing with the
|
||||||
|
speaker name or text for attention. */
|
||||||
|
.ts {
|
||||||
|
font-family: var(--font-mono, ui-monospace, monospace);
|
||||||
|
font-size: 0.78em;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-right: 0.15rem;
|
||||||
|
}
|
||||||
.segment-note-input {
|
.segment-note-input {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -713,6 +1009,40 @@
|
|||||||
.toolbar .spacer {
|
.toolbar .spacer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
.toolbar .enhance {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.toolbar .enhance:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--accent) 12%, var(--bg));
|
||||||
|
}
|
||||||
|
.toolbar .enhance:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.enhance-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.enhance-bar.error {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
.enhance-bar .link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-preview {
|
.editor-preview {
|
||||||
height: calc(100% - 2.5rem);
|
height: calc(100% - 2.5rem);
|
||||||
|
|||||||
Reference in New Issue
Block a user