Crawlers received an empty client-rendered shell (107 characters of text);
most AI crawlers do not run JavaScript, so the product was invisible to them.
- Build renders each locale to static HTML (/ for Korean, /en/ … /vi/) with
src/entry-server.tsx + scripts/prerender.mjs; the browser hydrates the same
locale. The language menu links to those pages instead of switching in place.
- Per-locale head: title, description, canonical, hreflang (+ x-default /en/),
Open Graph and X cards with a per-locale 1200x630 image.
- JSON-LD graph: Organization, WebSite, WebPage, SoftwareApplication (KRW
offers, version, release date, download URL from the canonical sources),
HowTo and FAQPage. No invented ratings.
- robots.txt (search, ai-input and ai-train allowed; /app, /api, invites
excluded), sitemap.xml with language alternates, llms.txt and llms-full.txt
generated from the same translations and canonical values.
- IndexNow key and a post-deploy ping (Bing, Naver, Yandex).
- A factual "at a glance" section and two FAQ answers (recognised languages,
where data is stored) in all 10 languages.
- Icons, apple-touch-icon and web manifest; asset base is now absolute so
sub-path pages load the same bundle.
Verified: 3.6k-7.7k characters of text per page, no hydration warnings,
no overflow at 320/1440 in six locales, axe 0, Lighthouse mobile SEO,
accessibility and best practices 100.
Replace the press-to-play demo and the "see how it works" button with an
example that keeps playing on the right: hold Right Alt, the capsule appears
with live wave bars, elapsed time and the partial transcript, releasing shows
the processing bar, and the cleaned sentence is pasted at the cursor of a
notes window. It cycles through two samples per language.
- Same numbers as apps/desktop recording-tip: 9 bars with cos weights, 100 ms
updates, 0.5 smoothing, +/-35% jitter, 2-28 px, per-bar colours, 120 px
progress bar min(95, (1 - 1/(1 + 1.5t)) * 100)%.
- WCAG 2.2.2: a pause button; stops when off screen or the tab is hidden.
Reduced motion shows one still frame. The animation is aria-hidden with a
text description for screen readers. Chinese and Japanese transcripts
appear three characters at a time.
- Add 404.html so unknown paths stop falling back to the landing page, delete
the duplicate accept-invite.html (/accept-invite/ is canonical) and link it
straight to /#download, and move the legal pages off the retired orange
accent (primary button contrast 5.2:1).
The landing page, download pages, invite page, assetlinks and installers
existed in two or three places; only site/ and the Forgejo feed are served.
- apps/api-server/wwwroot: delete the stale site build, download/invite
pages, .well-known copy, legacy static admin and 1.0.0 binaries. The API
no longer serves static files (UseStaticFiles/fallbacks and the apk/zip
blocker removed); the Next admin is the only admin UI.
- Delete 19 tracked installers/packages (~568 MiB) under site/public/releases,
apps/web/public/releases and wwwroot/releases; .gitignore blocks them.
- apps/web: delete the download/releases pages, desktop-release.ts, the
download.html and assetlinks copies, and the accept-invite page (invites
are only issued to the site's /accept-invite/). e2e specs call the /app
base path and check the /download redirect instead.
- scripts: delete the retired release/NAS site scripts, drop the web target
from sync-version, and check assetlinks in site/public only.
- Delete the unused Dockerfile.admin (apps/admin/Dockerfile is used).
Policy: docs/REFACTOR_POLICY.md Wave 3, W3-5 and W3-6.
Prices, quotas and site URLs were copied by hand into the edge functions,
admin, desktop and the landing site, and the copies disagreed (Payple billed
9,900/29,900 KRW, admin labels said 12,900/24,900 KRW and $9.9/$19.9, the
site said 2,900/8,900 KRW).
- packages/core/src/plan-catalog.ts is the single source for PLAN_PRICE_KRW
(Free 0 / Pro 2,900 / Pro+ 8,900 a month) and PLAN_QUOTA.
- packages/core/src/web-urls.ts is the single source for the public origin,
the /app web-app base path, SITE_URLS and billingUrl().
- Deno cannot bundle packages/core, so scripts/ci/sync-core-contract.mjs
generates _shared/core-contract.generated.ts; `npm run contract:check`
fails on drift (same pattern as version:sync).
- Payple checkout, renewal and webhook amount checks now bill the catalog
price, so existing subscribers move to the new price at their next renewal.
quota.ts, team-contract.ts and the tests read the generated values.
- Admin MRR/ARR is computed in KRW from the catalog; license labels, the
release link and desktop PREMIUM_LLM limits derive from core; the site
imports prices and quotas directly.
Policy: docs/REFACTOR_POLICY.md Wave 3, W3-1 and W3-2.
- Replace claims that contradicted the app: the default hotkey is Right Alt
(hold to talk), local features are free with no daily cap, there is no
14-day trial, Team/SSO/SCIM/ZDR are not offered, and the repository is not
public. Remove invented metrics, status badges, the competitor table, the
ad-mediation changelog, the hash calculator and the duplicate demo.
- Seven sections: hero with one labelled example, features, how it works,
privacy, pricing (Free / Pro 2,900 / Pro+ 8,900 KRW a month), download, FAQ.
Footer links the privacy policy, terms and account deletion pages.
- All copy moves into i18n and every one of the 10 locales is translated.
Pricing and cloud quotas come from site/src/pricing.ts.
- Accessibility: skip link, labelled sections, aria-expanded with Esc and
focus return for the menu, language list and FAQ, live status for the
example, reduced-motion support, 44px targets, AA contrast on the primary
button. Korean keep-all line breaking is scoped to :lang(ko) because
Tailwind's break-keep blocked wrapping in Japanese and Chinese.
- JS 111 -> 98 KB gzip, CSS 7.8 -> 5.2 KB gzip.
Next-sentence suggestions now arrive one at a time up to twelve, shown three
per page with Ctrl+Alt+Up/Down to move, Left/Right to page, Enter to accept
and Esc to close; old default bindings migrate and the panel guide follows the
live bindings. The overlay is redesigned, stays put while candidates stream
and sits outside the input box when no caret is reported.
The personal phrase memory stops learning from terminals, code editors and
the coding-agent hub, ignores symbol-heavy lines and empty-field placeholders,
and prunes existing entries that break those rules.
Fixes suggestion keys starting dictation, installs stuck on a pre-1.5.0
speech engine without the focus endpoint, Ollama runner windows flashing
while typing, the speech engine starting twice, and cold-model timeouts.
Live captions can be dragged to a remembered position and show a waiting
notice until the first line arrives.
Bumps the product version to 1.6.0 (Android/iOS build 1060000).
Adds next-sentence suggestions while typing, weekly input insights and a
personal phrase memory to the desktop app, and fixes custom instructions so
they process the text instead of inserting the instruction's own wording.
Local model requests are now bounded and individually cancellable.
Bumps the product version to 1.5.0 (Android/iOS build 1050000), refreshes the
landing and web download links, and records the new INPUT feature rows and the
open verification gaps in the infrastructure map.
Shortcuts gain several bindings per action, mouse buttons and a searchable
picker, and the stored shortcut schema changes shape, so this is a minor bump.
Existing shortcuts migrate on first launch.
Play release notes state that this release carries no mobile feature changes;
the work is desktop-only.
The hero typed its timer ref with NodeJS.Timeout, which only resolves when
@types/node is visible from an ancestor node_modules. The deploy job installs
site dependencies alone, so tsc -b failed with TS2503 and the landing site has
not been redeployed since. Use ReturnType<typeof setTimeout> and record the
measured deploy blocker plus the runner's missing Python in the map.
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4.1-flash
The download contract on both the web console and the landing site still
advertised 1.2.0, so the install button pointed at
D3RO-Voice-Setup-1.2.0-x64.exe, which the feed answers with 404 while 1.3.7
is live. Wire both contract files into version:sync so the installer version
and release date follow the version SSOT, and set them to the published 1.3.7.
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4.1-flash
The Ollama setup guide still led with two retired models while the app already
defaults to the newer one, so a fresh setup would install a model the app does
not use. The guide now recommends the same model as the rest of the app and
lists current lightweight alternatives.
Installing the previous build left an app that could not update itself: the
packaging path used to guarantee the native module build does not create the
updater configuration file, so the update client had nothing to read.
That file is now written from the single feed source and its presence in the
packaged app is checked before anything is published, so an installer that
cannot update can no longer be released.
The released installer could not start: it carried a better-sqlite3 build for the
host Node runtime instead of Electron, so the app died immediately with a module
version mismatch when it opened its database.
Packaging now proves the Electron build of every runtime-sensitive native module
before an installer or archive exists, and installers are produced only from that
verified tree, so the mistake cannot pass silently. The release pipelines run the
same check.
The default local model also pointed at a retired model: a *.gguf name that
Ollama cannot serve, while the settings, onboarding, and guide screens
recommended an older model. All of them now use the model the service code
already preferred.
Auto-update could not work at all: the installer was 189 MB because it carried
the local speech engine and ffmpeg, and the download feed rejects uploads over
about 100 MiB, so update metadata could never be published.
The installer now leaves those components out and the app fetches them the first
time they are needed, verifying every part and the joined archive before
installing. The installer is 90.6 MiB, the update feed is published again, and
updates stay small because the engine is not re-sent on every release.
The fetch is visible and recoverable: the download runs with progress, a failed
install cleans up after itself, and Settings > STT shows the runtime status with
a manual download action for when the automatic one cannot run.
The manual install path still needed 7-Zip, which the target machine does not
have, so "installable without a certificate" was not yet true.
The channel now also publishes the app as byte-split zip parts, and the install
script joins them and extracts with the built-in Windows Expand-Archive after
verifying every part and the joined archive. Version 1.3.1 republishes the
channel from a single build, because a version's artifacts can only match one
build and published volumes are never overwritten.
Local dictation had never produced a transcript on an installed build. The
engine itself was healthy; every connection to it was broken.
Installed builds shipped no speech engine at all: the packaging config had no
entry for the faster-whisper sidecar and no pipeline step built one, so the app
always fell back to a system Python without the runtime. Development was broken
too, because the sidecar and SoX paths were resolved against the Vite output
directory instead of the app root, which also meant recording failed with a SoX
ENOENT. On hosts where localhost resolves only to IPv6, every local request was
refused outright, which silently disabled both local transcription and the local
LLM.
The sidecar is now built and bundled (including the Silero VAD data it needs),
gated by a packaging check that fails when the engine or its data is missing.
Paths are discovered from the app root and fail loudly when the engine is
absent. Local engine URLs are normalized to the IPv4 loopback, decoding is tuned
so repeated hallucinations cannot compound (the same transcript now takes about
a fifth of the time), the engine is warmed up at startup, and holding the hotkey
now shows the text forming live in the recording tip.
Version 1.1.0 was published on 2026-09-15, so its tag is closed. Every commit
since then, the update-feed rework, dictionary import and export, the extra
push transports, and entitlement gating, needs its own immutable version.
Product version, Android version code, iOS build number, and all package and
store surfaces move to 1.2.0 / 1020001, with Korean and English store notes for
the new version code. The download centers read that version and its release
date from one place instead of repeating them inline.
The download section described a release as pending while the product was
already installable, and its installer link pointed at a path the site build
never ships, so a visitor would have reached a missing file.
The section now states the released version and links the published update
feed that the desktop client itself uses, with the release notes and hash
metadata one click away. Repeated color literals behind the hero animation
moved into named tokens.