diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f455cf..1edd706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ All notable changes to GoodWebTools are documented here. +## [1.0.0-beta.7] — 2026-10-09 + +A big batch of new tools and improvements since beta.6. + +### Added +- **3D Model Viewer:** open GLB/glTF, OBJ, STL, PLY, FBX and Blender `.blend` files (including Blender 5+). You can load them from a file or a URL, with wireframe, grid, auto-rotate and screenshot controls. `.blend` files open from their saved camera. +- **Voice to Text — live dictation and a Fast engine:** live dictation streams words as you speak, powered by a new on-device "Fast" engine (Cactus Whistle) that covers 7 languages. Files and recordings still default to the more accurate Multilingual (Whisper) model, and Fast is one tap away when you want quicker results. +- **Report a problem:** an opt-in report button on every tool page. You choose what to send, and there's a consent step and spam protection. +- **New tools:** Webcam Video Recorder (with filters, background blur/replace and green screen), Digital Clock, Sort Text Lines, Daily Word Guess (EN + ID) and Fruit Merge. +- **Install single tools as apps:** each tool page can be installed as its own PWA with a distinct icon. +- **Full-screen reading** for the document, image, map and Markdown viewers. The Markdown viewer also opens local `.md` files now. +- **Countdown:** a calendar date-time picker, and timers mirrored in the tab title. +- **PDF Redact:** a zoom control for precise redaction on small text. + +### Changed +- The 3D viewer frames models tightly, so they no longer look far away on mobile. + +### Fixed +- Downloads now work reliably on mobile. Large files show a clear message when memory runs out. +- The Whiteboard saves an opened `.excalidraw` file automatically. +- Clipboard Inspector saves the real bytes, not a blob URL. +- Sort Lines puts blank lines at the end. + ## [1.0.0-beta.6] — 2026-08-29 A big upgrade to the AI Agent, plus reliability work for smaller models. diff --git a/e2e/tools/voice-to-text.spec.ts b/e2e/tools/voice-to-text.spec.ts index 6689cda..505c3bd 100644 --- a/e2e/tools/voice-to-text.spec.ts +++ b/e2e/tools/voice-to-text.spec.ts @@ -46,14 +46,18 @@ async function waitForIsland(page: Page) { const languagePicker = (page: Page) => page.locator('select:has(option[value="indonesian"])'); -test('defaults to the Fast model and switches to Whisper for Indonesian', async ({ page }) => { +test('defaults to Whisper; Fast switches to Whisper for Indonesian', async ({ page }) => { test.setTimeout(90_000); await page.goto('/tools/voice-to-text?e2e'); await waitForIsland(page); - await expect(page.getByRole('button', { name: 'Fast · 7 languages' })).toHaveAttribute('aria-pressed', 'true'); + await expect(page.getByRole('button', { name: 'Multilingual', exact: true })).toHaveAttribute('aria-pressed', 'true'); + // Live dictation runs on Fast regardless of the file model. await expect(page.getByRole('checkbox', { name: 'Live dictation' })).toBeEnabled(); + await page.getByRole('button', { name: 'Fast · 7 languages' }).click(); + await expect(page.getByRole('button', { name: 'Fast · 7 languages' })).toHaveAttribute('aria-pressed', 'true'); + await languagePicker(page).selectOption('indonesian'); await expect(page.getByRole('button', { name: 'Multilingual', exact: true })).toHaveAttribute('aria-pressed', 'true'); await expect(page.getByText('Fast doesn’t support Indonesian (Bahasa) — switched to Multilingual.')).toBeVisible(); @@ -76,6 +80,7 @@ test('transcribes a file with the Fast engine', async ({ page }) => { test.setTimeout(90_000); await page.goto('/tools/voice-to-text?e2e'); await waitForIsland(page); + await page.getByRole('button', { name: 'Fast · 7 languages' }).click(); await page.locator('input[type="file"]').setInputFiles({ name: 'tone.wav', mimeType: 'audio/wav', buffer: toneWav() }); await page.getByRole('button', { name: 'Transcribe', exact: true }).click(); await expect(page.locator('textarea')).toHaveValue('Stub transcript from the fast engine.', { timeout: 30_000 }); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 3e63474..5d6f5e9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1966,7 +1966,7 @@ dependencies = [ [[package]] name = "goodwebtools" -version = "1.0.0-beta.6" +version = "1.0.0-beta.7" dependencies = [ "chrono", "cocoa", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 47e4f85..41bd26e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "goodwebtools" -version = "1.0.0-beta.6" +version = "1.0.0-beta.7" description = "Privacy-first client-side tools" authors = ["Kresna "] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ce270fb..d3e9afe 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "GoodWebTools", - "version": "1.0.0-beta.6", + "version": "1.0.0-beta.7", "identifier": "com.goodwebtools.app", "build": { "beforeDevCommand": "npm run download:ffmpeg", diff --git a/src/islands/media/VoiceToText.tsx b/src/islands/media/VoiceToText.tsx index dfc2074..31a6cee 100644 --- a/src/islands/media/VoiceToText.tsx +++ b/src/islands/media/VoiceToText.tsx @@ -13,7 +13,7 @@ import { decodeToMono16k } from '@/tools/media/stt-audio.lib'; import { transcribeInWorker } from '@/tools/media/stt.client'; import { saveRecording, loadRecording } from '@/tools/media/recording-store'; import { type SttModelId } from '@/tools/media/stt.engine'; -import { resolveModel, defaultLanguageFor, isWhistleLanguage, wordsToSegments } from '@/tools/media/whistle.lib'; +import { resolveModel, defaultLanguageFor, isWhistleLanguage, wordsToSegments, WHISPER_FALLBACK } from '@/tools/media/whistle.lib'; import { WHISTLE_CACHE } from '@/tools/media/whistle.engine'; import { segmentsToText, @@ -44,7 +44,7 @@ const TR: Record `Fast doesn’t support ${l} — switched to Multilingual.`, fastReset: 'Fast handles English, German, French, Spanish, Italian, Dutch and Polish — language reset to Auto-detect.', live: 'Live dictation', - liveHint: 'See words appear as you speak (Fast model).', - liveFastOnly: 'Live dictation needs the Fast model and one of its languages.', + liveHint: 'See words appear as you speak (uses the Fast engine).', + liveFastOnly: 'Live dictation uses the Fast engine: English, German, French, Spanish, Italian, Dutch or Polish (or Auto-detect).', listening: 'Listening…', liveStop: c => `Stop live (${c})`, liveStarting: 'Starting live dictation…', @@ -85,7 +85,7 @@ const TR: Record `Model Cepat tidak mendukung ${l} — dialihkan ke Multibahasa.`, fastReset: 'Model Cepat mendukung Inggris, Jerman, Prancis, Spanyol, Italia, Belanda, dan Polandia — bahasa diatur ke Deteksi otomatis.', live: 'Dikte langsung', - liveHint: 'Lihat kata muncul saat Anda berbicara (model Cepat).', - liveFastOnly: 'Dikte langsung membutuhkan model Cepat dan salah satu bahasanya.', + liveHint: 'Lihat kata muncul saat Anda berbicara (memakai engine Cepat).', + liveFastOnly: 'Dikte langsung memakai engine Cepat: Inggris, Jerman, Prancis, Spanyol, Italia, Belanda, atau Polandia (atau Deteksi otomatis).', listening: 'Mendengarkan…', liveStop: c => `Hentikan dikte (${c})`, liveStarting: 'Memulai dikte langsung…', @@ -126,9 +126,9 @@ const TR: Record(null); const [audioUrl, setAudioUrl] = useState(''); - // Default: the Fast engine, unless the locale's language needs Whisper (Indonesian pages). + // Files and recordings default to Whisper (more accurate); live dictation always uses Fast. const [language, setLanguage] = useState(() => defaultLanguageFor(lang)); - const [model, setModel] = useState(() => resolveModel('whistle', defaultLanguageFor(lang)).model); + const [model, setModel] = useState(WHISPER_FALLBACK); const [note, setNote] = useState(''); const [liveMode, setLiveMode] = useState(false); const [modelProgress, setModelProgress] = useState(null); const [transcribing, setTranscribing] = useState(false); const [elapsed, setElapsed] = useState(0); const [modelCached, setModelCached] = useState(false); + /** Live dictation always loads the Fast engine, so track its cache separately. */ + const [fastCached, setFastCached] = useState(false); const [segments, setSegments] = useState(null); const [editedText, setEditedText] = useState(''); const [tab, setTab] = useState('text'); @@ -258,17 +260,19 @@ export default function VoiceToText({ lang = 'en' }: { lang?: Lang }) { // Is the selected model already in the browser cache? (Definitive — the bar // shows even on a cache read, so this tells the user whether it's a real download.) - const refreshModelCached = async () => { + const isCached = async (id: SttModelId): Promise => { try { - if (model === 'whistle') { + if (id === 'whistle') { const keys = await (await caches.open(WHISTLE_CACHE)).keys(); - setModelCached(keys.some(r => r.url.endsWith('whistle.cact'))); - return; + return keys.some(r => r.url.endsWith('whistle.cact')); } - const cache = await caches.open('transformers-cache'); - const keys = await cache.keys(); - setModelCached(keys.some(r => r.url.includes(`${model}/resolve`))); - } catch { setModelCached(false); } + const keys = await (await caches.open('transformers-cache')).keys(); + return keys.some(r => r.url.includes(`${id}/resolve`)); + } catch { return false; } + }; + const refreshModelCached = async () => { + setModelCached(await isCached(model)); + setFastCached(await isCached('whistle')); }; useEffect(() => { void refreshModelCached(); @@ -312,7 +316,6 @@ export default function VoiceToText({ lang = 'en' }: { lang?: Lang }) { const r = resolveModel(model, value); if (r.switched) { setModel(r.model); - setLiveMode(false); setNote(t.switched(t.languages[value] ?? value)); } else setNote(''); }; @@ -322,7 +325,6 @@ export default function VoiceToText({ lang = 'en' }: { lang?: Lang }) { setLanguage(''); setNote(t.fastReset); } else setNote(''); - if (value !== 'whistle') setLiveMode(false); }; // Live dictation: words stream in while recording; on stop they become the result. @@ -340,6 +342,7 @@ export default function VoiceToText({ lang = 'en' }: { lang?: Lang }) { setError(''); void wakeLock.request(); await dictation.start(language || undefined); + void refreshModelCached(); } }; // Keep the live recording (replay / re-transcribe) without wiping its transcript. @@ -371,7 +374,8 @@ export default function VoiceToText({ lang = 'en' }: { lang?: Lang }) { }; const busy = transcribing || dictation.starting; - const liveAvailable = model === 'whistle' && isWhistleLanguage(language); + // Live dictation runs on the Fast engine whatever model files use. + const liveAvailable = isWhistleLanguage(language); return (
@@ -402,7 +406,7 @@ export default function VoiceToText({ lang = 'en' }: { lang?: Lang }) {

{liveAvailable ? t.liveHint : t.liveFastOnly}

{dictation.progress !== null && ( - + )} {(dictation.live || dictation.words.length > 0 || dictation.pending) && !segments && (
diff --git a/src/registry/tool-seo.ts b/src/registry/tool-seo.ts index a3e3b14..c649414 100644 --- a/src/registry/tool-seo.ts +++ b/src/registry/tool-seo.ts @@ -3073,16 +3073,16 @@ const en: Record = { 'voice-to-text': { title: 'Free Voice to Text — Live Dictation, Transcripts & Subtitles', description: 'Free voice-to-text that transcribes recordings, audio and video on your device — with live dictation and SRT/VTT subtitles. Runs in your browser; nothing uploaded.', - intro: 'This free voice-to-text tool turns speech into editable text, timestamps and subtitles right in your browser. The Fast model (a 17 MB on-device engine) handles English, German, French, Spanish, Italian, Dutch and Polish quickly — even on phones — and can transcribe live as you speak; Whisper models cover Indonesian and many other languages. Your audio is processed on your device and never uploaded.', + intro: 'This free voice-to-text tool turns speech into editable text, timestamps and subtitles right in your browser. Whisper models transcribe files and recordings accurately in English, Indonesian and many other languages, and a 17 MB Fast engine powers live dictation as you speak in English, German, French, Spanish, Italian, Dutch and Polish — even on phones. Your audio is processed on your device and never uploaded.', howTo: [ 'Click Record to capture from your mic, tick Live dictation to see words as you speak, or drop an audio or video file (mp3, wav, m4a, mp4…).', - 'Choose the spoken language. Fast is used for English, German, French, Spanish, Italian, Dutch and Polish; other languages such as Indonesian switch to a Multilingual (Whisper) model automatically.', + 'Choose the spoken language and a model. Multilingual (Whisper) is the default for files and recordings; pick Fast for quicker results in English, German, French, Spanish, Italian, Dutch or Polish.', 'Click Transcribe (or stop live dictation); the model downloads once on first use, then runs on your device.', 'Switch between the Text, Timestamped and Subtitles tabs, edit the text, and download .txt, .srt or .vtt.', ], faqs: [ { q: 'Is my audio uploaded to a server?', a: 'No. Transcription runs entirely in your browser with on-device models. Your recording or file never leaves your device, so private conversations stay private.' }, - { q: 'Which model should I use?', a: 'Fast (17 MB) is the quickest and is used for English, German, French, Spanish, Italian, Dutch and Polish. For Indonesian, Malay, Japanese and other languages pick a Multilingual (Whisper) model — the tool switches automatically when you choose one of those languages. Multilingual · Better is the most accurate but much slower.' }, + { q: 'Which model should I use?', a: 'Multilingual (Whisper) is the default for files and recordings and works for English, Indonesian and many other languages. Multilingual · Better is the most accurate but much slower. Fast (17 MB) is the quickest option for English, German, French, Spanish, Italian, Dutch and Polish, and it always powers live dictation.' }, { q: 'How does live dictation work?', a: 'Tick Live dictation and press the button: your speech is transcribed about once a second on your device. Confirmed words appear solid and the latest guess is shown faded until it settles. When you stop, you get the full transcript and subtitles, and the recording is kept so you can replay it.' }, { q: 'Can I export subtitles?', a: 'Yes. Open the Subtitles tab and download SRT or VTT files with timestamps, or grab plain text (.txt) or a timestamped version instead.' }, { q: 'Why does the first run take a while?', a: 'The model is downloaded and cached on first use (17 MB for Fast; Whisper models are much larger). After that it loads from your browser\'s cache — even offline — and transcription runs locally.' }, @@ -6527,16 +6527,16 @@ const id: Record = { 'voice-to-text': { title: 'Suara ke Teks Gratis — Dikte Langsung, Transkrip & Subtitle', description: 'Tool suara-ke-teks gratis yang mentranskripsi rekaman, audio, dan video di perangkat Anda — dengan dikte langsung dan subtitle SRT/VTT. Berjalan di browser; tanpa unggahan.', - intro: 'Tool suara-ke-teks gratis ini mengubah ucapan menjadi teks, timestamp, dan subtitle yang dapat diedit langsung di browser Anda. Model Cepat (engine 17 MB di perangkat) menangani Inggris, Jerman, Prancis, Spanyol, Italia, Belanda, dan Polandia dengan cepat — bahkan di ponsel — dan bisa mentranskripsi langsung saat Anda berbicara; model Whisper mencakup Bahasa Indonesia dan banyak bahasa lain. Audio Anda diproses di perangkat dan tidak pernah diunggah.', + intro: 'Tool suara-ke-teks gratis ini mengubah ucapan menjadi teks, timestamp, dan subtitle yang dapat diedit langsung di browser Anda. Model Whisper mentranskripsi berkas dan rekaman secara akurat dalam Bahasa Indonesia, Inggris, dan banyak bahasa lain, dan engine Cepat 17 MB menjalankan dikte langsung saat Anda berbicara dalam Inggris, Jerman, Prancis, Spanyol, Italia, Belanda, dan Polandia — bahkan di ponsel. Audio Anda diproses di perangkat dan tidak pernah diunggah.', howTo: [ 'Klik Rekam untuk merekam dari mikrofon, centang Dikte langsung untuk melihat kata saat Anda berbicara, atau letakkan berkas audio atau video (mp3, wav, m4a, mp4…).', - 'Pilih bahasa yang diucapkan. Model Cepat dipakai untuk Inggris, Jerman, Prancis, Spanyol, Italia, Belanda, dan Polandia; bahasa lain seperti Bahasa Indonesia otomatis beralih ke model Multibahasa (Whisper).', + 'Pilih bahasa yang diucapkan dan modelnya. Multibahasa (Whisper) adalah default untuk berkas dan rekaman; pilih Cepat untuk hasil lebih cepat dalam Inggris, Jerman, Prancis, Spanyol, Italia, Belanda, atau Polandia.', 'Klik Transkripsi (atau hentikan dikte langsung); model diunduh sekali pada penggunaan pertama, lalu berjalan di perangkat Anda.', 'Beralih antara tab Teks, Berstempel waktu, dan Subtitel, edit teksnya, lalu unduh .txt, .srt, atau .vtt.', ], faqs: [ { q: 'Apakah audio saya diunggah ke server?', a: 'Tidak. Transkripsi berjalan sepenuhnya di browser Anda dengan model di perangkat. Rekaman atau berkas Anda tidak pernah meninggalkan perangkat, sehingga percakapan pribadi tetap privat.' }, - { q: 'Model mana yang sebaiknya saya pakai?', a: 'Cepat (17 MB) adalah yang tercepat dan dipakai untuk Inggris, Jerman, Prancis, Spanyol, Italia, Belanda, dan Polandia. Untuk Bahasa Indonesia, Melayu, Jepang, dan bahasa lain pilih model Multibahasa (Whisper) — tool ini beralih otomatis saat Anda memilih salah satu bahasa tersebut. Multibahasa · Lebih Baik paling akurat tetapi jauh lebih lambat.' }, + { q: 'Model mana yang sebaiknya saya pakai?', a: 'Multibahasa (Whisper) adalah default untuk berkas dan rekaman dan bekerja untuk Bahasa Indonesia, Inggris, dan banyak bahasa lain. Multibahasa · Lebih Baik paling akurat tetapi jauh lebih lambat. Cepat (17 MB) adalah pilihan tercepat untuk Inggris, Jerman, Prancis, Spanyol, Italia, Belanda, dan Polandia, dan selalu dipakai untuk dikte langsung.' }, { q: 'Bagaimana dikte langsung bekerja?', a: 'Centang Dikte langsung lalu tekan tombolnya: ucapan Anda ditranskripsi sekitar sekali per detik di perangkat. Kata yang sudah pasti tampil tegas dan tebakan terbaru tampil samar sampai stabil. Saat berhenti, Anda mendapat transkrip dan subtitle lengkap, dan rekamannya disimpan agar bisa diputar ulang.' }, { q: 'Bisakah saya mengekspor subtitle?', a: 'Ya. Buka tab Subtitel dan unduh berkas SRT atau VTT dengan timestamp, atau ambil teks biasa (.txt) atau versi dengan timestamp.' }, { q: 'Mengapa penggunaan pertama memakan waktu?', a: 'Model diunduh dan disimpan di cache pada penggunaan pertama (17 MB untuk Cepat; model Whisper jauh lebih besar). Setelah itu dimuat dari cache browser — bahkan offline — dan transkripsi berjalan secara lokal.' },