返回资讯中心
外部精选
软件工程
#开发者实时流

Dhwani: A self-hosted voice notes app built with Whistle by Cactus Compute

Dhwani (ध्वनि) means sound in Hindi and Sanskrit. It is a self-hosted voice notes app. Speak a note in your browser. Dhwani turns it into text on your own machine and saves it as a .txt file in the folder you choose.

Hacker News 最新deepDutta_0032026/10/28 · 7 天前20 分钟阅读

以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。

Dhwani (ध्वनि) means sound in Hindi and Sanskrit. It is a self-hosted voice notes app. Speak a note in your browser. Dhwani turns it into text on your own machine and saves it as a .txt file in the folder you choose.

Tags: voice-notes speech-to-text note-taking self-hosted privacy offline local-first fastapi python tailwindcss whistle transcription

Architecture

First-time setup

Run

Configuration

Model download

Save a note

Architecture decisions

Test

Known limits

Architecture

Design principles

Principle What it means here

Local first The server runs on the user's machine and binds to 127.0.0.1.

Data stays local Audio is transcribed in memory and discarded. Notes are plain files on disk.

Small surface One FastAPI app, one speech model, one file-based store.

Explicit trade-offs Each choice has an ADR with its context, consequences and a revisit trigger.

Testable seams The transcriber and the folder registry are injected. Tests use fakes.

System context

flowchart LR subgraph Machine["Your machine"] subgraph Browser["Browser"] Home["Homepage<br/>/"] App["Notes app<br/>/app"] Rec["recorder.js<br/>16 kHz capture and WAV encode"] end subgraph Server["Python server (FastAPI, 127.0.0.1:8000)"] Guard["Host and Origin guard"] Routes["API routes"] Trans["WhistleTranscriber<br/>one clip at a time"] Reg["FolderRegistry"] Save["save_note"] end Model[("Whistle engine and model<br/>~/.cache/cactus-needle")] Registry[("data/folders.json")] Notes[("Your folders<br/>*.txt files")] end CDN["cdn.jsdelivr.net<br/>Tailwind CSS only"]

Home --> App App --> Rec App -- "JSON" --> Guard Rec -- "audio/wav" --> Guard Guard --> Routes Routes --> Trans --> Model Routes --> Reg --> Registry Routes --> Save --> Notes Browser -. "styles only" .-> CDN

Loading

Everything inside "Your machine" stays local. The only outside request is the Tailwind stylesheet. See ADR-006.

sequenceDiagram autonumber actor User participant Browser as Browser (app.js, recorder.js) participant Server as Server (FastAPI) participant Whistle participant Folder as Chosen folder

User->>Browser: Click Record Browser->>Browser: Capture mic at 16 kHz User->>Browser: Click Stop (or 25 s cap) Browser->>Browser: Encode 16-bit mono WAV Browser->>Server: POST /api/transcriptions (audio/wav) Server->>Server: Check Host, Origin, size and WAV format Server->>Whistle: transcribe(samples, language en, keywords) Whistle-->>Server: text Server-->>Browser: 200 with text Browser->>User: Transcript appended to the note User->>Browser: Click Save as .txt Browser->>Server: POST /api/notes Server->>Folder: Create new .txt file, never overwrite Server-->>Browser: 201 with file path

Loading

Audio lifecycle

Audio lives in memory only. Debug mode is the one exception, and it is off by default.

flowchart TD A["Microphone samples<br/>browser memory"] --> B["WAV bytes<br/>request body"] B --> C["Float samples<br/>server memory"] C --> D["Transcript text"] D --> E["Note box<br/>edited by the user"] E --> F[".txt file<br/>chosen folder"] C -. "debug only<br/>VOICENOTES_DEBUG_CLIPS_DIR" .-> G[("Debug folder<br/>clip and transcript")]

Loading

Module layout

flowchart TD Main["main.py<br/>entry point"] --> App["app.py<br/>routes and middleware"] Main --> Config["config.py<br/>settings from env"] Main --> Folders["folders.py<br/>folder registry"] Main --> Transcription["transcription.py<br/>WAV read and Whistle"] App --> Config App --> Folders App --> Notes["notes.py<br/>file names and writes"] App --> Transcription Warmup["warmup.py<br/>setup-time model load"] --> Transcription

Loading

Path Purpose

voicenotes/app.py Routes, request guard, error mapping

voicenotes/transcription.py WAV parsing, Whistle wrapper, privacy environment settings

voicenotes/folders.py Registered folders, stored in one JSON file

voicenotes/notes.py Safe file names and no-overwrite writes

voicenotes/config.py Environment settings

voicenotes/static/ home.html, app.html, app.js, recorder.js, recorder-worklet.js

scripts/setup.sh First-time setup

adr/ Architecture decision records

API

Method Path Purpose

GET / Homepage

GET /app Notes app

GET /healthz Health check

GET /api/folders List registered folders

POST /api/folders Register a folder by absolute path

DELETE /api/folders/{id} Unregister a folder. Files stay on disk.

POST /api/transcriptions Send one WAV clip, get text back

POST /api/notes Save a note as a .txt file

Trade-offs and scaling

One user, one process. Whistle runs on the CPU, one clip at a time. This is the first bottleneck. The next step would be a worker pool, then a job queue.

Files, not a database. Simple and portable. Search across folders would need an index. See ADR-002.

Clips, not streams. Text appears after each clip. Live text needs streaming. See ADR-004.

Local-only access. No login. Only local Host and Origin headers are accepted. See ADR-008.

First-time setup

After you clone the repo, run the setup script once from the project root:

Setup needs uv. macOS and Linux work. On Windows, use WSL. You can run the script again at any time.

uv run python -m voicenotes

Open http://127.0.0.1:8000 in your browser. The homepage is /. The notes app is /app.

The pages load Tailwind CSS from cdn.jsdelivr.net, so the styling needs internet access. Notes and audio stay on your machine. See ADR-006.

Name Default Description

VOICENOTES_HOST 127.0.0.1 Address to listen on. Keep the default. The app has no login.

VOICENOTES_PORT 8000 Port.

VOICENOTES_FOLDERS_PATH data/folders.json File that stores the folder list.

VOICENOTES_KEYWORDS empty Comma-separated names to favor in speech, for example Priya,Tom. Helps some names, not all.

VOICENOTES_DEBUG_CLIPS_DIR empty Debug only. Saves each clip and its transcript to this folder. Leave empty for normal use. See ADR-005.

Model download

./scripts/setup.sh downloads the Whistle engine and model from Hugging Face into ~/.cache/cactus-needle/. Setup sets HF_HUB_OFFLINE=0 for that one step. If you skip setup, run the warm-up step by hand before the first use:

HF_HUB_OFFLINE=0 uv run python -m voicenotes.warmup

After that, the app runs with no network access for speech recognition. The app also turns off the Whistle usage events (NEEDLE_TELEMETRY=0, DO_NOT_TRACK=1). See ADR-007.

Add a folder by its absolute path, for example /Users/you/Documents/Notes.

Click Record, speak, then click Stop. The clip stops by itself after 25 seconds. The transcript is added to the note. You can also type.

Review the text. Pick the folder and click Save as .txt.

The title becomes the file name. A name that already exists gets (2), (3) and so on. Files are never overwritten.

Removing a folder from the list does not delete its files.

Audio is not saved by default. The browser sends one clip, the server transcribes it, and the clip is discarded. Debug mode is the exception.

ADR Decision

ADR-001 Project plan. Whistle on the server, FastAPI, self-hosted, no outbound network for speech.

ADR-002 Notes are .txt files in user-chosen folders. No notes database.

ADR-003 Capture audio at 16 kHz in the browser.

ADR-004 25 s browser cap, 30 s server limit, no automatic splitting.

ADR-005 Audio kept only in opt-in debug mode.

ADR-006 Tailwind CSS from CDN. Homepage at /, app at /app.

ADR-007 Whistle telemetry off, offline by default, English only, keyword biasing.

ADR-008 Only local Host and Origin headers accepted.

Test

uv run pytest

The suite covers folder rules, file names and writes, the API, and WAV parsing. It uses a fake transcriber. The real Whistle path was checked by hand.

English only.

No login. Only 127.0.0.1 or localhost requests are accepted.

Recordings stop at 25 seconds. Longer notes need several recordings.

Whistle sometimes misses names, even with VOICENOTES_KEYWORDS. Edit those words by hand.

A file write is not atomic. A crash in the middle can leave a partial file.

No search across folders.

The styling needs the Tailwind CDN. Without it, pages load unstyled.

Made by Snehal Dutta · LinkedIn

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →