ESP32-S3 Bench Guide
ESP32-S3 N16R8 bench guide: live pinout, lessons, simulators and the bench log.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Free, private in-browser transcription with speaker detection (Whisper + pyannote via Transformers.js)
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗Free, private transcription with speaker detection. Audio is processed 100% in your browser; signed-in users can save the finished transcript text to their account, and turn it into a summary, key points, chapters, action items and answers (from the transcript text only; audio is never sent anywhere).
Drop a voice note, MP3, M4A, MP4, WAV, OGG or WebM file and get a timestamped transcript
split by speaker ("Speaker 1", "Speaker 2", … or "Unknown" when it can't tell). Rename
speakers, click any line to play it, and export as .txt or .srt.
Everything runs in a Web Worker on your device (WebGPU when available, CPU/WASM otherwise). Your audio is never uploaded. Models download once from Hugging Face and are cached by the browser.
| Mode | GPU (WebGPU) | CPU only | Extra work |
|---|---|---|---|
| Fast | Whisper base (fp16/fp32 encoder, 4-bit decoder) | Whisper base (8-bit) | – |
| Best Accuracy (default) | Whisper large-v3-turbo, 4-bit weights / fp16 maths (q4f16, ≈565 MB) | Whisper small (8-bit) | second pass on segments Whisper is unsure about |
Advanced settings can override the model (tiny / base / small / large-v3-turbo). large-v3-turbo is GPU-only: on a
CPU it needed 2½ minutes just to process three short clips. The q4f16 turbo weights were chosen by
measurement: 1.0 % vs 0.9 % WER for the 1.6 GB fp16 version on the synthetic set, at a third of the download.
smoothSpeakers in src/worker.js): short pieces are re-checked against their
neighbours. Candidates are Unknown pieces ≤ 3 s and pieces ≤ 1.5 s that sit between two turns of the same
other speaker (each neighbour within 1.5 s). Pieces ≥ 0.25 s get a voice fingerprint; a piece keeps its own
label only if it matches that voice at least 0.10 (cosine) better than the surrounding speaker — so a genuine
"yeah" from another person survives, while a pitch-shifted word of the main speaker rejoins them. Pieces too
short to fingerprint take the surrounding speaker. Unknown pieces go to the closest known voice (similarity
≥ 0.25), else a neighbour within 0.5 s (no fingerprint) or one whose voice is at least loosely similar (≥ 0.15).src/engine/merge.js). Every segment keeps its original start/end time.<|en|> + <|transcribe|>: English speech → English text.
No language detection, never Whisper's translate task.WhisperControl (src/engine/decoding.js):
<|endoftext|> (Transformers.js keeps stepping finished rows until the
whole batch is done — reading past the end is what once produced "so, so, so…" / "m m m…");<|nospeech|> after <|startoftranscript|>) is recorded; a clip with
no-speech > 0.6 and avg log-prob < −1.0 (OpenAI's rule), or a "loud only" clip with no-speech > 0.5, gives no text;src/engine/loops.js; one copy of a repeated phrase is kept, none of a repeated filler).
Emphasis like "no, no, no" or "very, very important" is far below these limits and kept as said.
Important words become a Whisper prompt (<|startofprev|> + the words), like OpenAI's initial_prompt.
No previous-text context is carried between clips, so one bad clip cannot poison the next.Long recordings (15+ minutes) and unusual formats are converted with FFmpeg compiled to WebAssembly
(public/ffmpeg/, one-time ~32 MB download).
accuracy/results/ come from synthetic speech and are regression checks only.See accuracy/README.md for measuring accuracy on real recordings.
/ to focus, Enter / Shift+Enter to step through matches)M, remembered for the
browser session), auto-follow. Volume only affects playback — transcription uses its own decoded copy of the file.src/lib/clean.js): it can only delete, never add or reword, so
it can't change meaning. The original is never modified and is what gets saved; exports follow the view on screen.Built on top of a saved transcript; the transcript stays the source of truth. Requires a signed-in user.
| Tab | What | When it's generated |
|---|---|---|
| Summary | short summary (TL;DR for voice messages), key points, chapters (recordings ≥ 8 min) | automatically after a fresh transcription once the user has turned summaries on once (first time: one click); otherwise on click |
| Summary | detailed summary, structured for the recording type | on click |
| Notes | everything extracted and organized from the recording: topic notes plus the structured sections that used to be the Insights tab — General: main topics, actions, decisions, dates & times, ideas, priorities, concerns · Meeting: discussion topics, decisions, action items (owner / deadline only when said), dates, open questions, follow-ups · Lecture: topics, concepts, examples, definitions, explicit exam information (needs exam wording) vs worth reviewing · Interview: Q&A, important quotes, takeaways, themes · Podcast: topics, arguments, takeaways, quotes · Voice Message: key information, requested actions, dates & times. Only sections with content are shown, and each item appears once | on click ("Organize notes"), or automatically when a recording type was chosen before transcribing |
| Ask | questions answered only from the recording, with clickable timestamp citations, streamed | per question |
Recording type (General, Lecture, Meeting, Interview, Podcast, Voice Message) is optional: pick it in Advanced settings before transcribing or from the Type control in the recording header. With no type chosen, SparkScribe suggests one from the length and number of speakers ("Auto", no AI). It decides how Notes are organized and which Create tools are offered; the transcript never changes. It is stored on the transcript.
Grounding. The model sees each transcript line as [id] m:ss Speaker: text and cites line ids, never
timestamps. The server (server/ai/grounding.js) keeps only ids that exist (for Ask: only ids it actually sent),
drops any item without a valid line, drops "quotes" that don't appear word for word in the cited lines, drops
decisions and action items whose supporting words (evidence) aren't word for word in the cited lines, and builds
chapter times from real lines. Prompts (server/ai/prompts.js) share one rule set for every feature: keep the
speakers' own hedges and frequency words (sometimes ≠ often, might ≠ will, suggested ≠ agreed), no outside
knowledge, quotation marks only around exact words, a decision only where agreement is stated, owner/deadline only
when said ("Not specified" otherwise), and speakers named only by their transcript labels. Ask may reason from what was said
when a question calls for it ("What does he want from me?", "Does this seem urgent?"), but keeps explicit statements and
inference apart ("He said…" vs "It sounds like…"), cites the lines behind an inference, never claims to have heard tone
of voice (the model only gets text), and answers questions the recording doesn't cover specifically ("No deadline was
mentioned.") without outside knowledge. Follow-ups ("Why?") use the last few turns for context, never as evidence. An
answer that states something without any citation is labelled as unsupported. Speaker labels are never turned into guessed
names, and relative dates ("next Friday") are kept as said.
Retrieval (Ask). Transcripts up to ~7k tokens (≈ 25–30 min) are sent whole. Longer ones: a cheap call expands
the question into likely keywords, BM25 scores ~75-second chunks of the transcript, chunks get boosted for a speaker
named in the question, for "this part" (current playback position) and for lines behind already-extracted insights
that match the question's intent (decisions, deadlines…); the best chunks plus their neighbours are sent, up to
~6k tokens, with "…" marking gaps. No embeddings or vector store: segments are retrieved on the fly from the stored
transcript, so nothing extra is stored or regenerated (server/ai/retrieve.js).
Caching & staleness. Results are stored in transcription_insights (one row per transcript, kind and recording
type) and Ask answers in transcription_questions. Reopening a recording reads them; nothing regenerates by
itself. Identical questions on the same transcript version are answered from storage. transcriptions.content_version
is bumped by a database trigger whenever the text or segments change (e.g. a speaker rename); results from an older
version are shown with an "out of date — Update" notice instead of as current.
Long transcripts. Each prompt stays under ~80k tokens (Haiku 5.5's cheaper price band is ≤ 100k); longer transcripts are analysed in parts and the partial results merged in one more call.
Model & cost. Claude Haiku 5.5 (claude-haiku-5-5) for everything, called only from the server
(server/ai/). Per-task overrides: AI_MODEL_SUMMARY, AI_MODEL_INSIGHTS, AI_MODEL_ASK (or AI_MODEL for all).
At $0.10 / $0.50 per million input / output tokens, a 1-hour recording (~15k tokens) costs roughly $0.002–0.005
per summary or insight set and about $0.001–0.002 per question (thinking tokens included in output).
Privacy. Only transcript text (plus length and speaker labels) is sent to Anthropic, and only when one of these features runs. Audio is never sent. API keys stay on the server.
The Create tab on a saved recording (src/insights/tools.js, server server/ai/tools.js, endpoint api/tools.js)
turns it into finished material. What it offers depends on the recording type; nothing is generated until the user
clicks Create, and every result is saved with the recording.
| Type | Tools |
|---|---|
| Lecture | Study Guide (focus: balanced / exam / key concepts / detailed), Flashcards (fewer / recommended / more), Practice Quiz (count, difficulty, question types; multiple choice, true/false, short answer; score, review, free retakes) |
| Meeting | Meeting Recap, Action Plan, Follow-up Email draft |
| Interview | Interview Q&A with verbatim quotes |
| Podcast | Episode Notes (summary, chapter outline, takeaways, verbatim quotes) |
| General | Action Plan |
| Voice message | Reply Draft |
Plus Export for every type. Extracted information (actions, dates, decisions, definitions, exam information, quotes) lives in Notes, not Create. Reply Draft and Follow-up Email require the user to write what the draft should be about (no preset buttons): the request decides the purpose, facts come from the recording, anything the request itself states (e.g. "tell him I'll call Friday") is included because the user asked for it, and no other commitments are added; other dates, times, amounts or urgency are flagged.
quote and evidence fields must appear word for word. On top of that: an "explicit exam" item
needs exam wording in its quote or line (otherwise it becomes "worth reviewing"); quiz questions must have exactly
one matching option and verbatim evidence; flashcards are de-duplicated and vague cards dropped; meeting decisions
and action items need evidence, owner/deadline only when stated; drafts list the facts they use, and any date, time,
amount or urgency word in a draft that the recording never mentions is flagged for the user to check.transcription_artifacts, one row per (recording, kind, settings), with source_version for "Out of
date" + Update, a unique key so concurrent clicks share one generation, progress for the latest quiz score, RLS
like transcription_insights, and on delete cascade from the recording. A failed update keeps the previous result.
Created outputs are not part of Library search.feature: tool:<kind>). Long
recordings use the same part-by-part analysis as summaries, so late sections are covered. api/tools.js may run up
to 300 s.src/insights/export.js): choose any of Original / Clean transcript, Summary, Notes, Insights and created
outputs, then PDF (jsPDF), Word (docx), Markdown or text; SRT / VTT subtitles for the transcript when it has
line-level timestamps (older recordings: disabled, with the reason). Files are built in the browser from data the
signed-in user already loaded, with clean file names; PDF and Word code loads only when used.The signed-in user's searchable archive (/library, src/pages/library.js, data layer src/lib/library.js).
Nothing here uses AI; it's all Postgres.
library_list(), loaded as you scroll (plus a "Show more" button).search_library() — Postgres full-text search (English stemming: "pricing" finds "price"; quotes
for exact phrases; or, -word). Matches titles, transcript text and real speaker names (generic
"Speaker 1" labels are not indexed), plus generated Notes, which are labelled Notes, rank below the
transcript, and only count while they match the current transcript version (out-of-date Notes stay stored but
are not searched until regenerated). Ranking: title match first, then transcript relevance (ts_rank_cd), then Notes. Each result
shows up to 3 matching lines with their timestamps; clicking one opens /transcript?id=…&t=…&line=…, which
jumps to and highlights that line (or starts Play along there). Older saves without line timestamps link to
the start of the matching paragraph, marked ¶. A recording that has all the words but never on one line shows
lines with any of them, marked "Words found separately".tsvector per recording with a per-user GIN
index (btree_gin on (user_id, search_tsv)); line snippets are computed only for the page of results.
Tested with 1,000 one-hour recordings in one account: search 0.25–0.7 s, a list page ~50 ms. No transcript is
sent to the browser for searching.search_library() already returns ranked recordings with exact line
ids and timestamps, so a future multi-recording Ask can use it as its retrieval step and send only those lines.Optional. Without the two variables below the app works exactly as before, with account features hidden.
/auth (sign in, sign up, forgot/reset password, Google), /auth/callback, /library, /transcript?id=….@supabase/supabase-js in the browser with the PKCE flow (src/lib/supabase.js). All data
protection is enforced by Postgres Row Level Security.api/insights.js and api/ask.js (code in server/). They verify the
caller's Supabase access token with Supabase Auth and then query the database as that user (their token,
the publishable key), so RLS applies to the server too. No service-role key is used.segments
with start/end/speaker for timestamps). Later speaker renames sync to the saved copy.
Signed-out users can choose "Sign in to save": the transcript waits in that browser's localStorage and is
saved right after sign-in.| Name | Where | Notes |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | Vercel (Production) + .env.local | https://<project-ref>.supabase.co |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | Vercel (Production) + .env.local | the sb_publishable_… key |
ANTHROPIC_API_KEY | Vercel (Production + Preview, Sensitive) + .env.local | server only; enables Summary / Notes / Ask / Insights |
AI_MODEL, AI_MODEL_SUMMARY, AI_MODEL_INSIGHTS, AI_MODEL_ASK | optional | override claude-haiku-5-5 |
AI_PROVIDER=mock | .env.local only | local development without a key (ignored in production) |
STRIPE_SECRET_KEY | Vercel (Sensitive; test key in Preview, live key in Production only) | server only; sk_test_… / sk_live_… decides test vs live mode |
STRIPE_WEBHOOK_SECRET | Vercel (Sensitive, per environment) | server only; signing secret of that environment's webhook endpoint |
STRIPE_PLUS_PRICE_ID, STRIPE_PRO_PRICE_ID | Vercel (per environment) | price_… IDs (not secret); one per paid plan in billing_plans (STRIPE_<PLAN>_PRICE_ID) |
STRIPE_PORTAL_CONFIGURATION_ID | Vercel (per environment) | bpc_… Customer Portal configuration created by scripts/stripe-setup.mjs |
BILLING_DB_SECRET | Vercel (Sensitive) | server only; unlocks the database's billing write functions (only its SHA-256 is stored in the database) |
APP_URL | optional | fixed return URL origin for Stripe (defaults to the request's own SparkScribe host) |
Vite is configured (envPrefix) to expose only VITE_* / NEXT_PUBLIC_* variables to the browser.
Never put the secret / service-role key or ANTHROPIC_API_KEY in either prefix. Copy .env.example to
.env.local for local development; .env* files are git-ignored. npm run dev also runs the api/ functions.
The schema lives in supabase/migrations/. Apply it with the Supabase CLI (supabase db push) or by pasting
the SQL into the dashboard's SQL editor. It creates profiles, transcriptions, usage and subscriptions,
a trigger that creates a profile for every new auth user, indexes, column privileges and RLS policies:
display_name, avatar_url only)user_id always comes from auth.uid()usage and subscriptions are read-only for users; only trusted server code (service role) may write themanon) have no access to any of these tables20261008000000_phase3_insights.sql): transcriptions.segments + content_version (trigger-managed,
not writable by users); transcription_insights and transcription_questions, readable/writable only when
user_id = auth.uid() and the parent transcription belongs to that user; user_id is never writable;
deleting a transcription deletes its results.20261009000000_phase4_library.sql + two follow-ups): transcriptions.is_favorite,
last_opened_at (both user-writable) and search_tsv (generated, not writable); folders and
folder_items (a membership can only link the user's own folder to the user's own recording);
transcription_insights.search_tsv for Notes (trigger-managed); search_library, library_list,
library_stats are SECURITY INVOKER (RLS applies) and not executable by anon. Notes count in search only
while they match the transcript's current version (20261010000000_phase4_current_notes_only.sql).20261010010000_phase5_artifacts.sql): transcription_artifacts, readable/writable only when
user_id = auth.uid() and the recording is the user's; user_id never writable; deleted with the recording.20261011000000_phase6_billing.sql + hardening): see Plans & billing below.Plans live in one place, the billing_plans table (Free $0 / 60 min, Plus $7.99 / 20 h, Pro $14.99 / 50 h,
stored as cents and seconds). The pricing page, account page, server and database all read it. Stripe price IDs
differ between test and live mode, so they are server environment variables named after the plan.
What counts: only the length of recordings transcribed while signed in (transcribing requires an account;
Free needs no card). Opening, searching, Summary, Notes, Ask, Create tools and exports never count. Existing
recordings are never counted: counting starts at billing_settings.metering_starts_at.
How it's enforced:
/api/billing authorize) compares the decoded audio length with the time
left; a recording that doesn't fit is refused before any work starts ("This recording is about 30 minutes,
but you have 8 minutes remaining").usage row per transcription (unique on transcription_id, so a save can never be
counted twice). A saved recording's length can't be increased afterwards.usage and subscriptions rows and call billing_status(); they can't write
either table, billing_settings, billing_events or anything in the private schema.Transcription runs on the user's device, so recording lengths are reported by the browser. The cross-checks make under-reporting harder but this is not cryptographically tamper-proof; audio is never uploaded to verify it.
Periods: Free uses the calendar month (UTC). Paid plans use Stripe's current billing period. Access by
subscription status: active, trialing, past_due (Stripe still retrying) keep the paid plan; unpaid,
canceled, incomplete*, paused fall back to Free. Cancellation is at period end; downgrades never delete data.
Stripe: Checkout (Free → Plus/Pro) and the Customer Portal (payment method, invoices, Plus ↔ Pro with
proration, cancel at period end) are created server-side for the signed-in user's own customer (one per
account, linked by user id in the database and in Stripe metadata). The browser only ever sends plus / pro.
Webhooks (/api/stripe-webhook) are verified with STRIPE_WEBHOOK_SECRET, recorded in billing_events for
idempotency, and always re-read the subscription from Stripe before storing it. Events from the other mode, for
unknown customers, or whose metadata names a different user change nothing. Test-mode subscriptions never grant
access once billing_settings.stripe_livemode is true.
Setup: node scripts/stripe-setup.mjs --webhook-url https://<host>/api/stripe-webhook --secret-out <file>
creates the products, prices, portal configuration and webhook in the mode of the key in .env.local and prints
the IDs to set. Before launch: billing_settings.enforce_quota = true and metering_starts_at = launch time.
https://<your-domain>/** and http://localhost:5173/**.https://<project-ref>.supabase.co/auth/v1/callback.npm install
npm run dev
npm run build # outputs to dist/
Deployed on Vercel: the static site plus the two functions in api/ (vercel.json sets their max duration).
sparkscribe, GitHub repo MohamadRizk-Official/sparkscribe).
Every push to main deploys production; other branches get preview deployments.neuralscribe-moerizz.vercel.app,
neuralscribe-five.vercel.app) and the old GitHub URL still point here, so links in earlier emails keep working.
No custom domain yet.