Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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 — Fast engine and live dictation:** a new on-device "Fast" model (Cactus Whistle) for 7 languages, and live dictation that streams words as you speak. Unsupported languages, like Bahasa Indonesia, switch to the Multilingual Whisper model automatically.
- **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.
Expand Down
9 changes: 7 additions & 2 deletions e2e/tools/voice-to-text.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 });
Expand Down
48 changes: 26 additions & 22 deletions src/islands/media/VoiceToText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -44,7 +44,7 @@ const TR: Record<Lang, {
en: {
models: {
whistle: { label: 'Fast · 7 languages', note: '17 MB, quick on phones — English, German, French, Spanish, Italian, Dutch, Polish' },
'onnx-community/whisper-base': { label: 'Multilingual', note: 'auto-detects language; use for Bahasa and other languages' },
'onnx-community/whisper-base': { label: 'Multilingual', note: 'default — auto-detects language; English, Bahasa and many more' },
'onnx-community/whisper-small': { label: 'Multilingual · Better', note: 'much better for non-English (e.g. Bahasa); larger download' },
},
languages: {
Expand Down Expand Up @@ -73,8 +73,8 @@ const TR: Record<Lang, {
switched: l => `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…',
Expand All @@ -85,7 +85,7 @@ const TR: Record<Lang, {
id: {
models: {
whistle: { label: 'Cepat · 7 bahasa', note: '17 MB, cepat di ponsel — Inggris, Jerman, Prancis, Spanyol, Italia, Belanda, Polandia' },
'onnx-community/whisper-base': { label: 'Multibahasa', note: 'mendeteksi bahasa otomatis; gunakan untuk Bahasa Indonesia dan bahasa lain' },
'onnx-community/whisper-base': { label: 'Multibahasa', note: 'default — mendeteksi bahasa otomatis; Bahasa Indonesia, Inggris, dan banyak lagi' },
'onnx-community/whisper-small': { label: 'Multibahasa · Lebih Baik', note: 'jauh lebih baik untuk non-Inggris (mis. Bahasa Indonesia); unduhan lebih besar' },
},
languages: {
Expand Down Expand Up @@ -114,8 +114,8 @@ const TR: Record<Lang, {
switched: l => `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…',
Expand All @@ -126,9 +126,9 @@ const TR: Record<Lang, {
};

const MODELS: { value: SttModelId; label: string; note: string }[] = [
{ value: 'whistle', label: 'Fast · 7 languages', note: '17 MB, quick on phones' },
{ value: 'onnx-community/whisper-base', label: 'Multilingual', note: 'auto-detects language' },
{ value: 'onnx-community/whisper-small', label: 'Multilingual · Better', note: 'much better for non-English (e.g. Bahasa); larger download' },
{ value: 'whistle', label: 'Fast · 7 languages', note: '17 MB, quick on phones' },
];

// Whisper source-language options (value = the lowercase name Whisper expects).
Expand Down Expand Up @@ -169,15 +169,17 @@ export default function VoiceToText({ lang = 'en' }: { lang?: Lang }) {
const [restored, setRestored] = useState(false);
const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
const [audioUrl, setAudioUrl] = useState<string>('');
// 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<SttModelId>(() => resolveModel('whistle', defaultLanguageFor(lang)).model);
const [model, setModel] = useState<SttModelId>(WHISPER_FALLBACK);
const [note, setNote] = useState('');
const [liveMode, setLiveMode] = useState(false);
const [modelProgress, setModelProgress] = useState<number | null>(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<TranscriptSegment[] | null>(null);
const [editedText, setEditedText] = useState('');
const [tab, setTab] = useState<Tab>('text');
Expand Down Expand Up @@ -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<boolean> => {
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();
Expand Down Expand Up @@ -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('');
};
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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 (
<div className="space-y-4">
Expand Down Expand Up @@ -402,7 +406,7 @@ export default function VoiceToText({ lang = 'en' }: { lang?: Lang }) {
<p className="text-xs text-muted-foreground">{liveAvailable ? t.liveHint : t.liveFastOnly}</p>

{dictation.progress !== null && (
<ProgressBar percent={dictation.progress * 100} label={modelCached ? t.loadingCache : t.downloading} />
<ProgressBar percent={dictation.progress * 100} label={fastCached ? t.loadingCache : t.downloading} />
)}
{(dictation.live || dictation.words.length > 0 || dictation.pending) && !segments && (
<div data-testid="live-transcript" aria-live="polite" className="min-h-[4rem] border-2 border-border bg-muted p-3 text-sm">
Expand Down
12 changes: 6 additions & 6 deletions src/registry/tool-seo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3073,16 +3073,16 @@ const en: Record<string, ToolSeoContent> = {
'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.' },
Expand Down Expand Up @@ -6527,16 +6527,16 @@ const id: Record<string, ToolSeoContent> = {
'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.' },
Expand Down
Loading