docs(web): 16 new SEO pages for v0.17–v0.18 features and more distros (#1038)

* fix(web): stop doubled title suffix and correct stale page claims

- buildPageMetadata now emits titles that already end in the brand
  (e.g. "... | Vocalinux") as absolute, so the layout template no longer
  renders "| Vocalinux | Vocalinux" on 15 existing pages
- autostart: Start on Login moved out of the tray menu in v0.18.1; point
  readers at Settings -> General and the first-run prompt
- wayland: align compositor support with docs/DISTRO_COMPATIBILITY.md
  (sway good, Hyprland experimental, river untested)

* docs(web): add guides for v0.17-v0.18 features and more distros

New pages for features that only had changelog one-liners:
/parakeet/, /faster-whisper/, /dictation-pad/, /transcribe-audio-files/,
/custom-dictionary/, /multilingual-dictation/, /pipewire/,
/global-shortcuts/, /hyprland-sway/, /local-gateway/, /flatpak/,
/install/nixos/, plus /install/debian/, /install/opensuse/,
/install/linux-mint/ and /install/pop-os/ via the distro route.

Each page is checked against src/ and docs/ at v0.18.5, carries
TechArticle/HowTo/FAQPage JSON-LD built from the visible content, and
links to existing guides. The distro route now lists support levels from
the compatibility matrix instead of unsourced "tested on" claims, uses
the documented install commands, adds AUR and native package options,
and drops its duplicate breadcrumb.

Registration: sitemap entries, breadcrumb titles, a Features footer
group, links from the install hub, homepage engine chips and the
reliability page, and a test that every static route is in the sitemap.
The homepage VocaPhone card now says Android is on Google Play.
This commit is contained in:
Jatin K Malik
2026-11-01 00:30:07 -07:00
committed by GitHub
parent dbf8039196
commit 6389e81d45
24 changed files with 6496 additions and 115 deletions
+97 -1
View File
@@ -141,7 +141,7 @@
</url>
<url>
<loc>https://vocalinux.com/autostart/</loc>
<lastmod>2026-02-22</lastmod>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
@@ -187,4 +187,100 @@
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://vocalinux.com/parakeet/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://vocalinux.com/faster-whisper/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://vocalinux.com/dictation-pad/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://vocalinux.com/transcribe-audio-files/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://vocalinux.com/custom-dictionary/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://vocalinux.com/multilingual-dictation/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://vocalinux.com/pipewire/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://vocalinux.com/global-shortcuts/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://vocalinux.com/hyprland-sway/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://vocalinux.com/local-gateway/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://vocalinux.com/flatpak/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://vocalinux.com/install/nixos/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://vocalinux.com/install/debian/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://vocalinux.com/install/opensuse/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://vocalinux.com/install/linux-mint/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://vocalinux.com/install/pop-os/</loc>
<lastmod>2026-11-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>
+47
View File
@@ -50,10 +50,57 @@ describe("SEO Assets", () => {
"https://vocalinux.com/advanced-settings/",
"https://vocalinux.com/desktop-reliability/",
"https://vocalinux.com/screenshots/",
"https://vocalinux.com/parakeet/",
"https://vocalinux.com/faster-whisper/",
"https://vocalinux.com/dictation-pad/",
"https://vocalinux.com/transcribe-audio-files/",
"https://vocalinux.com/custom-dictionary/",
"https://vocalinux.com/multilingual-dictation/",
"https://vocalinux.com/pipewire/",
"https://vocalinux.com/global-shortcuts/",
"https://vocalinux.com/hyprland-sway/",
"https://vocalinux.com/local-gateway/",
"https://vocalinux.com/flatpak/",
"https://vocalinux.com/install/nixos/",
"https://vocalinux.com/install/debian/",
"https://vocalinux.com/install/opensuse/",
"https://vocalinux.com/install/linux-mint/",
"https://vocalinux.com/install/pop-os/",
];
expectedUrls.forEach((url) => {
expect(sitemapContent).toContain(`<loc>${url}</loc>`);
});
});
it("lists every static route in the sitemap exactly once", () => {
const appDir = path.join(process.cwd(), "src", "app");
const routes: string[] = [];
const walk = (dir: string, prefix: string) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (
!entry.isDirectory() ||
entry.name.startsWith("__") ||
entry.name.startsWith("[")
) {
continue;
}
const routePath = `${prefix}/${entry.name}`;
if (fs.existsSync(path.join(dir, entry.name, "page.tsx"))) {
routes.push(`https://vocalinux.com${routePath}/`);
}
walk(path.join(dir, entry.name), routePath);
}
};
walk(appDir, "");
const locs = Array.from(
sitemapContent.matchAll(/<loc>([^<]+)<\/loc>/g),
(match) => match[1],
);
expect(new Set(locs).size).toBe(locs.length);
routes.forEach((url) => {
expect(locs).toContain(url);
});
});
});
+14 -4
View File
@@ -59,7 +59,8 @@ const howItWorks = [
{
step: 1,
title: "Enable in Vocalinux",
description: "Open Vocalinux and enable 'Start on Login' via the tray menu or Settings dialog.",
description:
"Answer yes on the first-run prompt, or open Settings → General and turn on 'Start on Login'.",
code: null,
},
{
@@ -78,8 +79,8 @@ const howItWorks = [
const managementMethods = [
{
method: "System Tray",
description: "Right-click the Vocalinux tray icon → Start on Login",
method: "First-run prompt",
description: "The welcome dialog asks once whether to start on login",
icon: Power,
},
{
@@ -117,7 +118,7 @@ export default function AutostartPage() {
headline: "Autostart Voice Dictation on Linux Login",
description:
"How to configure Vocalinux to start automatically on Linux login. XDG autostart guide for all desktop environments.",
dateModified: "2026-02-22",
dateModified: "2026-11-01",
author: {
"@type": "Person",
name: "Jatin K Malik",
@@ -235,6 +236,15 @@ export default function AutostartPage() {
</div>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-3 text-xl font-semibold text-foreground">Moved out of the tray in v0.18.1</h2>
<p className="text-sm text-muted-foreground">
Older guides say to right-click the tray icon. Since v0.18.1 the tray menu is slimmer:
Start on Login lives only in Settings → General, where the settings search also finds it
as &ldquo;autostart&rdquo;.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-primary/5 p-6">
<h2 className="mb-3 text-xl font-semibold text-foreground">
Not a systemd Service
+451
View File
@@ -0,0 +1,451 @@
import Link from "next/link";
import { type Metadata } from "next";
import {
BookOpen,
CheckCircle2,
ChevronRight,
Code,
Terminal,
} from "lucide-react";
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
import { TerminalBlock } from "@/components/terminal-block";
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
const PAGE_PATH = "/custom-dictionary";
const dictionaryFileCommand =
"vocalinux --dictionary-file /path/to/dictionary.txt";
const howToSteps = [
{
name: "Open the Custom Dictionary page",
text: "Open Settings from the tray menu and choose Custom Dictionary in the sidebar.",
},
{
name: "Add custom terms",
text: "Switch on Use custom terms, type a name or term into the Add a term field and click Add term. Or point Terms file at an existing UTF-8 file.",
},
{
name: "Add transcript corrections",
text: "Under Transcript corrections, enter what the engine writes (for example super base) and what you want instead (for example Supabase), then click Add correction.",
},
{
name: "Optionally set a post-processing script",
text: "Open the Post-Processing page, enter the path to an executable script in Script Path or click Browse…, and leave the field empty to turn it off.",
},
{
name: "Dictate",
text: "Both dictionary files are re-read before each completed segment, so changes apply to your next dictation without a restart.",
},
];
const pipeline = [
"The engine transcribes your speech, biased by custom terms on Whisper, whisper.cpp and Faster Whisper.",
"Transcript corrections replace known mishearings. This works with every engine.",
"Voice commands are interpreted, if Voice Commands is on.",
"The post-processing script, if set, rewrites the final text.",
"The result is typed into the focused app, or appended to the Dictation Pad.",
];
const faqs = [
{
q: "Which engines use custom terms?",
a: "Vocabulary bias works with Whisper, whisper.cpp and Faster Whisper. Transcript corrections work with every engine, including VOSK, Parakeet and the Remote API.",
},
{
q: "Are corrections added to the vocabulary bias automatically?",
a: "No. Corrections and terms are separate. If you want the engine to lean toward a spelling as well as fix it afterwards, add it as a term too.",
},
{
q: "My correction triggered a voice command. Why?",
a: "Corrections run before voice commands. If a replacement is itself a command phrase, it will act as one. Choose replacement text that is not a voice command unless that is what you want.",
},
{
q: "My post-processing script runs but nothing changes. What should I check?",
a: "Vocalinux falls back to the original text when the script exits non-zero, cannot be started, or runs longer than 10 seconds. Make sure the file is executable, the path is absolute, and the script writes to standard output. The Vocalinux log records the exit code and standard error.",
},
{
q: "Why did a segment disappear after I added a script?",
a: "If the script exits successfully but prints nothing, Vocalinux types nothing for that segment. Make sure every code path writes the text back out.",
},
];
export const metadata: Metadata = buildPageMetadata({
title: "Custom Dictionary, Corrections and Post-Processing | Vocalinux",
description:
"Teach Vocalinux your jargon: custom terms that bias Whisper, transcript corrections for every engine, and a stdin/stdout post-processing script hook.",
path: PAGE_PATH,
keywords: [
"custom dictionary voice dictation Linux",
"Whisper custom vocabulary",
"whisper.cpp initial prompt terms",
"speech to text corrections",
"dictation post-processing script",
"Vocalinux custom dictionary",
],
});
export default function CustomDictionaryPage() {
const articleJsonLd = {
"@context": "https://schema.org",
"@type": "TechArticle",
headline:
"Custom Dictionary, Transcript Corrections and Post-Processing in Vocalinux",
description:
"How Vocalinux custom terms, transcript corrections and the post-processing script hook work, with exact file formats and safe example scripts.",
dateModified: "2026-11-01",
author: {
"@type": "Person",
name: "Jatin K Malik",
url: "https://github.com/jatinkrmalik",
},
publisher: {
"@type": "Organization",
name: "Vocalinux",
logo: {
"@type": "ImageObject",
url: absoluteUrl("/vocalinux.png"),
},
},
mainEntityOfPage: absoluteUrl(PAGE_PATH),
};
const howToJsonLd = {
"@context": "https://schema.org",
"@type": "HowTo",
name: "Set up a custom dictionary in Vocalinux",
step: howToSteps.map((step, index) => ({
"@type": "HowToStep",
position: index + 1,
name: step.name,
text: step.text,
})),
};
const faqJsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((item) => ({
"@type": "Question",
name: item.q,
acceptedAnswer: { "@type": "Answer", text: item.a },
})),
};
return (
<SeoSubpageShell>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(howToJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<section>
<p className="subpage-kicker">Added in v0.18.0</p>
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
Custom dictionary, corrections and post-processing
</h1>
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
Speech models stumble on product names, colleagues&apos; names and
project jargon. Vocalinux gives you three tools for that: custom terms
that steer Whisper-family engines toward your vocabulary, transcript
corrections that fix known mishearings on every engine, and a script
hook that can rewrite each result before it is typed.
</p>
</section>
<section className="mb-12 grid gap-4 sm:grid-cols-3">
<div className="rounded-[12px] border border-border bg-background p-5">
<BookOpen className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h3 className="mb-2 font-semibold">Custom terms</h3>
<p className="text-sm text-muted-foreground">
A plain text list, one term per line, passed to the engine as
context so it is more likely to spell those words your way.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<CheckCircle2
className="mb-3 h-6 w-6 text-primary"
aria-hidden="true"
/>
<h3 className="mb-2 font-semibold">Transcript corrections</h3>
<p className="text-sm text-muted-foreground">
Heard-as and replace-with pairs applied to every finished
transcript, whatever the engine.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Code className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h3 className="mb-2 font-semibold">Post-processing script</h3>
<p className="text-sm text-muted-foreground">
Any executable that reads text on stdin and writes the replacement
on stdout.
</p>
</div>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">Set it up</h2>
<ol className="space-y-4">
{howToSteps.map((step, index) => (
<li
key={step.name}
className="rounded-[12px] border border-border bg-background p-5"
>
<div className="mb-2 flex items-center gap-3">
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
{index + 1}
</span>
<h3 className="font-semibold">{step.name}</h3>
</div>
<p className="ml-10 text-sm text-muted-foreground">{step.text}</p>
</li>
))}
</ol>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Order of operations
</h2>
<ol className="list-decimal space-y-2 pl-5 text-sm text-muted-foreground">
{pipeline.map((item) => (
<li key={item}>{item}</li>
))}
</ol>
<p className="mt-4 text-sm text-muted-foreground">
Because corrections come before voice commands, a correction can stop
a mishearing from triggering a destructive command. The flip side:
avoid replacements that are themselves command phrases. The command
list is on the <Link href="/shortcuts/">voice commands page</Link>.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
Custom terms file
</h2>
<p className="mb-4 text-muted-foreground">
Terms live in <code>~/.config/vocalinux/dictionary.txt</code> by
default: UTF-8, one term per line. Blank lines and lines starting with{" "}
<code>#</code> are skipped, and duplicates are ignored regardless of
case. The plain line format is deliberate, so accessibility tools and
scripts can maintain the same file.
</p>
<pre className="subpage-terminal mb-4 overflow-x-auto p-4">
<code>{`# ~/.config/vocalinux/dictionary.txt
Supabase
Kubernetes
PipeWire
Okonkwo`}</code>
</pre>
<h3 className="mb-2 font-semibold">Limits and engine support</h3>
<ul className="mb-4 list-disc space-y-2 pl-5 text-sm text-muted-foreground">
<li>
Bias applies to Whisper, whisper.cpp and Faster Whisper. On
whisper.cpp the terms are appended after anything in Advanced →
Initial Prompt, so your own prompt is kept.
</li>
<li>
Up to 200 terms are used by default (
<code>dictionary.max_words</code> in config.json), and the combined
prompt stops at 2,000 characters.
</li>
<li>
The editor rejects a single term longer than 200 characters. A file
larger than 1 MiB is read only up to its last complete line inside
that limit.
</li>
</ul>
<h3 className="mb-2 font-semibold">Session-only terms file</h3>
<p className="mb-3 text-muted-foreground">
To try a different list without touching saved settings, start
Vocalinux with:
</p>
<TerminalBlock
command={dictionaryFileCommand}
displayCommand={dictionaryFileCommand}
label="terminal"
/>
<p className="mt-3 text-sm text-muted-foreground">
The custom terms controls are disabled for that session. Transcript
corrections still apply.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
Transcript corrections file
</h2>
<p className="mb-4 text-muted-foreground">
Corrections are stored separately in{" "}
<code>~/.config/vocalinux/custom-dictionary-corrections.json</code>,
always in the config directory, whatever terms file you use. The
format is versioned:
</p>
<pre className="subpage-terminal mb-4 overflow-x-auto p-4">
<code>{`{
"version": 1,
"corrections": [
{ "heard": "super base", "replacement": "Supabase" },
{ "heard": "pipe wire", "replacement": "PipeWire" }
]
}`}</code>
</pre>
<ul className="list-disc space-y-2 pl-5 text-sm text-muted-foreground">
<li>
<code>heard</code> matches case-insensitively, only as a whole word
or phrase.
</li>
<li>
<code>replacement</code> is inserted with exactly the spelling you
typed.
</li>
<li>
Longer phrases win over shorter ones. For a repeated heard phrase,
the first wins.
</li>
<li>Up to 500 corrections, each side up to 500 characters.</li>
<li>
Settings refuses to edit a hand-written file with extra fields or
entries it would have to change. Fix the JSON, or edit it only by
hand.
</li>
</ul>
</section>
<section className="mb-12">
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
<Terminal className="h-5 w-5 text-primary" aria-hidden="true" />
The post-processing script hook
</h2>
<p className="mb-4 text-muted-foreground">
The path is saved as <code>post_processing.script_path</code> (empty
means off). For each completed segment, Vocalinux runs the file like
this:
</p>
<div className="mb-4 overflow-x-auto rounded-[12px] border border-border bg-background p-6">
<table className="w-full text-left text-sm">
<tbody>
{[
[
"Invocation",
"The path is executed directly, with no arguments and no shell. Use an absolute path to an executable file; ~ is not expanded.",
],
[
"stdin",
"The text of one segment, after corrections and voice commands.",
],
[
"stdout",
"The replacement text, used verbatim. One trailing newline is dropped if the input had none, so echo and print are safe; other newlines are kept.",
],
[
"Environment",
"Inherits Vocalinux's environment. When Vocalinux runs from the AppImage, the bundle's library paths are removed first.",
],
["Timeout", "10 seconds."],
[
"Failure",
"A non-zero exit, a timeout or a script that cannot start means the original text is used. Standard error is logged on a non-zero exit.",
],
["Empty output", "Nothing is typed for that segment."],
[
"Ordering",
"Scripts run one at a time on a dedicated worker, in order, so a slow script does not interrupt dictation. A waiting segment is dropped if you switch to another app before it is typed, unless it is going to the Dictation Pad.",
],
].map(([label, value]) => (
<tr key={label} className="border-b border-border">
<th scope="row" className="py-3 pr-4 align-top font-semibold">
{label}
</th>
<td className="py-3 text-muted-foreground">{value}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="mb-2 font-semibold">Example: uppercase everything</h3>
<p className="mb-3 text-sm text-muted-foreground">
The example from the Vocalinux user guide. Make it executable with{" "}
<code>chmod +x</code> before setting the path.
</p>
<pre className="subpage-terminal mb-4 overflow-x-auto p-4">
<code>{`#!/bin/bash
tr '[:lower:]' '[:upper:]'`}</code>
</pre>
<h3 className="mb-2 font-semibold">
Example: typographic apostrophes in Python
</h3>
<p className="mb-3 text-sm text-muted-foreground">
Reads everything from stdin and writes it back without adding a
newline. Any exception exits non-zero, so Vocalinux falls back to the
original text.
</p>
<pre className="subpage-terminal mb-4 overflow-x-auto p-4">
<code>{`#!/usr/bin/env python3
import sys
text = sys.stdin.read()
sys.stdout.write(text.replace("'", "\\u2019"))`}</code>
</pre>
<p className="text-sm text-muted-foreground">
Before pointing Vocalinux at a script, pipe a sample sentence into it
from a terminal and check that it prints the text back. Output is not
written to the log, but anything your script does with the text
(files, network calls) is up to the script.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">FAQ</h2>
<div className="space-y-4">
{faqs.map((item) => (
<div
key={item.q}
className="rounded-[12px] border border-border bg-background p-5"
>
<h3 className="mb-2 font-semibold">{item.q}</h3>
<p className="text-sm text-muted-foreground">{item.a}</p>
</div>
))}
</div>
</section>
<section className="rounded-[12px] border border-border bg-muted p-8">
<h2 className="mb-4 font-display text-2xl font-semibold">
Make dictation speak your language
</h2>
<p className="mb-6 text-muted-foreground">
Custom terms help most with a Whisper-family engine; the{" "}
<Link href="/compare/">engine comparison</Link> and{" "}
<Link href="/advanced-settings/">advanced settings</Link> pages cover
the rest of the tuning. Developers may also like{" "}
<Link href="/for-developers/">dictation for code</Link>.
</p>
<div className="flex flex-wrap gap-4">
<Link
href="/install/"
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
>
Install Vocalinux
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Link>
<Link
href="/multilingual-dictation/"
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted"
>
Multilingual dictation
</Link>
</div>
</section>
</SeoSubpageShell>
);
}
+27
View File
@@ -203,6 +203,33 @@ export default function DesktopReliabilityPage() {
</div>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-3 font-display text-2xl font-semibold">Guides for the v0.17 and v0.18 features</h2>
<p className="mb-4 text-sm text-muted-foreground">
Each of these features has its own setup guide with settings, limits, and troubleshooting.
</p>
<ul className="grid gap-2 text-sm sm:grid-cols-2">
{[
{ href: "/dictation-pad/", label: "Dictation Pad" },
{ href: "/transcribe-audio-files/", label: "File transcription with speaker labels" },
{ href: "/pipewire/", label: "PipeWire capture and system-audio sources" },
{ href: "/global-shortcuts/", label: "D-Bus activation and desktop shortcuts" },
{ href: "/multilingual-dictation/", label: "Per-language shortcuts and layout-follow" },
{ href: "/custom-dictionary/", label: "Custom dictionary and postprocessing hook" },
{ href: "/hyprland-sway/", label: "Hyprland, sway, and i3" },
{ href: "/faster-whisper/", label: "Faster Whisper engine" },
{ href: "/parakeet/", label: "Parakeet engine" },
{ href: "/flatpak/", label: "VocaHQ Flatpak remote" },
].map((guide) => (
<li key={guide.href}>
<Link href={guide.href} className="font-medium text-primary hover:underline">
{guide.label}
</Link>
</li>
))}
</ul>
</section>
<section className="mb-12 grid gap-6 lg:grid-cols-[1fr_1fr]">
<div className="rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">Why This Matters</h2>
+432
View File
@@ -0,0 +1,432 @@
import Link from "next/link";
import { type Metadata } from "next";
import {
CheckCircle2,
ChevronRight,
Copy,
Keyboard,
Monitor,
} from "lucide-react";
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
const PAGE_PATH = "/dictation-pad";
const howToSteps = [
{
name: "Turn on pad capture",
text: "Open Settings from the tray menu, go to the Dictation page and, in the Output group, switch on Dictate into Dictation Pad.",
},
{
name: "Open the pad",
text: "Click Open on the Open Dictation Pad row, or choose Dictation Pad from the tray menu. A window titled Vocalinux Dictation Pad appears.",
},
{
name: "Dictate as usual",
text: "Use your normal dictation shortcut. Each finished segment is appended to the end of the pad instead of being typed into the focused app.",
},
{
name: "Copy the text out",
text: "Click Copy All to put the whole pad on the clipboard (the button briefly reads Copied), or select part of the text and copy it with your keyboard. Paste it into the app you are working in.",
},
{
name: "Clear when done",
text: "Click Clear to empty the pad for the next piece of writing.",
},
];
const settingsRows = [
{
label: "Dictate into Dictation Pad",
where: "Settings → Dictation → Output",
key: "text_injection.dictate_to_pad",
note: "Off by default. On routes every dictated segment into the pad.",
},
{
label: "Open Dictation Pad",
where: "Settings → Dictation → Output",
key: "(button)",
note: "Shows the pad window. The tray menu has the same Dictation Pad entry.",
},
{
label: "Voice Commands",
where: "Settings → Dictation → Output",
key: "speech_recognition.voice_commands_enabled",
note: "Needed if you want spoken editing commands to act on the pad.",
},
{
label: "Copy to Clipboard",
where: "Settings → Dictation → Output",
key: "text_injection.copy_to_clipboard",
note: "Separate option that copies each result to the clipboard. Not required for the pad.",
},
];
const faqs = [
{
q: "Does the Dictation Pad save my text to disk?",
a: "No. The pad keeps its text for the current session only. Closing the window hides it and keeps the text, but quitting Vocalinux discards it. Copy anything you want to keep before you quit.",
},
{
q: "Why does the pad not stay on top of my other windows on Wayland?",
a: "Vocalinux asks for the pad to float above other windows and appear on every workspace, which works on X11. Wayland has no always-on-top protocol for normal apps, so the compositor decides. If a covered pad stops drawing, choose Dictation Pad from the tray menu again and Vocalinux recreates the window.",
},
{
q: "Do I still need IBus, ydotool or wtype?",
a: "Not for getting text out. Pad capture skips cross-application text injection entirely. You still need a working dictation shortcut, which on Wayland means the built-in listener with access to input devices, or External Activation (Desktop Shortcut) with a desktop shortcut that runs vocalinux --toggle.",
},
{
q: "Can I edit text inside the pad?",
a: "Yes. The pad is an ordinary editable text box. You can type, fix words and delete lines by hand, and new dictation is still appended at the end.",
},
{
q: "Do voice commands work in the pad?",
a: "Yes, when Voice Commands is on. While pad capture is on, editing commands such as delete that, undo, redo, select all, cut, copy and paste act on the pad and never reach the app that has keyboard focus. Editing commands are English-only.",
},
];
export const metadata: Metadata = buildPageMetadata({
title: "Dictation Pad: Wayland-Safe Voice Typing on Linux | Vocalinux",
description:
"Dictate into the Vocalinux Dictation Pad, then copy text out by hand. The Wayland-safe path that skips text injection. Setup, settings, limits and FAQ.",
path: PAGE_PATH,
keywords: [
"Wayland voice typing",
"Wayland dictation workaround",
"Linux dictation pad",
"voice typing without text injection",
"Vocalinux Dictation Pad",
"speech to text window Linux",
],
});
export default function DictationPadPage() {
const articleJsonLd = {
"@context": "https://schema.org",
"@type": "TechArticle",
headline: "Dictation Pad: Wayland-Safe Voice Typing on Linux",
description:
"How the Vocalinux Dictation Pad receives dictation in its own window so text never depends on cross-application injection, and how to copy it out.",
dateModified: "2026-11-01",
author: {
"@type": "Person",
name: "Jatin K Malik",
url: "https://github.com/jatinkrmalik",
},
publisher: {
"@type": "Organization",
name: "Vocalinux",
logo: {
"@type": "ImageObject",
url: absoluteUrl("/vocalinux.png"),
},
},
mainEntityOfPage: absoluteUrl(PAGE_PATH),
};
const howToJsonLd = {
"@context": "https://schema.org",
"@type": "HowTo",
name: "Dictate into the Vocalinux Dictation Pad",
step: howToSteps.map((step, index) => ({
"@type": "HowToStep",
position: index + 1,
name: step.name,
text: step.text,
})),
};
const faqJsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((item) => ({
"@type": "Question",
name: item.q,
acceptedAnswer: { "@type": "Answer", text: item.a },
})),
};
return (
<SeoSubpageShell>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(howToJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<section>
<p className="subpage-kicker">Added in v0.18.0</p>
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
Dictation Pad: voice typing that skips text injection
</h1>
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
The Dictation Pad is a small Vocalinux window with an editable text
box. When pad capture is on, everything you dictate lands there
instead of being typed into another app. You copy the text out
yourself. Because nothing is injected into other windows, the
compositor rules that make keystroke injection fragile on Wayland
never come into play.
</p>
</section>
<section className="mb-12 grid gap-4 sm:grid-cols-3">
<div className="rounded-[12px] border border-border bg-background p-5">
<Monitor className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h3 className="mb-2 font-semibold">Who it is for</h3>
<p className="text-sm text-muted-foreground">
Wayland users whose compositor blocks or garbles injected text, and
anyone who wants to read and fix a draft before it reaches the real
app.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Copy className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h3 className="mb-2 font-semibold">How text gets out</h3>
<p className="text-sm text-muted-foreground">
Copy All puts the whole pad on the clipboard. You can also select
part of the text and copy it like any other text box.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Keyboard className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h3 className="mb-2 font-semibold">What stays the same</h3>
<p className="text-sm text-muted-foreground">
Your shortcut, engine, transcript corrections and post-processing
script all work as before. Only the destination of the text changes.
</p>
</div>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
Why the Dictation Pad exists
</h2>
<p className="mb-4 text-muted-foreground">
On X11, Vocalinux types into the focused window with xdotool. Wayland
deliberately stops apps from sending keystrokes to each other, so
Vocalinux has to go through IBus, the RemoteDesktop portal, ydotool or
wtype, and which of those works depends on your desktop. Most setups
are covered (see the <Link href="/wayland/">Wayland guide</Link>), but
some compositor and app combinations still drop or mangle text.
</p>
<p className="text-muted-foreground">
The pad is the explicit fallback for those cases. Dictation is written
straight into a Vocalinux window, so there is no injection step left
to fail. Text bound for the pad also lands there even if you switched
to another app while it was being transcribed.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
How to use it
</h2>
<ol className="space-y-4">
{howToSteps.map((step, index) => (
<li
key={step.name}
className="rounded-[12px] border border-border bg-background p-5"
>
<div className="mb-2 flex items-center gap-3">
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
{index + 1}
</span>
<h3 className="font-semibold">{step.name}</h3>
</div>
<p className="ml-10 text-sm text-muted-foreground">{step.text}</p>
</li>
))}
</ol>
<p className="mt-4 text-sm text-muted-foreground">
In v0.18.4 the switch moved to Settings → Dictation, next to the Open
Dictation Pad button. On older 0.18 releases, update first; the{" "}
<Link href="/changelog/">changelog</Link> lists what changed in each
patch.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Settings it touches
</h2>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th className="pb-3 pr-4 font-semibold">Setting</th>
<th className="pb-3 pr-4 font-semibold">Where</th>
<th className="pb-3 pr-4 font-semibold">config.json key</th>
<th className="pb-3 font-semibold">What it does</th>
</tr>
</thead>
<tbody>
{settingsRows.map((row) => (
<tr key={row.label} className="border-b border-border">
<td className="py-3 pr-4 font-medium">{row.label}</td>
<td className="py-3 pr-4 text-muted-foreground">
{row.where}
</td>
<td className="py-3 pr-4">
<code>{row.key}</code>
</td>
<td className="py-3 text-muted-foreground">{row.note}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mt-4 text-sm text-muted-foreground">
The switch is stored in <code>~/.config/vocalinux/config.json</code>.
Flipping it in Settings applies to the next dictated segment without a
restart. The saved value looks like this:
</p>
<pre className="subpage-terminal mt-3 overflow-x-auto p-4">
<code>{`{
"text_injection": {
"dictate_to_pad": true
}
}`}</code>
</pre>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
Working inside the pad
</h2>
<h3 className="mb-2 font-semibold">Editing by hand and by voice</h3>
<p className="mb-4 text-muted-foreground">
The text box is editable, so you can fix a word with the keyboard
between dictations. With Voice Commands on, spoken editing commands
target the pad while capture is on: <em>delete that</em> removes the
last dictated segment, <em>undo</em> and <em>redo</em> step through
the pad&apos;s own history, and <em>select all</em>,{" "}
<em>select line</em>, <em>select word</em>, <em>select paragraph</em>,{" "}
<em>cut</em>, <em>copy</em> and <em>paste</em> work on the pad&apos;s
text. They never reach the app that happens to have focus. The full
command list is on the{" "}
<Link href="/shortcuts/">voice commands page</Link>.
</p>
<h3 className="mb-2 font-semibold">
Recent snippets as a second safety net
</h3>
<p className="text-muted-foreground">
The tray&apos;s Recent Snippets menu keeps your last dictations (10 by
default) in memory. Clicking one copies it to the clipboard, which
helps when a single segment went to the wrong place.
</p>
</section>
<section className="bg-primary/5 mb-12 rounded-[12px] border border-border p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Limitations and gotchas
</h2>
<ul className="space-y-3 text-sm text-muted-foreground">
<li className="flex items-start gap-2">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary"
aria-hidden="true"
/>
<span>
<strong className="text-foreground">
Copying out is manual.
</strong>{" "}
The pad never pastes for you. That extra step is the trade for not
depending on injection.
</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary"
aria-hidden="true"
/>
<span>
<strong className="text-foreground">Session only.</strong> Text
survives closing the window, not quitting the app.
</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary"
aria-hidden="true"
/>
<span>
<strong className="text-foreground">
No always-on-top on Wayland.
</strong>{" "}
A covered, idle pad can be shelved by the compositor. Reopen it
from the tray. v0.18.1 fixed the pad ghosting away, and v0.18.5
stopped it travelling between workspaces with Settings.
</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary"
aria-hidden="true"
/>
<span>
<strong className="text-foreground">
The shortcut still has to fire.
</strong>{" "}
The pad solves text output, not hotkeys. On Wayland the built-in
listener needs access to <code>/dev/input</code>, or you can turn
on External Activation (Desktop Shortcut) and bind{" "}
<code>vocalinux --toggle</code> to a desktop shortcut (toggle
only, no push-to-talk). See{" "}
<Link href="/global-shortcuts/">global shortcuts</Link> and{" "}
<Link href="/hyprland-sway/">Hyprland and sway</Link>.
</span>
</li>
</ul>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">FAQ</h2>
<div className="space-y-4">
{faqs.map((item) => (
<div
key={item.q}
className="rounded-[12px] border border-border bg-background p-5"
>
<h3 className="mb-2 font-semibold">{item.q}</h3>
<p className="text-sm text-muted-foreground">{item.a}</p>
</div>
))}
</div>
</section>
<section className="rounded-[12px] border border-border bg-muted p-8">
<h2 className="mb-4 font-display text-2xl font-semibold">
Try the Dictation Pad
</h2>
<p className="mb-6 text-muted-foreground">
Install Vocalinux, open Settings → Dictation and switch on Dictate
into Dictation Pad. If injection already works on your desktop, the{" "}
<Link href="/troubleshooting/">troubleshooting guide</Link> covers the
other paths.
</p>
<div className="flex flex-wrap gap-4">
<Link
href="/install/"
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
>
Install Vocalinux
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Link>
<Link
href="/wayland/"
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted"
>
Wayland guide
</Link>
</div>
</section>
</SeoSubpageShell>
);
}
+617
View File
@@ -0,0 +1,617 @@
import Link from "next/link";
import { type Metadata } from "next";
import {
CheckCircle2,
ChevronRight,
Cpu,
Download,
Lock,
XCircle,
Zap,
} from "lucide-react";
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
const installCommands = [
"curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh -o /tmp/vl.sh",
"bash /tmp/vl.sh --auto --engine=faster_whisper",
];
const setupSteps = [
{
title: "Install the Faster Whisper engine",
text: "Faster Whisper is an optional extra. Run the installer and choose Faster-Whisper in the engine menu, or pass --engine=faster_whisper. If the faster-whisper package fails to install, the installer falls back to whisper.cpp and says so.",
},
{
title: "Open Settings → Speech Model",
text: "Open Settings and go to Speech Model. Set your Language, then expand Advanced to see the Engine and Model size pickers.",
},
{
title: "Pick Faster Whisper and a size",
text: "Set Engine to Faster Whisper and choose a Model size. Settings suggests one for your hardware. Picking only selects the model; nothing downloads yet.",
},
{
title: "Confirm the download",
text: "If the model is not on disk, click Download and confirm. Every file is checked against a pinned checksum before the engine loads it.",
},
{
title: "Dictate",
text: "Hold your dictation shortcut (Right Alt by default on new installs), speak, and release. Text is typed into the focused app.",
},
];
const sizes = [
{
size: "Tiny",
ids: "tiny, tiny.en",
params: "39M",
desc: "Fastest, lowest accuracy",
},
{
size: "Base",
ids: "base, base.en",
params: "74M",
desc: "Fast, good for basic use",
},
{
size: "Small",
ids: "small, small.en",
params: "244M",
desc: "Balanced speed and accuracy",
},
{
size: "Medium",
ids: "medium, medium.en",
params: "769M",
desc: "High accuracy, slower",
},
{
size: "Large v3",
ids: "large-v3",
params: "1550M",
desc: "Highest accuracy, slower",
},
];
const packages = [
{
name: "install.sh / source install",
included: true,
note: "Menu option or --engine=faster_whisper",
},
{
name: "AUR (vocalinux)",
included: true,
note: "Listed as an optional dependency: python-faster-whisper",
},
{
name: "AppImage (and AUR vocalinux-bin)",
included: false,
note: "Ships whisper.cpp only",
},
{ name: "Flatpak", included: false, note: "Ships whisper.cpp only" },
{ name: "Snap", included: false, note: "Ships whisper.cpp only" },
{
name: ".deb / .rpm",
included: false,
note: "Thin packages; whisper.cpp on CPU",
},
{
name: "Nix flake",
included: false,
note: "faster_whisper extra is not packaged",
},
];
const choices = [
{
engine: "whisper.cpp",
pick: "The default. Ships in every package, uses a Vulkan GPU on AMD, Intel and NVIDIA, and offers quantized and English-only variants. Start here.",
},
{
engine: "Faster Whisper",
pick: "You installed with install.sh, from source or from the AUR and want a Whisper model on a CPU-only machine, running through CTranslate2 instead of whisper.cpp.",
},
{
engine: "Whisper (OpenAI)",
pick: "You already work with PyTorch and NVIDIA CUDA and accept a much larger install.",
},
{
engine: "VOSK",
pick: "Older or low-RAM machines. Small models are about 40 MB, with one model per language.",
},
];
const faqs = [
{
question: "Is Faster Whisper the same model as Whisper?",
answer:
"It runs the same Whisper model family converted to the CTranslate2 format. Vocalinux downloads those conversions from pinned Hugging Face revisions. The sizes and English-only variants match the Whisper names you already know.",
},
{
question: "Does Faster Whisper use my GPU?",
answer:
"On a typical install it runs on the CPU with int8. Vocalinux switches to CUDA with float16 only when PyTorch is installed and reports a CUDA GPU. The installer's Faster Whisper option does not install CUDA. For Vulkan GPU acceleration on AMD, Intel or NVIDIA, use whisper.cpp.",
},
{
question: "Can I change the compute type?",
answer:
"There is no setting for it. Vocalinux picks int8 on the CPU and float16 on CUDA automatically.",
},
{
question: "Does audio leave my machine?",
answer:
"No. Faster Whisper is a local engine. After the model download, recognition runs on your machine. Only the Remote API engine and VocaGateway send audio to an endpoint you configure.",
},
{
question: "Why is Faster Whisper not in my Engine list?",
answer:
"Settings lists only engines whose libraries are installed. The AppImage, Flatpak, Snap, .deb and .rpm packages do not include faster-whisper. Re-run the installer with --engine=faster_whisper, or on Arch install python-faster-whisper.",
},
];
export const metadata: Metadata = buildPageMetadata({
title: "Faster Whisper on Linux: Offline Voice Typing | Vocalinux",
description:
"Use Faster Whisper (CTranslate2, int8 on CPU) for offline voice typing on Linux with Vocalinux: install, model sizes, GPU and compute options, and fixes.",
path: "/faster-whisper",
keywords: [
"Faster Whisper Linux",
"faster-whisper dictation",
"CTranslate2 Whisper Linux",
"Faster Whisper vs whisper.cpp",
"offline Whisper CPU int8",
"Linux voice typing Whisper",
"faster-whisper model sizes",
],
});
export default function FasterWhisperPage() {
const jsonLd = [
{
"@context": "https://schema.org",
"@type": "TechArticle",
headline: "Faster Whisper on Linux",
description:
"How to install and use the Faster Whisper speech engine in Vocalinux on Linux, including model sizes, compute options, package availability and troubleshooting.",
dateModified: "2026-11-01",
author: {
"@type": "Person",
name: "Jatin K Malik",
url: "https://github.com/jatinkrmalik",
},
publisher: {
"@type": "Organization",
name: "Vocalinux",
logo: {
"@type": "ImageObject",
url: absoluteUrl("/vocalinux.png"),
},
},
mainEntityOfPage: absoluteUrl("/faster-whisper"),
},
{
"@context": "https://schema.org",
"@type": "HowTo",
name: "Set up the Faster Whisper engine in Vocalinux",
step: setupSteps.map((step, index) => ({
"@type": "HowToStep",
position: index + 1,
name: step.title,
text: step.text,
})),
},
{
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: { "@type": "Answer", text: faq.answer },
})),
},
];
return (
<SeoSubpageShell>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<section>
<p className="subpage-kicker">
<Zap className="h-4 w-4" aria-hidden="true" />
Added in v0.17.0
</p>
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
Faster Whisper on Linux
</h1>
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
Faster Whisper is an optional local speech engine in Vocalinux. It
runs Whisper models through CTranslate2 with int8 quantization on your
CPU and types what you say into the focused app. It is for people who
want Whisper models and the full language catalog on a machine without
a GPU, using a different runtime from the default whisper.cpp.
</p>
</section>
<section className="mb-12 grid gap-4 sm:grid-cols-3">
<div className="rounded-[12px] border border-border bg-background p-5">
<Cpu className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<p className="mb-2 font-semibold text-foreground">CPU first</p>
<p className="text-sm text-muted-foreground">
int8 on the CPU by default. No PyTorch needed for the CPU path.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Lock className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<p className="mb-2 font-semibold text-foreground">On-device</p>
<p className="text-sm text-muted-foreground">
After the one-time model download, recognition runs locally. Audio
stays on your machine.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Download className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<p className="mb-2 font-semibold text-foreground">Pinned models</p>
<p className="text-sm text-muted-foreground">
Models load only from checksum-verified local files, never from an
unpinned upstream revision.
</p>
</div>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
What is Faster Whisper?
</h2>
<div className="max-w-4xl space-y-4 text-muted-foreground">
<p>
<a
href="https://github.com/SYSTRAN/faster-whisper"
className="text-primary underline"
rel="noopener noreferrer"
>
faster-whisper
</a>{" "}
is a reimplementation of OpenAI&apos;s Whisper on top of
CTranslate2, an inference engine for transformer models. It runs
converted Whisper models and supports reduced precision such as
int8, which lowers memory use on the CPU.
</p>
<p>
Vocalinux added Faster Whisper as an optional engine in v0.17.0,
together with Parakeet. whisper.cpp remains the default. Vocalinux
stores Faster Whisper models under{" "}
<code className="rounded bg-muted px-1">
~/.local/share/vocalinux/models/
</code>{" "}
and checks each file against a pinned checksum before loading it.
</p>
</div>
</section>
<section className="mb-12">
<h2 className="mb-6 font-display text-2xl font-semibold">
Install and select Faster Whisper
</h2>
<div className="mb-6 rounded-lg bg-[color:var(--terminal)] p-4">
{installCommands.map((command) => (
<code
key={command}
className="block text-sm text-[color:var(--terminal-fg)]"
>
{command}
</code>
))}
</div>
<ol className="space-y-4">
{setupSteps.map((step, index) => (
<li
key={step.title}
className="rounded-[12px] border border-border bg-background p-5"
>
<div className="mb-2 flex items-center gap-3">
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
{index + 1}
</span>
<h3 className="font-semibold">{step.title}</h3>
</div>
<p className="ml-10 text-sm text-muted-foreground">{step.text}</p>
</li>
))}
</ol>
<p className="mt-4 text-sm text-muted-foreground">
To start once with Faster Whisper from a terminal, run{" "}
<code className="rounded bg-muted px-1">
vocalinux --engine faster_whisper
</code>
.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Model sizes
</h2>
<p className="mb-4 text-sm text-muted-foreground">
The Model size picker offers these sizes. The <code>.en</code>{" "}
variants are English only; if your language is English, Settings
suggests the <code>.en</code> variant of the recommended size. There
is no <code>large-v3.en</code>.
</p>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th className="pb-3 pr-4 font-semibold">Size</th>
<th className="pb-3 pr-4 font-semibold">Model IDs</th>
<th className="pb-3 pr-4 font-semibold">Parameters</th>
<th className="pb-3 font-semibold">Description</th>
</tr>
</thead>
<tbody>
{sizes.map((row) => (
<tr key={row.size} className="border-b border-border">
<td className="py-3 pr-4 font-medium">{row.size}</td>
<td className="py-3 pr-4 font-mono text-xs">{row.ids}</td>
<td className="py-3 pr-4 text-muted-foreground">
{row.params}
</td>
<td className="py-3 text-muted-foreground">{row.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="mb-2 mt-6 font-semibold">
Which size Settings suggests
</h3>
<ul className="list-disc space-y-1 pl-5 text-sm text-muted-foreground">
<li>CPU only, under 16 GB RAM: Tiny</li>
<li>CPU only, 16 GB RAM or more: Base</li>
<li>CUDA GPU detected, under 8 GB RAM: Base</li>
<li>CUDA GPU detected, 8 GB RAM or more: Small</li>
</ul>
<p className="mt-3 text-sm text-muted-foreground">
The suggestion never changes your model or starts a download. If
dictation feels slow, go one size down; if it makes too many mistakes
and speed is fine, go one size up. The{" "}
<Link href="/whisper-model-guide/" className="text-primary underline">
Whisper model guide
</Link>{" "}
explains the size trade-offs in more depth.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
Compute options and GPUs
</h2>
<div className="max-w-4xl space-y-4 text-muted-foreground">
<p>
Vocalinux chooses the device and precision for you; there is no
compute-type setting. On the CPU it loads the model with{" "}
<code className="rounded bg-muted px-1">int8</code>. If PyTorch is
installed and reports a CUDA GPU, it loads the model on CUDA with{" "}
<code className="rounded bg-muted px-1">float16</code> instead.
</p>
<p>
The installer&apos;s Faster Whisper option is the CPU CTranslate2
path and does not install the CUDA toolkit. If you want GPU
dictation on AMD, Intel or NVIDIA, whisper.cpp with Vulkan is the
supported route; see{" "}
<Link href="/gpu-acceleration/" className="text-primary underline">
GPU acceleration
</Link>
.
</p>
</div>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">Languages</h2>
<p className="max-w-4xl text-muted-foreground">
Faster Whisper uses the same language catalog as the other Whisper
engines, including Auto-detect. It also supports{" "}
<strong>Follow keyboard layout</strong>, which switches the dictation
language with your active layout, and custom dictionary terms that
bias recognition toward names and jargon. See{" "}
<Link href="/languages/" className="text-primary underline">
languages by engine
</Link>
,{" "}
<Link
href="/multilingual-dictation/"
className="text-primary underline"
>
multilingual dictation
</Link>{" "}
and{" "}
<Link href="/custom-dictionary/" className="text-primary underline">
custom dictionary
</Link>
.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Package availability
</h2>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th className="pb-3 pr-4 font-semibold">Install method</th>
<th className="pb-3 pr-4 font-semibold">Faster Whisper</th>
<th className="pb-3 font-semibold">Notes</th>
</tr>
</thead>
<tbody>
{packages.map((pkg) => (
<tr key={pkg.name} className="border-b border-border">
<td className="py-3 pr-4 font-medium">{pkg.name}</td>
<td className="py-3 pr-4">
{pkg.included ? (
<span className="inline-flex items-center gap-1.5 text-primary">
<CheckCircle2 className="h-4 w-4" aria-hidden="true" />
Yes
</span>
) : (
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
<XCircle className="h-4 w-4" aria-hidden="true" />
No
</span>
)}
</td>
<td className="py-3 text-muted-foreground">{pkg.note}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
Faster Whisper vs whisper.cpp, Whisper and VOSK
</h2>
<p className="mb-4 max-w-4xl text-muted-foreground">
All four are local engines. Choose by hardware and by how you
installed Vocalinux. For the full table, see the{" "}
<Link href="/compare/" className="text-primary underline">
engine comparison
</Link>{" "}
or{" "}
<Link href="/parakeet/" className="text-primary underline">
Parakeet
</Link>{" "}
for the other CPU engine added in v0.17.0.
</p>
<div className="overflow-x-auto rounded-[12px] border border-border bg-background p-6">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th className="pb-3 pr-4 font-semibold">Engine</th>
<th className="pb-3 font-semibold">Pick it when</th>
</tr>
</thead>
<tbody>
{choices.map((row) => (
<tr key={row.engine} className="border-b border-border">
<th scope="row" className="py-3 pr-4 font-medium">
{row.engine}
</th>
<td className="py-3 text-muted-foreground">{row.pick}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Troubleshooting
</h2>
<div className="space-y-5 text-sm text-muted-foreground">
<div>
<h3 className="mb-1 font-semibold text-foreground">
The installer finished, but whisper.cpp is selected
</h3>
<p>
If faster-whisper fails to install or import, the installer falls
back to whisper.cpp. Read the installer output, then re-run it
with{" "}
<code className="rounded bg-muted px-1">
--engine=faster_whisper
</code>
.
</p>
</div>
<div>
<h3 className="mb-1 font-semibold text-foreground">
Another language is not recognized
</h3>
<p>
The <code>.en</code> models are English only. Pick the size
without <code>.en</code>, or set Language to Auto-detect with a
multilingual model.
</p>
</div>
<div>
<h3 className="mb-1 font-semibold text-foreground">
Dictation feels slow
</h3>
<p>
Go one size down. On CPU-only machines, Tiny or Base is the usual
starting point. Larger sizes take longer per sentence on a CPU.
</p>
</div>
<div>
<h3 className="mb-1 font-semibold text-foreground">
Anything else
</h3>
<p>
Run{" "}
<code className="rounded bg-muted px-1">vocalinux --debug</code>{" "}
and check{" "}
<code className="rounded bg-muted px-1">
~/.local/share/vocalinux/vocalinux.log
</code>
, then see the{" "}
<Link href="/troubleshooting/" className="text-primary underline">
troubleshooting guide
</Link>
.
</p>
</div>
</div>
</section>
<section className="mb-12">
<h2 className="mb-6 font-display text-2xl font-semibold">
Faster Whisper FAQ
</h2>
<div className="space-y-4">
{faqs.map((faq) => (
<div
key={faq.question}
className="rounded-[12px] border border-border bg-background p-5"
>
<h3 className="mb-2 font-semibold">{faq.question}</h3>
<p className="text-sm text-muted-foreground">{faq.answer}</p>
</div>
))}
</div>
</section>
<section className="rounded-[12px] border border-border bg-muted p-8">
<h2 className="mb-4 font-display text-2xl font-semibold">
Try Faster Whisper
</h2>
<p className="mb-6 text-muted-foreground">
Install Vocalinux with the Faster Whisper engine, or keep whisper.cpp
and switch later from Settings → Speech Model.
</p>
<div className="flex flex-wrap gap-4">
<Link
href="/install/"
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
>
Install Vocalinux
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Link>
<Link
href="/whisper-model-guide/"
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted"
>
Whisper model guide
</Link>
</div>
</section>
</SeoSubpageShell>
);
}
+482
View File
@@ -0,0 +1,482 @@
import Link from "next/link";
import { type Metadata } from "next";
import {
AlertTriangle,
CheckCircle2,
ChevronRight,
CircleHelp,
Download,
Keyboard,
RefreshCw,
ShieldCheck,
XCircle,
} from "lucide-react";
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
const PAGE_PATH = "/flatpak";
const PAGE_TITLE = "Install Vocalinux via Flatpak (VocaHQ Remote) | Vocalinux";
const PAGE_DESCRIPTION =
"Install Vocalinux from the signed VocaHQ Flatpak remote (not Flathub): exact commands, updates, sandbox and Wayland notes, what is included, and uninstall.";
const FLATPAKREF_COMMAND =
"flatpak install https://vocahq.github.io/vocalinux-flatpak/com.vocalinux.Vocalinux.flatpakref";
const MANUAL_COMMANDS = `flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --if-not-exists vocahq https://vocahq.github.io/vocalinux-flatpak/vocahq.flatpakrepo
flatpak install vocahq com.vocalinux.Vocalinux`;
const DBUS_TOGGLE_COMMAND =
"gdbus call --session --dest com.vocalinux.Vocalinux --object-path /com/vocalinux/Vocalinux --method com.vocalinux.Vocalinux.Toggle";
const OVERRIDE_COMMAND =
"flatpak override --user --nodevice=all --device=dri com.vocalinux.Vocalinux";
const RUN_COMMAND = "flatpak run com.vocalinux.Vocalinux";
const installSteps = [
{
name: "Install from the VocaHQ remote",
text: `Run ${FLATPAKREF_COMMAND}. The .flatpakref adds the vocahq remote and resolves the GNOME runtime (org.gnome.Platform//50) through Flathub, so no separate Flathub setup is needed.`,
},
{
name: "Or add the remotes by hand",
text: "If you prefer explicit remotes, add flathub, add vocahq, then install com.vocalinux.Vocalinux from vocahq with the three commands below.",
},
{
name: "Start Vocalinux",
text: `Launch Vocalinux from your app menu or run ${RUN_COMMAND}.`,
},
{
name: "Download a speech model",
text: "Pick a model in Settings → Speech Model. Since v0.18.4 Vocalinux shows the download size and asks before downloading anything.",
},
];
const faqs = [
{
question: "Is Vocalinux on Flathub?",
answer:
"No. The Flathub submission was closed on policy grounds in July 2026 and the project is not re-submitting. The app comes from the self-hosted vocahq remote; only the GNOME runtime is pulled from Flathub.",
},
{
question: "Does the Flatpak update itself?",
answer:
"Flatpak updates it, not the app. Run flatpak update (or let your software center do it) and each stable release arrives as an incremental delta. Prerelease tags never reach the remote, and GitHub Release .flatpak bundles do not auto-update.",
},
{
question: "Can I use Parakeet or Faster Whisper in the Flatpak?",
answer:
"No. The Flatpak ships whisper.cpp only. Faster Whisper, Parakeet, OpenAI Whisper and VOSK are available through the install script or a source install.",
},
{
question: "My push-to-talk key does nothing on Wayland. Why?",
answer:
"On Wayland the built-in hotkey reads /dev/input. The manifest grants --device=all by default; if you removed device access with flatpak override, grant --device=input (Flatpak 1.15.6 or newer) or --device=all again, or enable External Activation and bind a desktop shortcut to the D-Bus Toggle command. Desktop shortcuts only toggle.",
},
{
question: "Can I start a local VocaGateway from the Flatpak?",
answer:
"No. The sandbox cannot reach the host podman or docker socket. Start VocaGateway on the host yourself and point Settings → Remote Server at it, or use the installer or AppImage. Either way, audio leaves Vocalinux for the endpoint you configured, and leaves the machine if that endpoint is elsewhere. VocaGateway is not on-device, so keep it on localhost, a trusted LAN, an encrypted private network such as Tailscale, or HTTPS.",
},
];
export const metadata: Metadata = buildPageMetadata({
title: PAGE_TITLE,
description: PAGE_DESCRIPTION,
path: PAGE_PATH,
keywords: [
"vocalinux flatpak",
"linux voice dictation flatpak",
"flatpak speech to text",
"vocahq flatpak remote",
"com.vocalinux.Vocalinux",
"flatpak dictation wayland",
],
});
function Commands({ children }: { children: string }) {
return (
<div className="subpage-terminal mt-3 p-4">
<pre>
<code>{children}</code>
</pre>
</div>
);
}
export default function FlatpakPage() {
const articleJsonLd = {
"@context": "https://schema.org",
"@type": "TechArticle",
headline: "Install Vocalinux with Flatpak from the VocaHQ remote",
description: PAGE_DESCRIPTION,
dateModified: "2026-11-01",
author: {
"@type": "Person",
name: "Jatin K Malik",
url: "https://github.com/jatinkrmalik",
},
publisher: {
"@type": "Organization",
name: "Vocalinux",
logo: {
"@type": "ImageObject",
url: absoluteUrl("/vocalinux.png"),
},
},
mainEntityOfPage: absoluteUrl(PAGE_PATH),
};
const howToJsonLd = {
"@context": "https://schema.org",
"@type": "HowTo",
name: "Install Vocalinux from the VocaHQ Flatpak remote",
description: PAGE_DESCRIPTION,
tool: [{ "@type": "HowToTool", name: "Flatpak" }],
step: installSteps.map((step) => ({
"@type": "HowToStep",
name: step.name,
text: step.text,
url: absoluteUrl(PAGE_PATH),
})),
};
const faqJsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: { "@type": "Answer", text: faq.answer },
})),
};
return (
<SeoSubpageShell>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(howToJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<section>
<p className="subpage-kicker">Flatpak remote added in v0.18.0</p>
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
Install Vocalinux with Flatpak
</h1>
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
Vocalinux publishes a signed, self-hosted Flatpak remote called{" "}
<code>vocahq</code> on every stable release. It is{" "}
<strong>not on Flathub</strong>: the app comes from VocaHQ, and only
the GNOME runtime comes from Flathub. Install once, and{" "}
<code>flatpak update</code> picks up each new release.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Who the Flatpak is for
</h2>
<p className="mb-3 text-muted-foreground">
Pick the Flatpak if your distribution already uses Flatpak, if you run
an immutable or non-standard layout, or if you want updates handled by
the same tool as your other desktop apps. It runs on x86_64 and
aarch64, and it ships the default whisper.cpp engine with Vulkan GPU
support.
</p>
<p className="text-muted-foreground">
Pick the <Link href="/install/">install script</Link> instead if you
want Faster Whisper, Parakeet, OpenAI Whisper or VOSK, or the local
VocaGateway launcher. Those are not in the Flatpak.
</p>
</section>
<section className="mb-12">
<h2 className="mb-6 inline-flex items-center gap-2 font-display text-2xl font-semibold">
<Download className="h-5 w-5 text-primary" aria-hidden="true" />
Install from the VocaHQ remote
</h2>
<ol className="space-y-6">
<li className="rounded-[12px] border border-border bg-background p-5">
<h3 className="font-semibold">1. {installSteps[0].name}</h3>
<p className="mt-2 text-sm text-muted-foreground">
One command. The <code>.flatpakref</code> adds the{" "}
<code>vocahq</code> remote and resolves the GNOME runtime (
<code>org.gnome.Platform//50</code>) through Flathub, so no
separate Flathub setup is needed.
</p>
<Commands>{FLATPAKREF_COMMAND}</Commands>
</li>
<li className="rounded-[12px] border border-border bg-background p-5">
<h3 className="font-semibold">2. {installSteps[1].name}</h3>
<p className="mt-2 text-sm text-muted-foreground">
If you prefer explicit remotes, add flathub, add vocahq, then
install <code>com.vocalinux.Vocalinux</code> from vocahq:
</p>
<Commands>{MANUAL_COMMANDS}</Commands>
</li>
<li className="rounded-[12px] border border-border bg-background p-5">
<h3 className="font-semibold">3. {installSteps[2].name}</h3>
<p className="mt-2 text-sm text-muted-foreground">
Launch Vocalinux from your app menu, or run:
</p>
<Commands>{RUN_COMMAND}</Commands>
</li>
<li className="rounded-[12px] border border-border bg-background p-5">
<h3 className="font-semibold">4. {installSteps[3].name}</h3>
<p className="mt-2 text-sm text-muted-foreground">
Pick a model in Settings → Speech Model. Since v0.18.4 Vocalinux
shows the download size and asks before downloading anything.
After that one download, the whisper.cpp engine transcribes on
your machine. Then hold Right Alt and speak.
</p>
</li>
</ol>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 inline-flex items-center gap-2 font-display text-2xl font-semibold">
<RefreshCw className="h-5 w-5 text-primary" aria-hidden="true" />
Updates
</h2>
<p className="mb-3 text-muted-foreground">
Every stable <code>v*</code> tag is imported into the remote, signed
with the VocaHQ GPG key and published with static deltas, so an update
downloads only what changed:
</p>
<Commands>flatpak update</Commands>
<ul className="mt-4 space-y-2 text-sm text-muted-foreground">
<li>
Alpha, beta and rc tags never reach the remote. The only Flatpak for
a prerelease is the <code>.flatpak</code> bundle on GitHub Releases.
</li>
<li>
Release bundles (
<code>Vocalinux-&lt;version&gt;-x86_64.flatpak</code> or{" "}
<code>-aarch64.flatpak</code>) do not auto-update. Switch to the
remote if you installed one.
</li>
<li>
The in-app update checker only tells you a release exists; it never
installs anything.
</li>
</ul>
<h3 className="mb-2 mt-6 font-semibold">
If an update fails signature verification
</h3>
<p className="text-sm text-muted-foreground">
After a signing-key rotation, existing installs must import the new
public key. Re-running <code>remote-add</code> does not replace it.
The documented fix:
</p>
<Commands>{`curl -fsSL https://vocahq.github.io/vocalinux-flatpak/vocahq.gpg -o /tmp/vocahq.gpg
flatpak remote-modify --gpg-import=/tmp/vocahq.gpg vocahq
flatpak update`}</Commands>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
What the Flatpak includes and leaves out
</h2>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th className="pb-3 pr-4 font-semibold">Feature</th>
<th className="pb-3 pr-4 font-semibold">In the Flatpak?</th>
<th className="pb-3 font-semibold">Notes</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-border">
<td className="py-3 pr-4 font-medium">whisper.cpp engine</td>
<td className="py-3 pr-4">Yes</td>
<td className="py-3 text-muted-foreground">
Built with Vulkan; the GPU is reached through{" "}
<code>--device=dri</code>
</td>
</tr>
<tr className="border-b border-border">
<td className="py-3 pr-4 font-medium">Silero neural VAD</td>
<td className="py-3 pr-4">Yes</td>
<td className="py-3 text-muted-foreground">
onnxruntime bundled since v0.18.4
</td>
</tr>
<tr className="border-b border-border">
<td className="py-3 pr-4 font-medium">Remote API engine</td>
<td className="py-3 pr-4">Yes</td>
<td className="py-3 text-muted-foreground">
Audio leaves the machine for the endpoint you configure
</td>
</tr>
<tr className="border-b border-border">
<td className="py-3 pr-4 font-medium">
Faster Whisper, Parakeet, OpenAI Whisper, VOSK
</td>
<td className="py-3 pr-4">No</td>
<td className="py-3 text-muted-foreground">
Use the install script or a source install
</td>
</tr>
<tr className="border-b border-border">
<td className="py-3 pr-4 font-medium">
Local VocaGateway launcher
</td>
<td className="py-3 pr-4">No</td>
<td className="py-3 text-muted-foreground">
The sandbox cannot reach the host podman or docker socket
</td>
</tr>
</tbody>
</table>
</div>
<p className="mt-4 text-sm text-muted-foreground">
Config and models live under{" "}
<code>~/.var/app/com.vocalinux.Vocalinux/</code>, not{" "}
<code>~/.config/vocalinux</code>.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 inline-flex items-center gap-2 font-display text-2xl font-semibold">
<Keyboard className="h-5 w-5 text-primary" aria-hidden="true" />
Sandbox permissions and Wayland
</h2>
<p className="mb-3 text-muted-foreground">
The manifest asks for the microphone (<code>--socket=pulseaudio</code>
), the GPU (<code>--device=dri</code>), network for model downloads,
the Wayland and X11 sockets, IBus and the tray watcher on D-Bus, and{" "}
<code>--device=all</code> for keyboard devices and{" "}
<code>/dev/uinput</code>. Flatpak has no narrower flag for uinput.
</p>
<h3 className="mb-2 mt-6 font-semibold">Hotkeys</h3>
<ul className="space-y-2 text-sm text-muted-foreground">
<li>
Native X11: since v0.18.4 the Flatpak uses the X11 listener, so the
hotkey needs no device access. GNOME on X11 is the exception and
still needs <code>/dev/input</code>.
</li>
<li>
Wayland: the built-in hotkey reads <code>/dev/input</code>. If you
tighten permissions, grant <code>--device=input</code> on Flatpak
1.15.6 or newer, or <code>--device=all</code> on older versions.
</li>
<li>
No device access at all: enable Settings → Shortcuts → External
Activation (Desktop Shortcut) and bind a desktop shortcut to the
D-Bus Toggle command below. Desktop shortcuts toggle dictation; they
cannot do push-to-talk.
</li>
</ul>
<Commands>{DBUS_TOGGLE_COMMAND}</Commands>
<p className="mt-3 text-sm text-muted-foreground">
To drop device access but keep the GPU for whisper.cpp:
</p>
<Commands>{OVERRIDE_COMMAND}</Commands>
<h3 className="mb-2 mt-6 font-semibold">Typing text</h3>
<p className="text-sm text-muted-foreground">
Text injection prefers the RemoteDesktop portal, which asks for
permission once and handles non-US layouts. When the portal is
unavailable it falls back to <code>wl-copy</code> plus ydotool paste,
then xdotool for X11 and XWayland clients. Without{" "}
<code>/dev/uinput</code> the ydotool fallback is gone, but the portal
and xdotool remain. On KDE Plasma Wayland, set System Settings →
Keyboard → Virtual Keyboard to &ldquo;IBus Wayland&rdquo;. On GNOME,
the tray icon needs the AppIndicator extension. For more, see{" "}
<Link href="/wayland/">Wayland voice typing</Link> and{" "}
<Link href="/global-shortcuts/">global shortcuts via D-Bus</Link>.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 inline-flex items-center gap-2 font-display text-2xl font-semibold">
<XCircle className="h-5 w-5 text-primary" aria-hidden="true" />
Uninstall
</h2>
<p className="mb-3 text-muted-foreground">
The <code>uninstall.sh</code> script is for script and source
installs. For the Flatpak, use Flatpak itself. Remove the app:
</p>
<Commands>flatpak uninstall com.vocalinux.Vocalinux</Commands>
<p className="mt-3 text-sm text-muted-foreground">
Add <code>--delete-data</code> to also remove settings and downloaded
models under <code>~/.var/app/com.vocalinux.Vocalinux/</code>. If no
other app uses the remote, drop it too:
</p>
<Commands>flatpak remote-delete vocahq</Commands>
</section>
<section className="mb-12">
<h2 className="mb-6 inline-flex items-center gap-2 font-display text-2xl font-semibold">
<CircleHelp className="h-5 w-5 text-primary" aria-hidden="true" />
Flatpak FAQ
</h2>
<div className="space-y-4">
{faqs.map((faq) => (
<div
key={faq.question}
className="rounded-[12px] border border-border bg-background p-5"
>
<h3 className="mb-2 font-semibold">{faq.question}</h3>
<p className="text-sm text-muted-foreground">{faq.answer}</p>
</div>
))}
</div>
<p className="mt-6 inline-flex items-start gap-2 text-sm text-muted-foreground">
<AlertTriangle
className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary"
aria-hidden="true"
/>
<span>
More fixes for hotkeys, audio and text injection are on the{" "}
<Link href="/troubleshooting/">troubleshooting page</Link>. To keep
the whole stack local, see the{" "}
<Link href="/gpu-acceleration/">GPU acceleration guide</Link> and{" "}
<Link href="/voice-activity-detection/">Silero VAD</Link>.
</span>
</p>
</section>
<section className="rounded-[12px] border border-border bg-muted p-8">
<h2 className="mb-4 inline-flex items-center gap-2 font-display text-2xl font-semibold">
<ShieldCheck className="h-5 w-5 text-primary" aria-hidden="true" />
Need a different package?
</h2>
<p className="mb-6 text-muted-foreground">
The install script covers every engine, the AUR and Nix flake fit Arch
and NixOS, and the .deb and .rpm suit Debian, Ubuntu and Fedora.
</p>
<div className="flex flex-wrap gap-4">
<Link
href="/install/"
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
>
All install options
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Link>
<Link
href="/install/nixos/"
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted"
>
Nix flake and NixOS module
</Link>
<Link
href="/local-gateway/"
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted"
>
Local VocaGateway
</Link>
</div>
</section>
</SeoSubpageShell>
);
}
+460
View File
@@ -0,0 +1,460 @@
import Link from "next/link";
import { type Metadata } from "next";
import type { ReactNode } from "react";
import {
AlertTriangle,
ChevronRight,
Command,
Keyboard,
ShieldCheck,
} from "lucide-react";
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
import { TerminalBlock } from "@/components/terminal-block";
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
const PAGE_PATH = "/global-shortcuts";
const PAGE_TITLE = "Global Shortcuts for Voice Dictation via D-Bus";
const PAGE_DESCRIPTION =
"Start and stop Vocalinux dictation from GNOME, KDE, sway or Hyprland shortcuts and scripts using its D-Bus service and the --toggle, --start and --stop flags.";
const GDBUS_TOGGLE =
"gdbus call --session --dest com.vocalinux.Vocalinux --object-path /com/vocalinux/Vocalinux --method com.vocalinux.Vocalinux.Toggle";
const dbusRows = [
{ item: "Bus", value: "Session bus" },
{ item: "Bus name", value: "com.vocalinux.Vocalinux" },
{ item: "Object path", value: "/com/vocalinux/Vocalinux" },
{ item: "Interface", value: "com.vocalinux.Vocalinux" },
{ item: "Methods", value: "Toggle, Start, Stop (no arguments)" },
];
const commandRows = [
{
cli: "vocalinux --toggle",
method: "Toggle",
effect: "Start if idle, stop if active",
},
{
cli: "vocalinux --start",
method: "Start",
effect: "Start voice typing; does nothing if already running",
},
{
cli: "vocalinux --stop",
method: "Stop",
effect: "Stop voice typing; does nothing if already idle",
},
];
const steps = [
{
name: "Turn on External Activation",
text: "Open Settings → Shortcuts (the Keyboard Shortcuts group on the Dictation page) and turn on External Activation (Desktop Shortcut). It applies immediately and the built-in key listener stops.",
},
{
name: "Pick the command",
text: "On a native install, use vocalinux --toggle. On Flatpak, use the gdbus call shown below, because vocalinux --toggle is not on the host PATH.",
},
{
name: "Bind it in your desktop",
text: "Create a custom shortcut in GNOME or KDE, or a bind line in your sway or Hyprland config, that runs that command.",
},
{
name: "Test it",
text: "Focus a text field, press the shortcut, speak, then press it again to stop.",
},
];
const faqs = [
{
question: "Can a desktop shortcut do push-to-talk?",
answer:
"No. A desktop shortcut only sends the key press and never the release, so it can toggle dictation but cannot hold-to-talk. Use the built-in listener if you need push-to-talk.",
},
{
question: "Will vocalinux --toggle launch Vocalinux if it is not running?",
answer:
"No. It forwards the command to a running instance over D-Bus and exits. If no instance answers, it logs that it could not reach a running Vocalinux and exits with status 1.",
},
{
question: "Do I still need the input group?",
answer:
"Not for starting and stopping dictation. With External Activation on, Vocalinux does not read /dev/input for the hotkey. Text injection is unaffected and keeps its own requirements.",
},
{
question: "What happens if the D-Bus service cannot start?",
answer:
"Vocalinux shows an External activation unavailable notification and falls back to the internal keyboard shortcut, so you are never left without a way to start or stop dictation.",
},
{
question: "Do per-language shortcuts work with External Activation?",
answer:
"No. Per-language shortcuts belong to the built-in listener, and the whole listener family stops when External Activation is on. Turn External Activation off to use them again.",
},
];
export const metadata: Metadata = buildPageMetadata({
title: `${PAGE_TITLE} | Vocalinux`,
description: PAGE_DESCRIPTION,
path: PAGE_PATH,
keywords: [
"linux dictation global shortcut",
"vocalinux d-bus",
"vocalinux --toggle",
"sway voice typing keybind",
"hyprland dictation keybind",
"kde global shortcut dictation",
"gnome custom shortcut voice typing",
],
});
function InlineCode({ children }: { children: ReactNode }) {
return (
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[0.85em] text-foreground">
{children}
</code>
);
}
function ConfigBlock({ label, children }: { label: string; children: string }) {
return (
<figure className="min-w-0">
<figcaption className="mb-1 text-sm font-medium">{label}</figcaption>
<pre className="subpage-terminal p-4 text-sm">
<code>{children}</code>
</pre>
</figure>
);
}
export default function GlobalShortcutsPage() {
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "TechArticle",
headline: PAGE_TITLE,
description: PAGE_DESCRIPTION,
dateModified: "2026-11-01",
author: {
"@type": "Person",
name: "Jatin K Malik",
url: "https://github.com/jatinkrmalik",
},
publisher: {
"@type": "Organization",
name: "Vocalinux",
logo: {
"@type": "ImageObject",
url: absoluteUrl("/vocalinux.png"),
},
},
mainEntityOfPage: absoluteUrl(PAGE_PATH),
},
{
"@type": "HowTo",
name: "Start Vocalinux dictation from a desktop shortcut",
step: steps.map((step, index) => ({
"@type": "HowToStep",
position: index + 1,
name: step.name,
text: step.text,
})),
},
{
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: { "@type": "Answer", text: faq.answer },
})),
},
],
};
return (
<SeoSubpageShell>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<section>
<p className="subpage-kicker">Added in v0.18.0</p>
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
Start dictation from desktop shortcuts and scripts
</h1>
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
Vocalinux can hand hotkey duty to your desktop. A running instance
answers on the session D-Bus, so a GNOME or KDE custom shortcut, a
sway or Hyprland bind, or any script can start and stop voice typing
without Vocalinux reading your keyboard.
</p>
</section>
<section className="mb-12 grid gap-4 md:grid-cols-3">
<div className="rounded-[12px] border border-border bg-background p-5">
<ShieldCheck
className="mb-3 h-6 w-6 text-primary"
aria-hidden="true"
/>
<h2 className="mb-2 font-semibold">No /dev/input access</h2>
<p className="text-sm text-muted-foreground">
On Wayland the built-in listener reads{" "}
<InlineCode>/dev/input</InlineCode>, which needs the{" "}
<InlineCode>input</InlineCode> group. A compositor shortcut avoids
that entirely.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Keyboard className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h2 className="mb-2 font-semibold">Toggle, not push-to-talk</h2>
<p className="text-sm text-muted-foreground">
Your desktop sends the key press only. Press once to start, press
again to stop.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Command className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h2 className="mb-2 font-semibold">Scriptable</h2>
<p className="text-sm text-muted-foreground">
Separate Start and Stop methods let scripts set a known state
instead of flipping it.
</p>
</div>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
The D-Bus interface
</h2>
<p className="mb-4 text-sm text-muted-foreground">
These names come from <InlineCode>dbus_service.py</InlineCode> in the
Vocalinux source.
</p>
<div className="mb-6 overflow-x-auto">
<table className="w-full text-left text-sm">
<tbody>
{dbusRows.map((row) => (
<tr key={row.item} className="border-b border-border">
<th scope="row" className="py-3 pr-4 font-medium">
{row.item}
</th>
<td className="py-3 font-mono text-muted-foreground">
{row.value}
</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="mb-2 font-semibold">CLI commands</h3>
<div className="mb-6 overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th scope="col" className="pb-3 pr-4 font-semibold">
Command
</th>
<th scope="col" className="pb-3 pr-4 font-semibold">
D-Bus method
</th>
<th scope="col" className="pb-3 font-semibold">
Effect
</th>
</tr>
</thead>
<tbody>
{commandRows.map((row) => (
<tr key={row.cli} className="border-b border-border">
<td className="py-3 pr-4 font-mono">{row.cli}</td>
<td className="py-3 pr-4 font-mono text-muted-foreground">
{row.method}
</td>
<td className="py-3 text-muted-foreground">{row.effect}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="mb-2 font-semibold">Calling D-Bus directly</h3>
<p className="mb-3 text-sm text-muted-foreground">
The <InlineCode>gdbus</InlineCode> form is what the Flatpak
documentation recommends: it is immediate and does not start another
sandbox on every press. Swap <InlineCode>Toggle</InlineCode> for{" "}
<InlineCode>Start</InlineCode> or <InlineCode>Stop</InlineCode> as
needed.
</p>
<TerminalBlock command={GDBUS_TOGGLE} displayCommand={GDBUS_TOGGLE} />
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">Set it up</h2>
<ol className="space-y-4">
{steps.map((step, index) => (
<li
key={step.name}
className="rounded-[12px] border border-border bg-background p-5"
>
<div className="mb-2 flex items-center gap-3">
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
{index + 1}
</span>
<h3 className="font-semibold">{step.name}</h3>
</div>
<p className="ml-10 text-sm text-muted-foreground">{step.text}</p>
</li>
))}
</ol>
<p className="mt-4 text-sm text-muted-foreground">
The same switch is stored as{" "}
<InlineCode>shortcuts.disable_internal_hotkey</InlineCode> in{" "}
<InlineCode>~/.config/vocalinux/config.json</InlineCode>. Quit
Vocalinux before editing that file by hand, or the running app can
overwrite your change.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Binding the shortcut on each desktop
</h2>
<p className="mb-6 text-sm text-muted-foreground">
The key combination is your choice; the examples use Super+Shift+D.
Only the command matters to Vocalinux. Menu names and config syntax
belong to each desktop and can differ between versions.
</p>
<h3 className="mb-2 font-semibold">GNOME</h3>
<p className="mb-6 text-sm text-muted-foreground">
In Settings → Keyboard, open the custom shortcuts list, add a
shortcut, enter <InlineCode>vocalinux --toggle</InlineCode> as the
command and record your key combination.
</p>
<h3 className="mb-2 font-semibold">KDE Plasma</h3>
<p className="mb-6 text-sm text-muted-foreground">
Open System Settings → Keyboard → Shortcuts → Add New → Command or
Script (older Plasma: System Settings → Shortcuts → Custom Shortcuts →
Edit → New → Global Shortcut → Command/URL) and bind your keys to{" "}
<InlineCode>vocalinux --toggle</InlineCode>.
</p>
<div className="grid gap-4 md:grid-cols-2">
<ConfigBlock label="sway (~/.config/sway/config)">
{"bindsym $mod+Shift+d exec vocalinux --toggle"}
</ConfigBlock>
<ConfigBlock label="Hyprland (hyprland.conf)">
{"bind = SUPER SHIFT, D, exec, vocalinux --toggle"}
</ConfigBlock>
</div>
<p className="mt-4 text-sm text-muted-foreground">
If your compositor cannot find <InlineCode>vocalinux</InlineCode>, its
PATH may not include <InlineCode>~/.local/bin</InlineCode>. Use the
full path{" "}
<InlineCode>~/.local/share/vocalinux/venv/bin/vocalinux</InlineCode>{" "}
for an installer setup, or the <InlineCode>gdbus</InlineCode> command
above. More on tiling compositors in{" "}
<Link href="/hyprland-sway/" className="text-primary underline">
voice typing on Hyprland and sway
</Link>
.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
<AlertTriangle className="h-5 w-5 text-primary" aria-hidden="true" />
Limitations and gotchas
</h2>
<ul className="list-disc space-y-2 pl-5 text-sm text-muted-foreground">
<li>
Toggle only. Push-to-talk needs the key release, which a desktop
shortcut never delivers.
</li>
<li>
The command needs a session bus. From SSH or a systemd unit without{" "}
<InlineCode>DBUS_SESSION_BUS_ADDRESS</InlineCode>, it fails with a
non-zero exit.
</li>
<li>
While a transcription is still processing, Toggle cancels it, the
same as pressing the recording hotkey again (since v0.18.4).
</li>
<li>
On Flatpak,{" "}
<InlineCode>
flatpak run com.vocalinux.Vocalinux --toggle
</InlineCode>{" "}
also works but starts a sandbox on each press; prefer{" "}
<InlineCode>gdbus</InlineCode>.
</li>
<li>
This changes how dictation starts, not how text is typed. If text
does not appear, see{" "}
<Link href="/wayland/" className="text-primary underline">
Wayland text injection
</Link>
.
</li>
</ul>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">FAQ</h2>
<div className="space-y-4">
{faqs.map((faq) => (
<div
key={faq.question}
className="rounded-[12px] border border-border bg-background p-5"
>
<h3 className="mb-2 font-semibold">{faq.question}</h3>
<p className="text-sm text-muted-foreground">{faq.answer}</p>
</div>
))}
</div>
<p className="mt-4 text-sm text-muted-foreground">
Related:{" "}
<Link href="/shortcuts/" className="text-primary underline">
shortcuts and voice commands
</Link>
,{" "}
<Link href="/gnome-kde/" className="text-primary underline">
GNOME and KDE
</Link>
,{" "}
<Link href="/flatpak/" className="text-primary underline">
Flatpak
</Link>{" "}
and{" "}
<Link href="/troubleshooting/" className="text-primary underline">
troubleshooting
</Link>
.
</p>
</section>
<section className="rounded-[12px] border border-border bg-muted p-8">
<h2 className="mb-4 font-display text-2xl font-semibold">
Bind your dictation key
</h2>
<p className="mb-6 text-muted-foreground">
Install Vocalinux, turn on External Activation, and let your desktop
own the shortcut.
</p>
<div className="flex flex-wrap gap-4">
<Link
href="/install/"
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
>
Install Vocalinux
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Link>
<Link
href="/hyprland-sway/"
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted"
>
Hyprland and sway guide
</Link>
</div>
</section>
</SeoSubpageShell>
);
}
+513
View File
@@ -0,0 +1,513 @@
import Link from "next/link";
import { type Metadata } from "next";
import type { ReactNode } from "react";
import {
AlertTriangle,
ChevronRight,
Keyboard,
Monitor,
PanelTop,
} from "lucide-react";
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
import { TerminalBlock } from "@/components/terminal-block";
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
const PAGE_PATH = "/hyprland-sway";
const PAGE_TITLE = "Voice Typing on Hyprland, Sway and i3 for Linux";
const PAGE_DESCRIPTION =
"Set up Vocalinux voice typing on sway, i3 and Hyprland: pick wtype, ydotool, IBus or the RemoteDesktop portal, bind keys over D-Bus, and use the Dictation Pad.";
const supportRows = [
{
name: "sway",
status: "Good support",
session: "Wayland",
notes: "Wayland native, uses wtype",
},
{ name: "i3", status: "Good support", session: "X11", notes: "Uses xdotool" },
{
name: "Hyprland",
status: "Experimental",
session: "Wayland",
notes: "May need wtype configuration",
},
];
const backendRows = [
{
backend: "IBus (ibus)",
where:
"Desktops that bridge IBus; on sway and Hyprland only with ibus-wayland running",
needs:
"IBus. On sway and Hyprland, Vocalinux skips IBus unless the ibus-wayland bridge is running",
},
{
backend: "RemoteDesktop portal (portal)",
where: "Wayland desktops whose portal implements RemoteDesktop",
needs:
"org.freedesktop.portal.RemoteDesktop. GNOME and KDE Plasma have it; most wlroots compositors do not",
},
{
backend: "ydotool (ydotool)",
where: "Any Wayland session",
needs: "A usable /dev/uinput and a working ydotoold setup",
},
{
backend: "wtype (wtype)",
where: "sway, Hyprland and other virtual-keyboard compositors",
needs: "The wtype package",
},
{
backend: "xdotool (xdotool)",
where: "i3 and other X11 sessions; XWayland windows only on Wayland",
needs: "The xdotool package",
},
];
const steps = [
{
name: "Install Vocalinux and the typing tools",
text: "Install Vocalinux, then make sure wtype and wl-clipboard are installed on sway or Hyprland, or xdotool on i3.",
},
{
name: "Check that the tool can type",
text: 'Focus a text field and run wtype "hello" on Wayland or xdotool type "hello" on X11. If nothing appears, Vocalinux cannot type there either.',
},
{
name: "Choose how dictation starts",
text: "Either add yourself to the input group for the built-in hotkey, or turn on External Activation and bind vocalinux --toggle in your compositor config.",
},
{
name: "Pin a backend if autodetection guesses wrong",
text: "Set text_injection.backend in ~/.config/vocalinux/config.json, for example to wtype, then restart Vocalinux.",
},
];
const faqs = [
{
question:
"Why does dictation work in my browser but not in native Wayland apps?",
answer:
"That is the classic IBus symptom: the compositor does not relay IBus commits to native Wayland clients, so IBus reports success while nothing appears. Pin the wtype backend and restart Vocalinux.",
},
{
question: "Does the RemoteDesktop portal work on sway or Hyprland?",
answer:
"Only if your portal backend implements org.freedesktop.portal.RemoteDesktop. GNOME and KDE Plasma do and most wlroots compositors do not. When it is missing, Vocalinux moves on to ydotool and then wtype.",
},
{
question: "Can I use push-to-talk on a tiling compositor?",
answer:
"Yes, with the built-in listener, which on Wayland needs your user in the input group. A compositor keybind over D-Bus can only toggle dictation.",
},
{
question: "Is Hyprland supported?",
answer:
"Hyprland is marked Experimental in the compatibility table and may need wtype configuration. sway and i3 are marked as good support.",
},
];
export const metadata: Metadata = buildPageMetadata({
title: `${PAGE_TITLE} | Vocalinux`,
description: PAGE_DESCRIPTION,
path: PAGE_PATH,
keywords: [
"hyprland voice typing",
"sway speech to text",
"i3 voice dictation",
"wtype dictation",
"wayland tiling compositor dictation",
"hyprland dictation keybind",
"ydotool voice typing",
],
});
function InlineCode({ children }: { children: ReactNode }) {
return (
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[0.85em] text-foreground">
{children}
</code>
);
}
function ConfigBlock({ label, children }: { label: string; children: string }) {
return (
<figure className="min-w-0">
<figcaption className="mb-1 text-sm font-medium">{label}</figcaption>
<pre className="subpage-terminal p-4 text-sm">
<code>{children}</code>
</pre>
</figure>
);
}
export default function HyprlandSwayPage() {
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "TechArticle",
headline: PAGE_TITLE,
description: PAGE_DESCRIPTION,
dateModified: "2026-11-01",
author: {
"@type": "Person",
name: "Jatin K Malik",
url: "https://github.com/jatinkrmalik",
},
publisher: {
"@type": "Organization",
name: "Vocalinux",
logo: {
"@type": "ImageObject",
url: absoluteUrl("/vocalinux.png"),
},
},
mainEntityOfPage: absoluteUrl(PAGE_PATH),
},
{
"@type": "HowTo",
name: "Set up Vocalinux voice typing on sway, i3 or Hyprland",
step: steps.map((step, index) => ({
"@type": "HowToStep",
position: index + 1,
name: step.name,
text: step.text,
})),
},
{
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: { "@type": "Answer", text: faq.answer },
})),
},
],
};
return (
<SeoSubpageShell>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<section>
<p className="subpage-kicker">Tiling compositors</p>
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
Voice typing on Hyprland, sway and i3
</h1>
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
Tiling setups are hand-built, so Vocalinux cannot assume the pieces a
full desktop provides. This guide covers which text injection backend
applies on each compositor, how to bind dictation to a key in your
config, and the fallback that works when typing into other windows
does not.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
<Monitor className="h-5 w-5 text-primary" aria-hidden="true" />
Support status
</h2>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th scope="col" className="pb-3 pr-4 font-semibold">
Compositor / WM
</th>
<th scope="col" className="pb-3 pr-4 font-semibold">
Status
</th>
<th scope="col" className="pb-3 pr-4 font-semibold">
Session
</th>
<th scope="col" className="pb-3 font-semibold">
Notes
</th>
</tr>
</thead>
<tbody>
{supportRows.map((row) => (
<tr key={row.name} className="border-b border-border">
<th scope="row" className="py-3 pr-4 font-medium">
{row.name}
</th>
<td className="py-3 pr-4">{row.status}</td>
<td className="py-3 pr-4 text-muted-foreground">
{row.session}
</td>
<td className="py-3 text-muted-foreground">{row.notes}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mt-4 text-sm text-muted-foreground">
Status comes from the project&apos;s desktop compatibility table. For
GNOME, KDE and the general Wayland picture, see{" "}
<Link href="/wayland/" className="text-primary underline">
voice dictation on Wayland
</Link>
.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Text injection options
</h2>
<p className="mb-4 text-sm text-muted-foreground">
Vocalinux picks a backend automatically. On Wayland, when IBus is not
selected, it tries the RemoteDesktop portal first, then ydotool, then
wtype, and finally xdotool for XWayland windows. sway and Hyprland are
on its list of compositors that do not bridge IBus to native Wayland
apps, so IBus is skipped there unless the{" "}
<InlineCode>ibus-wayland</InlineCode> helper (IBus 1.5.32 and newer)
is running.
</p>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th scope="col" className="pb-3 pr-4 font-semibold">
Backend (config value)
</th>
<th scope="col" className="pb-3 pr-4 font-semibold">
Where it applies
</th>
<th scope="col" className="pb-3 font-semibold">
Needs
</th>
</tr>
</thead>
<tbody>
{backendRows.map((row) => (
<tr
key={row.backend}
className="border-b border-border align-top"
>
<th scope="row" className="py-3 pr-4 font-medium">
{row.backend}
</th>
<td className="py-3 pr-4 text-muted-foreground">
{row.where}
</td>
<td className="py-3 text-muted-foreground">{row.needs}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="mb-2 mt-6 font-semibold">Pin a backend</h3>
<p className="mb-3 text-sm text-muted-foreground">
Edit <InlineCode>~/.config/vocalinux/config.json</InlineCode> and
restart Vocalinux. An explicit non-IBus value skips IBus selection. If
the pinned tool is missing, Vocalinux warns and falls back to its
normal order.
</p>
<ConfigBlock label="config.json">
{`{
"text_injection": {
"backend": "wtype"
}
}`}
</ConfigBlock>
<p className="mb-3 mt-4 text-sm text-muted-foreground">
To try a backend for one run without saving it:
</p>
<TerminalBlock
command="VOCALINUX_FORCE_BACKEND=wtype vocalinux --debug"
displayCommand="VOCALINUX_FORCE_BACKEND=wtype vocalinux --debug"
/>
<h3 className="mb-2 mt-6 font-semibold">Terminals</h3>
<p className="text-sm text-muted-foreground">
When text goes in through the clipboard, Vocalinux sends Ctrl+V in
normal fields and Ctrl+Shift+V in terminal windows. Terminal detection
works on Hyprland, sway and niri.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
Setup steps
</h2>
<ol className="space-y-4">
{steps.map((step, index) => (
<li
key={step.name}
className="rounded-[12px] border border-border bg-background p-5"
>
<div className="mb-2 flex items-center gap-3">
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
{index + 1}
</span>
<h3 className="font-semibold">{step.name}</h3>
</div>
<p className="ml-10 text-sm text-muted-foreground">{step.text}</p>
</li>
))}
</ol>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
<Keyboard className="h-5 w-5 text-primary" aria-hidden="true" />
Keybinds via D-Bus
</h2>
<p className="mb-4 text-sm text-muted-foreground">
On Wayland the built-in hotkey reads{" "}
<InlineCode>/dev/input</InlineCode>, so your user must be in the{" "}
<InlineCode>input</InlineCode> group (the source installer does this;
.deb and .rpm packages cannot). The alternative, added in v0.18.0, is
to turn on Settings → Shortcuts → External Activation (Desktop
Shortcut) and let the compositor own the key. These binds toggle
dictation; they cannot do push-to-talk.
</p>
<div className="mb-4 grid gap-4 md:grid-cols-2">
<ConfigBlock label="sway or i3 config">
{"bindsym $mod+Shift+d exec vocalinux --toggle"}
</ConfigBlock>
<ConfigBlock label="hyprland.conf">
{"bind = SUPER SHIFT, D, exec, vocalinux --toggle"}
</ConfigBlock>
</div>
<p className="mb-3 text-sm text-muted-foreground">
Or keep the built-in listener and join the group, then log out and
back in:
</p>
<TerminalBlock
command="sudo usermod -aG input $USER"
displayCommand="sudo usermod -aG input $USER"
/>
<p className="mt-4 text-sm text-muted-foreground">
The full D-Bus interface and Flatpak command are in{" "}
<Link href="/global-shortcuts/" className="text-primary underline">
global shortcuts
</Link>
. Minimal window-manager sessions may also need their own autostart
line; see{" "}
<Link href="/autostart/" className="text-primary underline">
autostart
</Link>
.
</p>
</section>
<section className="bg-primary/5 mb-12 rounded-[12px] border border-border p-6">
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
<PanelTop className="h-5 w-5 text-primary" aria-hidden="true" />
The Dictation Pad fallback
</h2>
<p className="text-sm text-muted-foreground">
If no backend types reliably on your setup, send dictation to the
Dictation Pad instead (added in v0.18.0). It is an in-app window that
receives the text, and you copy it out by hand, so no injection path
is involved. Turn on Settings → Dictation → Dictate into Dictation
Pad, then open it with Open Dictation Pad in the same group or
Dictation Pad in the tray menu. Later releases fixed the pad ghosting
away when idle under a Wayland compositor (v0.18.1) and stopped it
travelling between workspaces with Settings (v0.18.5). Details on the{" "}
<Link href="/dictation-pad/" className="text-primary underline">
Dictation Pad page
</Link>
.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
<AlertTriangle className="h-5 w-5 text-primary" aria-hidden="true" />
Troubleshooting
</h2>
<h3 className="mb-2 font-semibold">Nothing is typed anywhere</h3>
<p className="mb-3 text-sm text-muted-foreground">
Install the Wayland tools and test them by hand, then run Vocalinux
with debug logging and check which backend it reports. The startup log
records a pin request; a live test in the target app is the real
confirmation.
</p>
<div className="mb-3">
<TerminalBlock
command="sudo apt install wtype wl-clipboard xclip xsel"
displayCommand="sudo apt install wtype wl-clipboard xclip xsel"
/>
</div>
<div className="mb-6">
<TerminalBlock
command={'wtype "hello"'}
displayCommand={'wtype "hello"'}
/>
</div>
<h3 className="mb-2 font-semibold">
ydotool is chosen but does nothing
</h3>
<p className="mb-6 text-sm text-muted-foreground">
ydotool needs <InlineCode>/dev/uinput</InlineCode> and a running
ydotoold. Fix that setup or pin <InlineCode>wtype</InlineCode>.
</p>
<h3 className="mb-2 font-semibold">The hotkey never fires</h3>
<p className="text-sm text-muted-foreground">
Confirm you are in the <InlineCode>input</InlineCode> group in the
current session, or switch to External Activation with a compositor
bind. On i3 (X11) the built-in X11 listener handles the hotkey without
that group.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">FAQ</h2>
<div className="space-y-4">
{faqs.map((faq) => (
<div
key={faq.question}
className="rounded-[12px] border border-border bg-background p-5"
>
<h3 className="mb-2 font-semibold">{faq.question}</h3>
<p className="text-sm text-muted-foreground">{faq.answer}</p>
</div>
))}
</div>
<p className="mt-4 text-sm text-muted-foreground">
More help:{" "}
<Link href="/troubleshooting/" className="text-primary underline">
troubleshooting
</Link>{" "}
and{" "}
<Link href="/desktop-reliability/" className="text-primary underline">
desktop reliability
</Link>
.
</p>
</section>
<section className="rounded-[12px] border border-border bg-muted p-8">
<h2 className="mb-4 font-display text-2xl font-semibold">
Dictate on your tiling setup
</h2>
<p className="mb-6 text-muted-foreground">
Install Vocalinux, check that wtype or xdotool can type, and bind
dictation to a key in your compositor config.
</p>
<div className="flex flex-wrap gap-4">
<Link
href="/install/"
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
>
Install Vocalinux
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Link>
<Link
href="/wayland/"
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted"
>
Wayland guide
</Link>
</div>
</section>
</SeoSubpageShell>
);
}
+374 -95
View File
@@ -5,7 +5,11 @@ import {
CheckCircle2,
ChevronRight,
Cpu,
Download,
Laptop,
Monitor,
RefreshCw,
Server,
Shield,
Sparkles,
Terminal,
@@ -13,81 +17,149 @@ import {
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
type DistroSlug = "ubuntu" | "fedora" | "arch";
type DistroSlug =
| "ubuntu"
| "debian"
| "fedora"
| "arch"
| "opensuse"
| "linux-mint"
| "pop-os";
type DistroParams = { distro: string } | Promise<{ distro: string }>;
interface InstallOption {
label: string;
note: string;
command: string;
}
interface DistroConfig {
name: string;
title: string;
description: string;
intro: string;
prerequisites: string[];
testedOn: string[];
supportedOn: string[];
installCommand: string;
otherInstallOptions: InstallOption[];
setupNotes: string[];
postInstallChecks: string[];
}
const INSTALL_SCRIPT_COMMAND = `curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh -o /tmp/vl.sh
bash /tmp/vl.sh`;
const DEB_OPTION: InstallOption = {
label: "Native .deb package",
note: "Download the .deb for your CPU from GitHub Releases (arm64 files are attached too). It is a thin package: Python and GTK come from the distro, whisper.cpp runs on the CPU, and there is no auto-update. On Wayland, add yourself to the input group (sudo usermod -aG input $USER, then log out and back in) or the default hotkey stays off.",
command: "sudo apt install ./vocalinux_*_amd64.deb",
};
const RPM_OPTION: InstallOption = {
label: "Native .rpm package",
note: "Download the .rpm for your CPU from GitHub Releases (aarch64 files are attached too). It is a thin package: Python and GTK come from Fedora, whisper.cpp runs on the CPU, and there is no auto-update or COPR yet. On Wayland, add yourself to the input group (sudo usermod -aG input $USER, then log out and back in) or the default hotkey stays off.",
command: "sudo dnf install ./vocalinux-*.x86_64.rpm",
};
const FLATPAK_OPTION: InstallOption = {
label: "Flatpak from the VocaHQ remote",
note: "Not on Flathub. The self-hosted vocahq remote updates with flatpak update and ships whisper.cpp only.",
command:
"flatpak install https://vocahq.github.io/vocalinux-flatpak/com.vocalinux.Vocalinux.flatpakref",
};
const COMMON_CHECKS = [
"Run vocalinux --version to confirm the installed release.",
"Launch vocalinux and confirm the tray icon appears. On GNOME, the tray needs the AppIndicator extension.",
"Hold Right Alt (Option) to dictate, or switch to toggle mode in Settings → Shortcuts, and confirm text lands in a terminal and a browser.",
"If anything misbehaves, run vocalinux --debug and check the troubleshooting guide.",
];
const distroConfig: Record<DistroSlug, DistroConfig> = {
ubuntu: {
name: "Ubuntu",
title: "Install Voice Dictation on Ubuntu",
description:
"Step-by-step Ubuntu voice dictation setup for offline speech-to-text with whisper.cpp and VOSK. Works on Ubuntu 24.04+, GNOME, KDE, X11, and Wayland.",
"Step-by-step Ubuntu voice dictation setup for offline speech-to-text with whisper.cpp. Covers Ubuntu 24.04+, the install script, the .deb, X11 and Wayland.",
intro:
"This Ubuntu guide is tuned for practical desktop use: terminals, browsers, IDEs, office suites, and chat apps. Local engines keep audio on your machine. Remote API is optional.",
"This Ubuntu guide is tuned for practical desktop use: terminals, browsers, IDEs, office suites, and chat apps. Local engines keep audio on your machine after the one-time model download. Remote API is optional.",
prerequisites: [
"Ubuntu 24.04+ (or Ubuntu-based distro)",
"Python 3.11+",
"Ubuntu 24.04 or newer (Ubuntu 22.04 ships Python 3.10, below the 3.11 floor)",
"Python 3.11+ with the distro's python3-gi (the installer handles this)",
"Working microphone",
"curl installed (sudo apt update && sudo apt install -y curl)",
],
testedOn: [
"Ubuntu 24.04 LTS (GNOME, Wayland)",
"Ubuntu 24.04 LTS (GNOME, X11)",
"Kubuntu and Linux Mint derivatives",
supportedOn: [
"Ubuntu 24.04 LTS and newer: full support",
"Ubuntu-based derivatives on a 24.04 base, such as Linux Mint 22, Pop!_OS 24.04, elementary OS 8 and Zorin OS on 24.04",
],
installCommand:
"curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh -o /tmp/vl.sh && bash /tmp/vl.sh --interactive",
installCommand: INSTALL_SCRIPT_COMMAND,
otherInstallOptions: [DEB_OPTION, FLATPAK_OPTION],
setupNotes: [
"Choose whisper.cpp for the fastest setup and cross-vendor GPU support.",
"On lower-RAM systems, pick VOSK for lightweight realtime dictation.",
"If your desktop uses Wayland, keep keyboard injection defaults unless you know your compositor specifics.",
"Keep the default whisper.cpp engine: it installs in about 1-2 minutes and uses a Vulkan GPU when one is available.",
"On lower-RAM systems, VOSK is the lightweight option (--engine=vosk with the install script).",
"On KDE Plasma Wayland (Kubuntu), set System Settings → Keyboard → Virtual Keyboard to IBus Wayland.",
"The install script adds you to the input group for Wayland hotkeys; log out and back in afterwards.",
],
postInstallChecks: [
"Run vocalinux and confirm the tray indicator appears.",
"Hold Right Alt (Option) to dictate, or switch to toggle mode in Settings and verify dictation starts.",
"Set language and model size in the settings panel for your workload.",
...COMMON_CHECKS,
"Set language and model size in Settings → Speech Model for your workload.",
],
},
debian: {
name: "Debian",
title: "Install Voice Dictation on Debian",
description:
"Install Vocalinux offline voice dictation on Debian 12, 13 and Testing with the install script or the native .deb. Covers AppIndicator, ydotool and Wayland.",
intro:
"Debian 12 and newer are a primary target for Vocalinux. The install script detects Debian, installs the system packages it needs, and sets up whisper.cpp with a checksum-verified tiny model.",
prerequisites: [
"Debian 12 or newer, including Testing (Debian 11 and earlier are below the Python 3.11 floor)",
"Python 3.11+ with the distro's python3-gi (the installer handles this)",
"Working microphone",
"curl installed (sudo apt update && sudo apt install -y curl)",
],
supportedOn: [
"Debian 12 and newer: full support",
"Debian Testing: full support",
],
installCommand: INSTALL_SCRIPT_COMMAND,
otherInstallOptions: [DEB_OPTION, FLATPAK_OPTION],
setupNotes: [
"Debian uses the Ayatana AppIndicator for the tray (gir1.2-ayatanaappindicator3-0.1); the installer picks it.",
"Clean Debian installs lack some build tools for compiling pywhispercpp (libssl-dev, autoconf, automake, libtool, patchelf). The installer adds them; add them yourself before a manual rebuild.",
"ydotool is not in Debian's standard repositories. Vocalinux falls back to IBus or wtype for most Wayland compositors.",
"On KDE Plasma Wayland, set System Settings → Keyboard → Virtual Keyboard to IBus Wayland.",
],
postInstallChecks: [
...COMMON_CHECKS,
"On GNOME, install gnome-shell-extension-appindicator if the tray icon never appears.",
],
},
fedora: {
name: "Fedora",
title: "Install Voice Dictation on Fedora",
description:
"Fedora speech-to-text installation guide for Vocalinux. Set up offline Linux voice typing on Fedora Workstation with Wayland or X11.",
"Fedora speech-to-text installation guide for Vocalinux. Set up offline Linux voice typing on Fedora 42+ with the install script or .rpm, on Wayland or X11.",
intro:
"Fedora is an excellent match for Vocalinux thanks to current Python stacks and up-to-date system libraries. This guide focuses on reliable setup and minimal friction.",
"Fedora 42 and newer ship the Python and GTK stack Vocalinux needs, and Fedora 42 is the release in the project's CI matrix. This guide focuses on reliable setup and minimal friction.",
prerequisites: [
"Fedora Workstation 39+ (or Fedora-based distro)",
"Python 3.11+",
"Fedora 42 or newer (Workstation or a spin)",
"Python 3.11+ with python3-gobject (the installer handles this)",
"Working microphone",
"curl installed (sudo dnf install -y curl)",
],
testedOn: [
"Fedora Workstation 41 (Wayland)",
"Fedora Workstation 40 (X11 fallback)",
"Fedora KDE spin",
],
installCommand:
"curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh -o /tmp/vl.sh && bash /tmp/vl.sh --interactive",
supportedOn: ["Fedora 42 and newer: good support (uses lib64 paths)"],
installCommand: INSTALL_SCRIPT_COMMAND,
otherInstallOptions: [RPM_OPTION, FLATPAK_OPTION],
setupNotes: [
"If prompted for optional dependencies, accept defaults unless your desktop setup requires custom input tooling.",
"Keep the default whisper.cpp engine for best speed-to-accuracy tradeoff.",
"Use the smallest model first, then scale up after confirming stable dictation latency.",
"Keep the default whisper.cpp engine for the best speed-to-accuracy tradeoff.",
"Use the smallest model first, then scale up once dictation feels stable on your hardware.",
"On the KDE spin under Wayland, set System Settings → Keyboard → Virtual Keyboard to IBus Wayland.",
],
postInstallChecks: [
"Run vocalinux from terminal and test transcription in GNOME Text Editor or VS Code.",
"Verify toggle mode and push-to-talk behavior with your preferred keyboard layout.",
...COMMON_CHECKS,
"Confirm startup behavior and language settings persist after reboot.",
],
},
@@ -95,33 +167,135 @@ const distroConfig: Record<DistroSlug, DistroConfig> = {
name: "Arch Linux",
title: "Install Voice Dictation on Arch Linux",
description:
"Arch Linux voice dictation guide for offline speech recognition. Install Vocalinux on Arch, Manjaro, and EndeavourOS with distro-aware setup notes.",
"Arch Linux voice dictation guide for offline speech recognition. Install Vocalinux from the AUR or the install script on Arch, Manjaro and EndeavourOS.",
intro:
"Arch users usually care about control and performance. This guide keeps setup lean while preserving compatibility across rolling-release desktop environments.",
"Arch users can install Vocalinux from the AUR or with the install script. Both keep whisper.cpp as the default engine and work across rolling-release desktops.",
prerequisites: [
"Arch Linux / Manjaro / EndeavourOS",
"Python 3.11+",
"Arch Linux, Manjaro or EndeavourOS",
"Python 3.11+ with python-gobject",
"Working microphone",
"curl installed (sudo pacman -Sy --needed curl)",
"curl installed (sudo pacman -Sy --needed curl), or an AUR helper such as yay",
],
testedOn: [
"Arch Linux (GNOME, Wayland)",
"Arch Linux (KDE Plasma)",
"Manjaro (X11 and Wayland sessions)",
supportedOn: [
"Arch Linux (rolling): good support",
"Manjaro (rolling): good support, inherits Arch compatibility",
"EndeavourOS (rolling): good support, inherits Arch compatibility",
],
installCommand: INSTALL_SCRIPT_COMMAND,
otherInstallOptions: [
{
label: "AUR packages",
note: "vocalinux builds the canonical release sdist against Arch dependencies. vocalinux-bin installs the prebuilt release AppImage. vocalinux-git is a community package that tracks main. The three conflict, so install only one.",
command: `yay -S vocalinux # canonical release sdist, built against Arch dependencies
yay -S vocalinux-git # community package; tracks main`,
},
],
installCommand:
"curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh -o /tmp/vl.sh && bash /tmp/vl.sh --interactive",
setupNotes: [
"Prefer whisper.cpp for lower dependency footprint and faster cold start.",
"Prefer whisper.cpp: it avoids the full PyTorch stack that OpenAI Whisper pulls in.",
"On custom compositor setups, verify text injection behavior in both terminal and browser contexts.",
"If you tweak keybinds heavily, set a non-conflicting activation shortcut in Vocalinux settings.",
"AUR packages update with your AUR helper; the in-app update checker only notifies.",
],
postInstallChecks: [
"Run vocalinux and test dictation in multiple apps (terminal, browser, editor).",
"Confirm your chosen speech engine model balances latency and accuracy.",
...COMMON_CHECKS,
"Back up ~/.config/vocalinux/config.json after tuning preferences.",
],
},
opensuse: {
name: "openSUSE",
title: "Install Voice Dictation on openSUSE",
description:
"Install Vocalinux offline voice dictation on openSUSE Tumbleweed with the install script. Covers versioned Python packages, Vulkan builds, X11 and Wayland.",
intro:
"On openSUSE Tumbleweed the install script uses zypper, resolves the versioned Python packages for your snapshot, and sets up whisper.cpp with a checksum-verified tiny model.",
prerequisites: [
"openSUSE Tumbleweed",
"Python 3.11+ with the matching pythonXY-gobject package (the installer handles this)",
"Working microphone",
"curl installed (sudo zypper install curl)",
],
supportedOn: [
"openSUSE Tumbleweed: good support",
"openSUSE Leap is not listed in the compatibility matrix",
],
installCommand: INSTALL_SCRIPT_COMMAND,
otherInstallOptions: [
FLATPAK_OPTION,
{
label: "AppImage",
note: "Download the x86_64 or aarch64 AppImage from GitHub Releases. It starts on Tumbleweed and uses Vulkan through the host driver, but still needs host text-injection tools (xdotool on X11; wtype, ydotool or clipboard tools on Wayland).",
command: `chmod +x Vocalinux-*-x86_64.AppImage # or aarch64
./Vocalinux-*-x86_64.AppImage`,
},
],
setupNotes: [
"Tumbleweed uses versioned Python package names such as python313-devel. The -devel suffix means development headers, not beta packages.",
"For a manual install, if ${PYVER}-virtualenv is unavailable on your snapshot, try ${PYVER}-venv.",
"For whisper.cpp Vulkan GPU builds, install vulkan-tools, vulkan-devel and shaderc (sudo zypper install -y vulkan-tools vulkan-devel shaderc).",
"On KDE Plasma Wayland, set System Settings → Keyboard → Virtual Keyboard to IBus Wayland.",
],
postInstallChecks: [
...COMMON_CHECKS,
"Run vulkaninfo --summary if you expect GPU acceleration.",
],
},
"linux-mint": {
name: "Linux Mint",
title: "Install Voice Dictation on Linux Mint",
description:
"Install Vocalinux offline voice dictation on Linux Mint 22 with the install script or .deb. Covers Cinnamon, the Ubuntu 24.04 base and why Mint 21 is too old.",
intro:
"Linux Mint is judged by its Ubuntu base, not its own version number. Mint 22 is built on Ubuntu 24.04, so the install script treats it as Ubuntu and installs everything Vocalinux needs.",
prerequisites: [
"Linux Mint 22 or newer, on an Ubuntu 24.04+ base (Mint 21 is on Ubuntu 22.04, below the floor)",
"Python 3.11+ with the distro's python3-gi (the installer handles this)",
"Working microphone",
"curl installed (sudo apt update && sudo apt install -y curl)",
],
supportedOn: [
"Linux Mint on Ubuntu 24.04+ (Mint 22): good support",
"Cinnamon, MATE and Xfce desktops: full support",
],
installCommand: INSTALL_SCRIPT_COMMAND,
otherInstallOptions: [
{
...DEB_OPTION,
note: `The .deb targets Debian 12+ and Ubuntu 24.04+, the base Mint 22 is built on. ${DEB_OPTION.note}`,
},
FLATPAK_OPTION,
],
setupNotes: [
"Keep the default whisper.cpp engine unless you have a specific reason to switch.",
"Cinnamon is fully supported and works with AppIndicator for the tray icon.",
"Text is typed with xdotool on X11 sessions; on Wayland, Vocalinux picks from IBus, the RemoteDesktop portal, wtype or ydotool.",
"The install script adds you to the input group for Wayland hotkeys; log out and back in afterwards.",
],
postInstallChecks: [...COMMON_CHECKS],
},
"pop-os": {
name: "Pop!_OS",
title: "Install Voice Dictation on Pop!_OS",
description:
"Install Vocalinux offline voice dictation on Pop!_OS 24.04 with the install script or .deb. Covers the Ubuntu base, Wayland hotkeys and fallback options.",
intro:
"Pop!_OS uses Ubuntu's own version numbering, and 24.04 and newer meet the Python 3.11 floor. The install script detects Pop!_OS as part of the Ubuntu family.",
prerequisites: [
"Pop!_OS 24.04 or newer",
"Python 3.11+ with the distro's python3-gi (the installer handles this)",
"Working microphone",
"curl installed (sudo apt update && sudo apt install -y curl)",
],
supportedOn: ["Pop!_OS 24.04 and newer: good support"],
installCommand: INSTALL_SCRIPT_COMMAND,
otherInstallOptions: [DEB_OPTION, FLATPAK_OPTION],
setupNotes: [
"For a manual install, use libgirepository-2.0-dev if libgirepository1.0-dev is not available.",
"The install script adds you to the input group for Wayland hotkeys; log out and back in afterwards.",
"If the hotkey or typing does not work in your session, enable Settings → Shortcuts → External Activation (Desktop Shortcut) and bind a desktop shortcut to vocalinux --toggle (toggle only, no push-to-talk), or dictate into the Dictation Pad and copy the text out.",
"whisper.cpp uses a Vulkan GPU when available; check with vulkaninfo --summary.",
],
postInstallChecks: [...COMMON_CHECKS],
},
};
const distroKeywords: Record<DistroSlug, string[]> = {
@@ -131,6 +305,12 @@ const distroKeywords: Record<DistroSlug, string[]> = {
"ubuntu voice typing",
"offline dictation ubuntu",
],
debian: [
"debian voice dictation",
"speech to text debian",
"debian voice typing",
"offline dictation debian",
],
fedora: [
"fedora voice dictation",
"speech to text fedora",
@@ -141,7 +321,25 @@ const distroKeywords: Record<DistroSlug, string[]> = {
"arch linux voice dictation",
"speech to text arch linux",
"manjaro dictation",
"offline dictation arch",
"vocalinux aur",
],
opensuse: [
"opensuse voice dictation",
"speech to text opensuse tumbleweed",
"opensuse voice typing",
"offline dictation opensuse",
],
"linux-mint": [
"linux mint voice dictation",
"speech to text linux mint",
"linux mint voice typing",
"cinnamon dictation",
],
"pop-os": [
"pop os voice dictation",
"speech to text pop!_os",
"pop os voice typing",
"offline dictation pop os",
],
};
@@ -149,35 +347,65 @@ const isDistroSlug = (value: string): value is DistroSlug => {
return value in distroConfig;
};
const resolveParams = async (params: DistroParams): Promise<{ distro: string }> => {
const resolveParams = async (
params: DistroParams,
): Promise<{ distro: string }> => {
return Promise.resolve(params);
};
const getDistroStyles = (distro: DistroSlug) => {
if (distro === "ubuntu") {
return {
icon: Laptop,
iconClass: "text-primary",
chipClass: "border-border bg-muted text-muted-foreground",
panelClass: "bg-primary/10",
};
switch (distro) {
case "ubuntu":
return {
icon: Laptop,
iconClass: "text-primary",
chipClass: "border-border bg-muted text-muted-foreground",
panelClass: "bg-primary/10",
};
case "debian":
return {
icon: Server,
iconClass: "text-primary",
chipClass: "border-border bg-muted text-muted-foreground",
panelClass: "bg-primary/10",
};
case "fedora":
return {
icon: Sparkles,
iconClass: "text-primary",
chipClass: "border-border bg-primary/10 text-primary",
panelClass: "bg-primary/10",
};
case "opensuse":
return {
icon: RefreshCw,
iconClass: "text-primary",
chipClass: "border-border bg-primary/10 text-primary",
panelClass: "bg-primary/10",
};
case "linux-mint":
return {
icon: Monitor,
iconClass: "text-primary",
chipClass: "border-border bg-muted text-muted-foreground",
panelClass: "bg-primary/10",
};
case "pop-os":
return {
icon: Laptop,
iconClass: "text-primary",
chipClass: "border-border bg-primary/10 text-primary",
panelClass: "bg-primary/10",
};
case "arch":
default:
return {
icon: Cpu,
iconClass: "text-primary",
chipClass: "border-border bg-primary/10 text-muted-foreground",
panelClass: "bg-primary/10",
};
}
if (distro === "fedora") {
return {
icon: Sparkles,
iconClass: "text-primary",
chipClass: "border-border bg-primary/10 text-primary",
panelClass: "bg-primary/10",
};
}
return {
icon: Cpu,
iconClass: "text-primary",
chipClass: "border-border bg-primary/10 text-muted-foreground",
panelClass: "bg-primary/10",
};
};
export function generateStaticParams() {
@@ -195,7 +423,7 @@ export async function generateMetadata({
return buildPageMetadata({
title: "Linux Voice Dictation Install Guide",
description:
"Install Vocalinux on Linux with distro-specific setup instructions for Ubuntu, Fedora, and Arch.",
"Install Vocalinux on Linux with distro-specific setup instructions for Ubuntu, Debian, Fedora, Arch, openSUSE, Linux Mint, and Pop!_OS.",
path: "/install",
});
}
@@ -277,18 +505,6 @@ export default async function DistroInstallPage({
dangerouslySetInnerHTML={{ __html: JSON.stringify(howToJsonLd) }}
/>
<nav className="mb-6 flex items-center gap-2 text-sm text-muted-foreground">
<Link href="/" className="hover:text-foreground hover:underline">
Home
</Link>
<ChevronRight className="h-4 w-4" />
<Link href="/install/" className="hover:text-foreground hover:underline">
Install Guides
</Link>
<ChevronRight className="h-4 w-4" />
<span className="font-medium text-foreground">{config.name}</span>
</nav>
<section>
<p
className={`mb-4 inline-flex items-center gap-2 rounded-full border px-4 py-1.5 text-sm font-medium ${styles.chipClass}`}
@@ -297,11 +513,15 @@ export default async function DistroInstallPage({
{config.name} Setup Guide
</p>
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">{config.title}</h1>
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">{config.intro}</p>
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
{config.title}
</h1>
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
{config.intro}
</p>
<div
className={`rounded-[12px] border border-border p-6 ${styles.panelClass}`}
className={`rounded-[12px] border border-border p-6 ${styles.panelClass}`}
>
<h2 className="mb-3 inline-flex items-center gap-2 text-xl font-semibold">
<Terminal className="h-5 w-5 text-primary" />
@@ -333,20 +553,69 @@ export default async function DistroInstallPage({
<article className="rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 inline-flex items-center gap-2 text-2xl font-semibold">
<Sparkles className="h-5 w-5 text-primary" />
Tested environments
<Sparkles className="h-5 w-5 text-primary" aria-hidden="true" />
Supported on
</h2>
<ul className="space-y-2 text-muted-foreground">
{config.testedOn.map((item) => (
{config.supportedOn.map((item) => (
<li key={item} className="flex items-start gap-2">
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary"
aria-hidden="true"
/>
{item}
</li>
))}
</ul>
<p className="mt-4 text-sm text-muted-foreground">
Support levels come from the project&rsquo;s distribution
compatibility matrix. They describe expected compatibility, not a
list of test machines.
</p>
</article>
</section>
{config.otherInstallOptions.length > 0 && (
<section className="mt-10 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 inline-flex items-center gap-2 text-2xl font-semibold">
<Download className="h-5 w-5 text-primary" aria-hidden="true" />
Other install options for {config.name}
</h2>
<div className="space-y-6">
{config.otherInstallOptions.map((option) => (
<div key={option.label}>
<h3 className="mb-2 font-semibold">{option.label}</h3>
<p className="mb-3 text-sm text-muted-foreground">
{option.note}
</p>
<div className="rounded-[12px] bg-[color:var(--terminal)] p-4">
<code className="block overflow-x-auto whitespace-pre-wrap break-all text-sm text-[color:var(--terminal-fg)]">
{option.command}
</code>
</div>
</div>
))}
</div>
<p className="mt-6 text-sm text-muted-foreground">
Compare every package format on the
<Link
href="/install/"
className="ml-1 font-semibold text-primary hover:underline"
>
install overview
</Link>
, or read the
<Link
href="/flatpak/"
className="ml-1 font-semibold text-primary hover:underline"
>
Flatpak guide
</Link>
.
</p>
</section>
)}
<section className="mt-10 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 inline-flex items-center gap-2 text-2xl font-semibold">
<Cpu className="h-5 w-5 text-primary" />
@@ -362,7 +631,7 @@ export default async function DistroInstallPage({
</ol>
</section>
<section className="mt-10 rounded-[12px] border border-border bg-muted p-6 /70">
<section className="/70 mt-10 rounded-[12px] border border-border bg-muted p-6">
<h2 className="mb-4 inline-flex items-center gap-2 text-2xl font-semibold">
<CheckCircle2 className="h-5 w-5 text-primary" />
Post-install checklist
@@ -378,8 +647,18 @@ export default async function DistroInstallPage({
<p className="mt-6 text-sm text-muted-foreground">
Need help selecting an engine? Read the
<Link href="/compare/" className="ml-1 font-semibold text-primary hover:underline">
whisper.cpp vs Whisper vs VOSK comparison
<Link
href="/compare/"
className="ml-1 font-semibold text-primary hover:underline"
>
engine comparison
</Link>
. Stuck? See
<Link
href="/troubleshooting/"
className="ml-1 font-semibold text-primary hover:underline"
>
troubleshooting
</Link>
.
</p>
+418
View File
@@ -0,0 +1,418 @@
import Link from "next/link";
import { type Metadata } from "next";
import {
AlertTriangle,
Braces,
CheckCircle2,
ChevronRight,
CircleHelp,
Settings2,
Terminal,
} from "lucide-react";
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
const PAGE_PATH = "/install/nixos";
const PAGE_TITLE = "Vocalinux on NixOS: Nix Flake and NixOS Module | Vocalinux";
const PAGE_DESCRIPTION =
"Install Vocalinux on NixOS or any Nix system with the official flake: nix run, nix profile, the NixOS and Home Manager modules, Wayland groups and scope.";
const TRY_COMMANDS = `nix run github:VocaHQ/vocalinux # try it
nix profile install github:VocaHQ/vocalinux`;
const NIXOS_SNIPPET = `{
inputs.vocalinux.url = "github:VocaHQ/vocalinux";
outputs = { nixpkgs, vocalinux, ... }: {
nixosConfigurations.host = nixpkgs.lib.nixosSystem {
modules = [
vocalinux.nixosModules.vocalinux
{
programs.vocalinux.enable = true;
users.users.you.extraGroups = [ "input" "uinput" ];
}
];
};
};
}`;
const HOME_MANAGER_SNIPPET = `{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
inputs.home-manager.url = "github:nix-community/home-manager";
inputs.home-manager.inputs.nixpkgs.follows = "nixpkgs";
inputs.vocalinux.url = "github:VocaHQ/vocalinux";
outputs = { nixpkgs, home-manager, vocalinux, ... }: {
homeConfigurations.you = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.x86_64-linux;
modules = [
vocalinux.homeManagerModules.vocalinux
{ programs.vocalinux.enable = true; }
];
};
};
}`;
const flakeOutputs = [
{
output: "packages.<system>.vocalinux (also default)",
purpose: "The Vocalinux package for x86_64-linux and aarch64-linux",
},
{
output: "apps.<system>.vocalinux (also default)",
purpose: "What nix run github:VocaHQ/vocalinux starts",
},
{
output: "overlays.default",
purpose: "Adds pkgs.vocalinux to your own nixpkgs",
},
{
output: "nixosModules.vocalinux (also default)",
purpose: "programs.vocalinux.enable and programs.vocalinux.package",
},
{
output: "homeManagerModules.vocalinux (also default)",
purpose: "The same programs.vocalinux options, per user",
},
];
const moduleSteps = [
{
name: "Add the flake input",
text: 'Add inputs.vocalinux.url = "github:VocaHQ/vocalinux" to your system flake.',
},
{
name: "Import the module and enable it",
text: "Add vocalinux.nixosModules.vocalinux to your modules and set programs.vocalinux.enable = true. The module installs the package and enables hardware.uinput.",
},
{
name: "Add each dictating user to input and uinput",
text: 'Set users.users.<name>.extraGroups = [ "input" "uinput" ] for Wayland shortcuts and ydotool typing. X11 sessions can skip this.',
},
{
name: "Rebuild and log in again",
text: "Rebuild your system as usual, then log out and back in so the new group membership applies.",
},
];
const faqs = [
{
question: "Is NixOS supported?",
answer:
"Yes, through the Nix flake added in v0.18.2. The standard install.sh script does not work on NixOS because of the /nix/store layout, which is why the distro compatibility doc still lists NixOS as unsupported for that installer. Use the flake instead.",
},
{
question: "Does the Nix package use my GPU?",
answer:
"The bundled pywhispercpp is built with the Vulkan backend, so whisper.cpp can use an AMD, Intel or NVIDIA GPU through Vulkan. It is also built with GGML_NATIVE=OFF, a portable baseline rather than -march=native.",
},
{
question:
"Can I use Parakeet, Faster Whisper, VOSK or OpenAI Whisper from the flake?",
answer:
"No. The flake packages whisper.cpp plus the Silero VAD extra. The other engines are not packaged because of the PyTorch footprint or because they are not in nixpkgs.",
},
{
question: "Why do I need the input group?",
answer:
"On Wayland the built-in push-to-talk hotkey reads keyboard devices under /dev/input. Membership in input grants read access to every keyboard and pointer device, so the module leaves that choice to each user. The alternative is a compositor shortcut bound to the D-Bus Toggle command, which only toggles.",
},
{
question: "How do I update?",
answer:
"Update the vocalinux input in your flake lock and rebuild, or upgrade your Nix profile. The in-app update checker only tells you a release exists; it never installs anything.",
},
];
export const metadata: Metadata = buildPageMetadata({
title: PAGE_TITLE,
description: PAGE_DESCRIPTION,
path: PAGE_PATH,
keywords: [
"nixos voice dictation",
"vocalinux nix flake",
"nixos speech to text",
"nixos module vocalinux",
"home manager vocalinux",
"nix whisper.cpp dictation",
],
});
function Commands({ children }: { children: string }) {
return (
<div className="subpage-terminal mt-3 p-4">
<pre>
<code>{children}</code>
</pre>
</div>
);
}
export default function NixosInstallPage() {
const articleJsonLd = {
"@context": "https://schema.org",
"@type": "TechArticle",
headline: "Install Vocalinux on NixOS with the Nix flake",
description: PAGE_DESCRIPTION,
dateModified: "2026-11-01",
author: {
"@type": "Person",
name: "Jatin K Malik",
url: "https://github.com/jatinkrmalik",
},
publisher: {
"@type": "Organization",
name: "Vocalinux",
logo: {
"@type": "ImageObject",
url: absoluteUrl("/vocalinux.png"),
},
},
mainEntityOfPage: absoluteUrl(PAGE_PATH),
};
const howToJsonLd = {
"@context": "https://schema.org",
"@type": "HowTo",
name: "Enable Vocalinux with the NixOS module",
description: PAGE_DESCRIPTION,
tool: [{ "@type": "HowToTool", name: "Nix with flakes" }],
step: moduleSteps.map((step) => ({
"@type": "HowToStep",
name: step.name,
text: step.text,
url: absoluteUrl(PAGE_PATH),
})),
};
const faqJsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: { "@type": "Answer", text: faq.answer },
})),
};
return (
<SeoSubpageShell>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(howToJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<section>
<p className="subpage-kicker">Added in v0.18.2</p>
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
Install Vocalinux on NixOS with the Nix flake
</h1>
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
The Vocalinux repository is a Nix flake. It builds the app against
nixpkgs, ships a NixOS module and a Home Manager module, and wires
every text-injection and clipboard tool onto the wrapped
binary&rsquo;s PATH. Use it on NixOS, or on any Linux with Nix and
flakes enabled.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 inline-flex items-center gap-2 font-display text-2xl font-semibold">
<Terminal className="h-5 w-5 text-primary" aria-hidden="true" />
Try it or install into a profile
</h2>
<p className="mb-3 text-muted-foreground">
No configuration needed. <code>nix run</code> starts Vocalinux once;{" "}
<code>nix profile install</code> keeps it in your user profile:
</p>
<Commands>{TRY_COMMANDS}</Commands>
<p className="mt-4 text-sm text-muted-foreground">
On a non-NixOS host, pair <code>nix profile install</code> with your
distro&rsquo;s udev rule and group membership for{" "}
<code>/dev/uinput</code> and <code>/dev/input</code> if you use
Wayland.
</p>
</section>
<section className="mb-12">
<h2 className="mb-6 inline-flex items-center gap-2 font-display text-2xl font-semibold">
<Settings2 className="h-5 w-5 text-primary" aria-hidden="true" />
Enable the NixOS module
</h2>
<ol className="mb-6 space-y-3">
{moduleSteps.map((step, index) => (
<li
key={step.name}
className="rounded-[12px] border border-border bg-background p-5"
>
<h3 className="font-semibold">
{index + 1}. {step.name}
</h3>
<p className="mt-2 text-sm text-muted-foreground">{step.text}</p>
</li>
))}
</ol>
<p className="text-sm text-muted-foreground">
The documented flake, with <code>host</code> and <code>you</code>{" "}
replaced by your hostname and user:
</p>
<Commands>{NIXOS_SNIPPET}</Commands>
<p className="mt-4 text-sm text-muted-foreground">
<code>hardware.uinput.enable</code>, which the module sets, loads the
uinput kernel module, creates the <code>uinput</code> group and
installs the matching udev rule. ydotool and the evdev backend&rsquo;s
keyboard clone both open <code>/dev/uinput</code>. Group membership
stays per user because <code>input</code> grants read access to every
keyboard and pointer device.
</p>
<h3 className="mb-2 mt-8 font-semibold">Home Manager</h3>
<p className="text-sm text-muted-foreground">
<code>homeManagerModules.vocalinux</code> takes the same{" "}
<code>programs.vocalinux</code> options and installs into your home
profile. Device permissions are a system concern, so on NixOS keep the
NixOS module (or the group lines) on the system side.
</p>
<Commands>{HOME_MANAGER_SNIPPET}</Commands>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 inline-flex items-center gap-2 font-display text-2xl font-semibold">
<Braces className="h-5 w-5 text-primary" aria-hidden="true" />
What the flake provides
</h2>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th className="pb-3 pr-4 font-semibold">Flake output</th>
<th className="pb-3 font-semibold">Use</th>
</tr>
</thead>
<tbody>
{flakeOutputs.map((row) => (
<tr key={row.output} className="border-b border-border">
<td className="py-3 pr-4 font-mono text-xs">{row.output}</td>
<td className="py-3 text-muted-foreground">{row.purpose}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="mb-2 mt-6 font-semibold">Included in the package</h3>
<ul className="space-y-2 text-sm text-muted-foreground">
{[
"whisper.cpp (the default engine), with pywhispercpp built from its PyPI sdist with the Vulkan backend",
"Silero neural VAD through onnxruntime (the [vad] extra)",
"GTK 3, PyGObject and the AppIndicator typelib from nixpkgs",
"xdotool, wtype, ydotool, wl-clipboard, xclip, xsel, xprop, setxkbmap, ibus, gdbus, notify-send, paplay, aplay and vulkaninfo on PATH",
"A desktop entry and the app icon",
].map((item) => (
<li key={item} className="flex items-start gap-2">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary"
aria-hidden="true"
/>
{item}
</li>
))}
</ul>
<h3 className="mb-2 mt-6 font-semibold">Not included</h3>
<p className="text-sm text-muted-foreground">
The <code>vosk</code>, <code>whisper</code>, <code>parakeet</code> and{" "}
<code>faster_whisper</code> extras are not packaged, so{" "}
<Link href="/parakeet/">Parakeet</Link> and{" "}
<Link href="/faster-whisper/">Faster Whisper</Link> need the install
script on another distro. The flake follows{" "}
<code>nixos-unstable</code> and builds for x86_64-linux and
aarch64-linux.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 inline-flex items-center gap-2 font-display text-2xl font-semibold">
<AlertTriangle className="h-5 w-5 text-primary" aria-hidden="true" />
Gotchas
</h2>
<ul className="space-y-2 text-sm text-muted-foreground">
<li>
Do not run <code>install.sh</code> on NixOS. It expects a
conventional filesystem layout; the flake is the supported path.
</li>
<li>
Speech models are not part of the Nix store. Vocalinux downloads one
only after you confirm it, and shows the size first.
</li>
<li>
On Wayland without <code>input</code> membership, the hotkey is
disabled. Bind a compositor shortcut through{" "}
<Link href="/global-shortcuts/">D-Bus activation</Link> instead; it
toggles, it cannot push-to-talk.
</li>
<li>
On KDE Plasma Wayland, set System Settings → Keyboard → Virtual
Keyboard to &ldquo;IBus Wayland&rdquo;. More in the{" "}
<Link href="/wayland/">Wayland guide</Link>.
</li>
</ul>
</section>
<section className="mb-12">
<h2 className="mb-6 inline-flex items-center gap-2 font-display text-2xl font-semibold">
<CircleHelp className="h-5 w-5 text-primary" aria-hidden="true" />
NixOS FAQ
</h2>
<div className="space-y-4">
{faqs.map((faq) => (
<div
key={faq.question}
className="rounded-[12px] border border-border bg-background p-5"
>
<h3 className="mb-2 font-semibold">{faq.question}</h3>
<p className="text-sm text-muted-foreground">{faq.answer}</p>
</div>
))}
</div>
</section>
<section className="rounded-[12px] border border-border bg-muted p-8">
<h2 className="mb-4 font-display text-2xl font-semibold">
Other ways to install
</h2>
<p className="mb-6 text-muted-foreground">
Not on Nix? The install script, AUR, .deb, .rpm, AppImage and the
VocaHQ Flatpak remote cover most other systems.
</p>
<div className="flex flex-wrap gap-4">
<Link
href="/install/"
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
>
All install options
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Link>
<Link
href="/flatpak/"
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted"
>
Flatpak remote
</Link>
<Link
href="/gpu-acceleration/"
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted"
>
GPU acceleration
</Link>
</div>
</section>
</SeoSubpageShell>
);
}
+32 -3
View File
@@ -34,12 +34,42 @@ const distroGuides = [
"Install Vocalinux on Arch, Manjaro, and EndeavourOS with package prerequisites and tips.",
icon: Cpu,
},
{
name: "Debian",
href: "/install/debian/",
description: "Debian 12+ with the install script or the native .deb package.",
icon: Terminal,
},
{
name: "openSUSE",
href: "/install/opensuse/",
description: "Set up offline voice typing on openSUSE Tumbleweed.",
icon: Cpu,
},
{
name: "Linux Mint",
href: "/install/linux-mint/",
description: "Voice dictation on Linux Mint releases built on Ubuntu 24.04.",
icon: Laptop,
},
{
name: "Pop!_OS",
href: "/install/pop-os/",
description: "Install Vocalinux on Pop!_OS 24.04 and later.",
icon: Laptop,
},
{
name: "NixOS",
href: "/install/nixos/",
description: "Use the Nix flake or the NixOS module shipped since v0.18.2.",
icon: Terminal,
},
];
export const metadata: Metadata = buildPageMetadata({
title: "Install Voice Dictation on Linux (Ubuntu, Fedora, Arch)",
description:
"Step-by-step Linux voice dictation installation guides for Ubuntu, Fedora, and Arch. Install Vocalinux in minutes and start offline speech-to-text.",
"Linux voice dictation install guides for Ubuntu, Debian, Fedora, Arch, openSUSE, Mint, Pop!_OS, NixOS, and Flatpak. Start offline speech-to-text.",
path: "/install",
keywords: [
"install voice dictation linux",
@@ -98,8 +128,7 @@ export default function InstallGuidesPage() {
</a>
<a
className="install-alt"
href="https://github.com/VocaHQ/vocalinux/blob/main/docs/INSTALL.md#flatpak"
rel="noopener noreferrer"
href="/flatpak/"
>
<p className="install-alt-kicker">Auto-updates</p>
<h3>Flatpak</h3>
+470
View File
@@ -0,0 +1,470 @@
import Link from "next/link";
import { type Metadata } from "next";
import type { ReactNode } from "react";
import {
AlertTriangle,
ChevronRight,
ExternalLink,
Lock,
Server,
} from "lucide-react";
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
const PAGE_PATH = "/local-gateway";
const PAGE_TITLE = "Run a Local VocaGateway for Linux Dictation";
const PAGE_DESCRIPTION =
"Start a local VocaGateway from Vocalinux Settings with podman or docker, connect it via Remote API, and learn why gateway mode is not on-device transcription.";
const comparisonRows = [
{
mode: "Local engine (default whisper.cpp)",
audio: "Stays on this machine; transcribed in the app",
setup: "Download a model once",
network: "None for transcription",
},
{
mode: "Local VocaGateway on 127.0.0.1",
audio: "Sent over HTTP to the gateway container on this machine",
setup: "podman or docker with Compose",
network: "Loopback only by default",
},
{
mode: "Gateway or Remote API on another machine",
audio: "Leaves this machine for the endpoint you configured",
setup: "Server URL and token in Remote Server",
network: "Your LAN, VPN or the internet",
},
];
const statusRows = [
{ status: "Stopped", meaning: "Not running." },
{
status: "Starting",
meaning: "Compose up in progress, or the live probe is not green yet.",
},
{ status: "Live", meaning: "The gateway answers /health/live." },
{
status: "Pairable",
meaning:
"Live, plus a non-loopback phone URL and bearer token for pairing.",
},
{
status: "Ready",
meaning: "/health/ready returns 200 (a model is selected in the gateway).",
},
{
status: "Error",
meaning: "Start, stop or probe failure; the subtitle says which.",
},
];
const steps = [
{
name: "Open the Advanced section",
text: "Open Settings → Speech Model and expand Advanced. The Local VocaGateway group sits next to Remote Server.",
},
{
name: "Run the gateway",
text: "Click Run VocaGateway locally. The first start builds the local image vocagateway:v0.1.0 from the pinned tag, so it takes longer than later starts.",
},
{
name: "Wait for Ready",
text: "Watch the Status row. Select a model in the gateway's own web UI; the status shows Ready once the gateway reports it can transcribe.",
},
{
name: "Use this Gateway",
text: "Click Use this Gateway. Vocalinux fills Remote Server with the gateway URL and bearer token, sets the endpoint to /v1/audio/transcriptions and switches the engine to Remote API.",
},
{
name: "Dictate and verify",
text: "Use Test Connection in Remote Server, then dictate as usual with your normal shortcut.",
},
];
const faqs = [
{
question: "Is a local gateway the same as on-device dictation?",
answer:
"No. Even on 127.0.0.1, audio leaves the Vocalinux client and is transcribed by a separate gateway process over HTTP. The on-device path is a local engine such as whisper.cpp, which stays the default.",
},
{
question: "Does quitting Vocalinux stop the gateway?",
answer:
"No. The pinned compose file uses restart: unless-stopped, so the container keeps running after Quit. Use Stop local Gateway in Settings or the tray menu.",
},
{
question: "Can I run it from the Flatpak?",
answer:
"No. The Flatpak sandbox cannot reach the host podman or docker socket. Use the native package or AppImage, or start VocaGateway on the host yourself and point Remote Server at it.",
},
{
question: "Do I need a Voca account?",
answer:
"No. VocaGateway is optional self-hosted software. There is no Voca account and no hosted Voca cloud.",
},
{
question: "How do I go back to on-device transcription?",
answer:
"Choose a local engine again under Speech Engine in Settings → Speech Model → Advanced. Stop the gateway too if you no longer need it.",
},
];
export const metadata: Metadata = buildPageMetadata({
title: `${PAGE_TITLE} | Vocalinux`,
description: PAGE_DESCRIPTION,
path: PAGE_PATH,
keywords: [
"vocagateway linux",
"local speech gateway podman",
"self hosted dictation gateway",
"vocalinux remote api gateway",
"docker speech to text linux dictation",
"phone dictation gateway linux",
],
});
function InlineCode({ children }: { children: ReactNode }) {
return (
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[0.85em] text-foreground">
{children}
</code>
);
}
export default function LocalGatewayPage() {
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "TechArticle",
headline: PAGE_TITLE,
description: PAGE_DESCRIPTION,
dateModified: "2026-11-01",
author: {
"@type": "Person",
name: "Jatin K Malik",
url: "https://github.com/jatinkrmalik",
},
publisher: {
"@type": "Organization",
name: "Vocalinux",
logo: {
"@type": "ImageObject",
url: absoluteUrl("/vocalinux.png"),
},
},
mainEntityOfPage: absoluteUrl(PAGE_PATH),
},
{
"@type": "HowTo",
name: "Run a local VocaGateway from Vocalinux Settings",
step: steps.map((step, index) => ({
"@type": "HowToStep",
position: index + 1,
name: step.name,
text: step.text,
})),
},
{
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: { "@type": "Answer", text: faq.answer },
})),
},
],
};
return (
<SeoSubpageShell>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<section>
<p className="subpage-kicker">Added in v0.18.0 · optional</p>
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
Run a local VocaGateway from Vocalinux
</h1>
<p className="mb-6 max-w-4xl text-lg text-muted-foreground">
Vocalinux can start{" "}
<a
href="https://vocagateway.vocahq.com/"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline"
>
VocaGateway
</a>
, the self-hosted speech gateway in the Voca family, as a podman or
docker container from its own Settings. It is a power-user path for
people who want one transcription service shared by the desktop and a
paired phone.
</p>
</section>
<section className="bg-primary/5 mb-12 rounded-[12px] border border-border p-6">
<h2 className="mb-3 flex items-center gap-2 text-xl font-semibold">
<Lock className="h-5 w-5 text-primary" aria-hidden="true" />
Gateway mode is not the on-device engine
</h2>
<p className="text-sm text-muted-foreground">
With a local engine, audio is transcribed on this machine by Vocalinux
itself. In gateway mode it is sent over HTTP to the gateway, even when
that gateway runs on <InlineCode>127.0.0.1</InlineCode>. If you point
Vocalinux at a gateway on another machine, your audio leaves this
machine for that endpoint. Keep it on a trusted LAN, an encrypted
private network such as Tailscale, or HTTPS, and never expose port
8765 to the public internet. Enabling the gateway does not change the
default engine; whisper.cpp stays on-device until you click Use this
Gateway.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Where your audio goes
</h2>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th scope="col" className="pb-3 pr-4 font-semibold">
Mode
</th>
<th scope="col" className="pb-3 pr-4 font-semibold">
Audio
</th>
<th scope="col" className="pb-3 pr-4 font-semibold">
Setup
</th>
<th scope="col" className="pb-3 font-semibold">
Network
</th>
</tr>
</thead>
<tbody>
{comparisonRows.map((row) => (
<tr key={row.mode} className="border-b border-border align-top">
<th scope="row" className="py-3 pr-4 font-medium">
{row.mode}
</th>
<td className="py-3 pr-4 text-muted-foreground">
{row.audio}
</td>
<td className="py-3 pr-4 text-muted-foreground">
{row.setup}
</td>
<td className="py-3 text-muted-foreground">{row.network}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
<Server className="h-5 w-5 text-primary" aria-hidden="true" />
Requirements
</h2>
<ul className="list-disc space-y-2 pl-5 text-sm text-muted-foreground">
<li>
podman (preferred) or docker, with a working Compose plugin, or{" "}
<InlineCode>podman-compose</InlineCode> /{" "}
<InlineCode>docker-compose</InlineCode>. If the engine is present
but Compose is missing, Settings tells you which to install.
</li>
<li>
A native install or the AppImage. The AppImage does not bundle a
container engine; it uses the host&apos;s podman or docker socket
when available.
</li>
<li>
Not the Flatpak: its sandbox cannot reach the host container socket,
so the controls are blocked there.
</li>
</ul>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">Set it up</h2>
<ol className="space-y-4">
{steps.map((step, index) => (
<li
key={step.name}
className="rounded-[12px] border border-border bg-background p-5"
>
<div className="mb-2 flex items-center gap-3">
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
{index + 1}
</span>
<h3 className="font-semibold">{step.name}</h3>
</div>
<p className="ml-10 text-sm text-muted-foreground">{step.text}</p>
</li>
))}
</ol>
<h3 className="mb-2 mt-6 font-semibold">Status labels</h3>
<div className="overflow-x-auto rounded-[12px] border border-border bg-background p-5">
<table className="w-full text-left text-sm">
<tbody>
{statusRows.map((row) => (
<tr
key={row.status}
className="border-b border-border last:border-0"
>
<th scope="row" className="py-2 pr-4 font-medium">
{row.status}
</th>
<td className="py-2 text-muted-foreground">{row.meaning}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mt-3 text-sm text-muted-foreground">
Pairable can appear before Ready, so a phone can pair while a model is
still downloading.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
What Vocalinux sets up
</h2>
<ul className="list-disc space-y-2 pl-5 text-sm text-muted-foreground">
<li>
A checkout of VocaGateway pinned to the{" "}
<InlineCode>v0.1.0</InlineCode> tag under your XDG cache, run as the
Compose project <InlineCode>vocagateway</InlineCode> with the{" "}
<InlineCode>cpu</InlineCode> profile. It never floats on{" "}
<InlineCode>latest</InlineCode>;{" "}
<InlineCode>VOCAGATEWAY_IMAGE</InlineCode> overrides the image.
</li>
<li>
A private bootstrap token in{" "}
<InlineCode>~/.config/vocalinux/gateway_embed/</InlineCode> with
mode 600, plus a matching Compose <InlineCode>.env</InlineCode>.
</li>
<li>
A port published on <InlineCode>127.0.0.1:8765</InlineCode> by
default, reachable from this desktop only.
</li>
<li>
The usual Remote API settings. Use this Gateway writes the URL,
token, endpoint and the model name{" "}
<InlineCode>whisper-1</InlineCode> into the same{" "}
<InlineCode>remote_api_*</InlineCode> keys described on the{" "}
<Link href="/remote-api/" className="text-primary underline">
Remote API page
</Link>
. The token is stored in plain JSON in{" "}
<InlineCode>config.json</InlineCode>, like any Remote API key.
</li>
</ul>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
<AlertTriangle className="h-5 w-5 text-primary" aria-hidden="true" />
LAN access, stopping and gotchas
</h2>
<h3 className="mb-2 font-semibold">Allow LAN access for Phone</h3>
<p className="mb-4 text-sm text-muted-foreground">
This switch publishes the gateway on{" "}
<InlineCode>0.0.0.0:8765</InlineCode> and advertises a LAN URL for
phone pairing. Open port 8765 in your firewall only on networks you
trust. If the switch fails, Compose keeps the previous bind and the
switch snaps back with an error; stop and run again, or retry.
</p>
<h3 className="mb-2 font-semibold">Quit does not stop it</h3>
<p className="mb-4 text-sm text-muted-foreground">
The container keeps running after you quit Vocalinux, including a
LAN-published one. Use Stop local Gateway in Settings or the tray
menu, which appears whenever Compose is running. Stopping keeps the
named data volume, so downloaded models survive. Vocalinux does not
start the gateway on login.
</p>
<h3 className="mb-2 font-semibold">Other machines</h3>
<p className="text-sm text-muted-foreground">
You can also run VocaGateway on a stronger box and enter its address
in Remote Server by hand. Audio then crosses your network, so prefer a
trusted LAN, Tailscale or HTTPS. Read the{" "}
<a
href="https://vocagateway.vocahq.com/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-primary underline"
>
VocaGateway documentation
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>{" "}
for server-side setup.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">FAQ</h2>
<div className="space-y-4">
{faqs.map((faq) => (
<div
key={faq.question}
className="rounded-[12px] border border-border bg-background p-5"
>
<h3 className="mb-2 font-semibold">{faq.question}</h3>
<p className="text-sm text-muted-foreground">{faq.answer}</p>
</div>
))}
</div>
<p className="mt-4 text-sm text-muted-foreground">
Related:{" "}
<Link href="/offline/" className="text-primary underline">
offline dictation
</Link>
,{" "}
<Link href="/privacy/" className="text-primary underline">
privacy
</Link>
,{" "}
<Link href="/flatpak/" className="text-primary underline">
Flatpak limitations
</Link>{" "}
and{" "}
<Link href="/gpu-acceleration/" className="text-primary underline">
GPU acceleration
</Link>{" "}
for faster on-device engines.
</p>
</section>
<section className="rounded-[12px] border border-border bg-muted p-8">
<h2 className="mb-4 font-display text-2xl font-semibold">
Start on-device, add a gateway later
</h2>
<p className="mb-6 text-muted-foreground">
Install Vocalinux with the default on-device engine. Add a local
VocaGateway only if you need a shared transcription service.
</p>
<div className="flex flex-wrap gap-4">
<Link
href="/install/"
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
>
Install Vocalinux
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Link>
<Link
href="/remote-api/"
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted"
>
Remote API settings
</Link>
</div>
</section>
</SeoSubpageShell>
);
}
+495
View File
@@ -0,0 +1,495 @@
import Link from "next/link";
import { type Metadata } from "next";
import {
CheckCircle2,
ChevronRight,
Globe,
Keyboard,
Languages,
} from "lucide-react";
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
const PAGE_PATH = "/multilingual-dictation";
// Mirrors SUPPORTED_LANGUAGES in src/vocalinux/utils/vosk_model_info.py (v0.18.5).
const catalog: { code: string; name: string; vosk: boolean }[] = [
{ code: "en-us", name: "English (US)", vosk: true },
{ code: "en-in", name: "English (India)", vosk: true },
{ code: "ar", name: "Arabic", vosk: true },
{ code: "bn", name: "Bengali", vosk: false },
{ code: "ca", name: "Catalan", vosk: true },
{ code: "zh", name: "Chinese", vosk: true },
{ code: "cs", name: "Czech", vosk: true },
{ code: "da", name: "Danish", vosk: false },
{ code: "nl", name: "Dutch", vosk: true },
{ code: "fi", name: "Finnish", vosk: false },
{ code: "fr", name: "French", vosk: true },
{ code: "de", name: "German", vosk: true },
{ code: "el", name: "Greek", vosk: false },
{ code: "he", name: "Hebrew", vosk: false },
{ code: "hi", name: "Hindi", vosk: true },
{ code: "hu", name: "Hungarian", vosk: false },
{ code: "id", name: "Indonesian", vosk: false },
{ code: "it", name: "Italian", vosk: true },
{ code: "ja", name: "Japanese", vosk: true },
{ code: "ko", name: "Korean", vosk: true },
{ code: "no", name: "Norwegian", vosk: false },
{ code: "fa", name: "Persian", vosk: true },
{ code: "pl", name: "Polish", vosk: true },
{ code: "pt", name: "Portuguese", vosk: true },
{ code: "ro", name: "Romanian", vosk: false },
{ code: "ru", name: "Russian", vosk: true },
{ code: "sl", name: "Slovenian", vosk: false },
{ code: "es", name: "Spanish", vosk: true },
{ code: "sv", name: "Swedish", vosk: true },
{ code: "ta", name: "Tamil", vosk: false },
{ code: "th", name: "Thai", vosk: false },
{ code: "tr", name: "Turkish", vosk: true },
{ code: "uk", name: "Ukrainian", vosk: true },
{ code: "vi", name: "Vietnamese", vosk: true },
];
const engineRows = [
{
engine: "whisper.cpp (default)",
picker: "Full catalog",
layout: "Yes",
shortcuts: "Yes",
note: "Needs a multilingual model, not a .en one. Also offers Language Candidates.",
},
{
engine: "Whisper, Faster Whisper",
picker: "Full catalog",
layout: "Yes",
shortcuts: "Yes",
note: "Needs a multilingual model.",
},
{
engine: "Remote API",
picker: "Full catalog",
layout: "Yes",
shortcuts: "Yes",
note: "The language is sent with each request to the server you configured.",
},
{
engine: "VOSK",
picker: "Languages with an official model",
layout: "No",
shortcuts: "No",
note: "One model per language, so switching means a model reload.",
},
{
engine: "Parakeet",
picker: "Not used",
layout: "No",
shortcuts: "No",
note: "Language comes from the model: v3-european or v2-english.",
},
];
const howToSteps = [
{
name: "Use a multilingual model",
text: "In Settings → Speech Model, pick a language other than English or a multilingual model. English-only models, the ones with .en in the name, cannot switch language.",
},
{
name: "Add a language shortcut",
text: "Open Settings → Dictation, find Language Shortcuts and click Add Language Shortcut.",
},
{
name: "Pick the language and record a key",
text: "Choose the language in the new row, click Record and press a modifier plus a key such as ctrl+alt+d, or a function key such as f10.",
},
{
name: "Dictate with it",
text: "Press the language shortcut instead of your main one. It follows the shortcut mode above it, so hold it for push-to-talk or press it to toggle. Your main language comes back after that dictation.",
},
];
const faqs = [
{
q: "How many languages does Vocalinux support?",
a: "The language catalog has 33 languages plus Auto-detect. English appears twice, as English (US) and English (India), so the list has 34 entries. Slovenian was added in v0.18.4.",
},
{
q: "Can Vocalinux switch language in the middle of a sentence?",
a: "No. Whisper-family engines take one language per dictation, or detect it once per utterance on Auto-detect. For mixed-language work, use Follow keyboard layout or a language shortcut so each dictation starts in the right language.",
},
{
q: "Follow keyboard layout always picks the same language. Why?",
a: "Reading the active layout uses GNOME's input-source settings. On other desktops Vocalinux falls back to the configured primary layout, which does not change when you switch. A layout that maps to no catalog language falls back to auto-detect.",
},
{
q: "My language shortcut plays an error sound. What is wrong?",
a: "The loaded model cannot dictate in that language without a reload: an English-only Whisper model, a VOSK model for another language, or Parakeet. Vocalinux refuses rather than dictating in the wrong language. Choose a multilingual Whisper-family model.",
},
{
q: "Do voice commands work in other languages?",
a: "Punctuation and line-break phrases also match in Italian, French, German, Spanish, Portuguese, Dutch, Polish and Russian. Editing commands such as delete that and undo are English-only.",
},
];
export const metadata: Metadata = buildPageMetadata({
title: "Multilingual Voice Dictation on Linux: 33 Languages | Vocalinux",
description:
"Dictate in more than one language on Linux: per-language shortcuts, follow your keyboard layout, 33 languages plus auto-detect, and per-engine limits.",
path: PAGE_PATH,
keywords: [
"multilingual dictation Linux",
"bilingual voice typing Linux",
"switch dictation language shortcut",
"follow keyboard layout dictation",
"Whisper multilingual dictation",
"offline speech to text languages Linux",
],
});
export default function MultilingualDictationPage() {
const articleJsonLd = {
"@context": "https://schema.org",
"@type": "TechArticle",
headline: "Multilingual Voice Dictation on Linux with Vocalinux",
description:
"How to dictate in more than one language with Vocalinux: the language catalog, per-language shortcuts, following the keyboard layout and engine caveats.",
dateModified: "2026-11-01",
author: {
"@type": "Person",
name: "Jatin K Malik",
url: "https://github.com/jatinkrmalik",
},
publisher: {
"@type": "Organization",
name: "Vocalinux",
logo: {
"@type": "ImageObject",
url: absoluteUrl("/vocalinux.png"),
},
},
mainEntityOfPage: absoluteUrl(PAGE_PATH),
};
const howToJsonLd = {
"@context": "https://schema.org",
"@type": "HowTo",
name: "Set up a per-language dictation shortcut in Vocalinux",
step: howToSteps.map((step, index) => ({
"@type": "HowToStep",
position: index + 1,
name: step.name,
text: step.text,
})),
};
const faqJsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((item) => ({
"@type": "Question",
name: item.q,
acceptedAnswer: { "@type": "Answer", text: item.a },
})),
};
return (
<SeoSubpageShell>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(howToJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<section>
<p className="subpage-kicker">Added in v0.18.0</p>
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
Multilingual dictation on Linux
</h1>
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
Per-language shortcuts and Follow keyboard layout arrived in v0.18.0.
If you write in more than one language, Vocalinux gives you three ways
to get the right one each time: auto-detect per utterance, a separate
shortcut per language, or following whichever keyboard layout you are
typing with. This page covers each, which engines support them, and
where they fall short. For a per-language view of engine support, see
the <Link href="/languages/">languages page</Link>.
</p>
</section>
<section className="mb-12 grid gap-4 sm:grid-cols-3">
<div className="rounded-[12px] border border-border bg-background p-5">
<Globe className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h3 className="mb-2 font-semibold">Auto-detect</h3>
<p className="text-sm text-muted-foreground">
The engine guesses the language for each utterance. Easiest to set
up, but short clips can be guessed wrong.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Keyboard className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h3 className="mb-2 font-semibold">Language shortcuts</h3>
<p className="text-sm text-muted-foreground">
A dedicated key per language. Explicit and reliable, for one
dictation at a time.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Languages className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h3 className="mb-2 font-semibold">Follow keyboard layout</h3>
<p className="text-sm text-muted-foreground">
Switch layout to switch dictation language. Best on GNOME, where the
active layout can be read.
</p>
</div>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
Choosing a main language
</h2>
<p className="mb-4 text-muted-foreground">
Settings → Speech Model opens with a simple Language group. Pick your
Main language from the searchable list. Turning on Other languages
makes the engine guess the language each time; the optional Other
language picker just records which one you had in mind, because
Whisper-family engines take a single language or none. On first run,
Vocalinux seeds the language from your keyboard layout and locale
(since v0.17.0).
</p>
<p className="text-muted-foreground">
Picking a language other than English switches off an English-only
model. From the command line, <code>--language</code> accepts{" "}
<code>auto</code> or any catalog code below.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
Per-language dictation shortcuts
</h2>
<ol className="space-y-4">
{howToSteps.map((step, index) => (
<li
key={step.name}
className="rounded-[12px] border border-border bg-background p-5"
>
<div className="mb-2 flex items-center gap-3">
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
{index + 1}
</span>
<h3 className="font-semibold">{step.name}</h3>
</div>
<p className="ml-10 text-sm text-muted-foreground">{step.text}</p>
</li>
))}
</ol>
<p className="mt-4 text-sm text-muted-foreground">
Bindings take effect immediately. A binding can also point at
Auto-detect. If two rows use the same key, the first one wins. More on
shortcut syntax and modes is on the{" "}
<Link href="/shortcuts/">shortcuts page</Link>.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
Following the keyboard layout
</h2>
<p className="mb-4 text-muted-foreground">
Expand Advanced on the Speech Model page and turn on Follow keyboard
layout. Vocalinux then re-reads the active layout at the start of
every dictation and uses the matching language. The Language picker
greys out and shows what the current layout resolves to; turning the
switch off pins that language.
</p>
<ul className="list-disc space-y-2 pl-5 text-sm text-muted-foreground">
<li>
Available for whisper.cpp, Whisper and Faster Whisper with a
multilingual model, and for the Remote API.
</li>
<li>
The active layout is read from GNOME&apos;s input-source settings.
Some IBus input methods map too, for example Mozc and Anthy to
Japanese, Pinyin and Rime to Chinese, and Hangul to Korean.
</li>
<li>
On other desktops Vocalinux uses the configured primary layout,
which does not follow your switches. A layout with no catalog
language falls back to auto-detect.
</li>
<li>
A language shortcut wins over the layout for that one dictation.
</li>
</ul>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
The settings in config.json
</h2>
<p className="mb-3 text-sm text-muted-foreground">
Settings writes these to <code>~/.config/vocalinux/config.json</code>.
The value <code>&quot;layout&quot;</code> is how Follow keyboard
layout is stored:
</p>
<pre className="subpage-terminal overflow-x-auto p-4">
<code>{`{
"speech_recognition": {
"language": "layout"
},
"shortcuts": {
"language_shortcuts": [
{ "shortcut": "ctrl+alt+d", "language": "de" },
{ "shortcut": "f10", "language": "fr" }
]
}
}`}</code>
</pre>
<p className="mt-3 text-sm text-muted-foreground">
On whisper.cpp, Settings → Advanced (after Unlock Advanced Settings)
also has Language Candidates: a comma-separated list such as{" "}
<code>en,es</code> that restricts auto-detect to those languages.
Leave it blank to consider all languages.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Engine caveats
</h2>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th className="pb-3 pr-4 font-semibold">Engine</th>
<th className="pb-3 pr-4 font-semibold">Language picker</th>
<th className="pb-3 pr-4 font-semibold">Follow layout</th>
<th className="pb-3 pr-4 font-semibold">Language shortcuts</th>
<th className="pb-3 font-semibold">Notes</th>
</tr>
</thead>
<tbody>
{engineRows.map((row) => (
<tr key={row.engine} className="border-b border-border">
<td className="py-3 pr-4 font-medium">{row.engine}</td>
<td className="py-3 pr-4 text-muted-foreground">
{row.picker}
</td>
<td className="py-3 pr-4 text-muted-foreground">
{row.layout}
</td>
<td className="py-3 pr-4 text-muted-foreground">
{row.shortcuts}
</td>
<td className="py-3 text-muted-foreground">{row.note}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mt-4 text-sm text-muted-foreground">
VOSK lists only languages that have an official Alphacephei model, 21
of the 33. Parakeet&apos;s v3-european model covers 25 European
languages and ignores the picker; see{" "}
<Link href="/parakeet/">Parakeet on Linux</Link>. For Whisper model
sizes, see the{" "}
<Link href="/whisper-model-guide/">Whisper model guide</Link>. The
Remote API sends your audio to the endpoint you configured; details
are on the <Link href="/remote-api/">Remote API page</Link>.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
The language catalog
</h2>
<p className="mb-4 text-muted-foreground">
33 languages plus Auto-detect, shared by Settings, language shortcuts
and the CLI. Slovenian was added in v0.18.4.
</p>
<div className="overflow-x-auto rounded-[12px] border border-border bg-background p-6">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th className="pb-3 pr-4 font-semibold">Language</th>
<th className="pb-3 pr-4 font-semibold">Code</th>
<th className="pb-3 font-semibold">VOSK model</th>
</tr>
</thead>
<tbody>
{catalog.map((lang) => (
<tr key={lang.code} className="border-b border-border">
<td className="py-2 pr-4 font-medium">{lang.name}</td>
<td className="py-2 pr-4">
<code>{lang.code}</code>
</td>
<td className="py-2 text-muted-foreground">
{lang.vosk ? (
<span className="inline-flex items-center gap-1.5">
<CheckCircle2
className="h-3.5 w-3.5 text-primary"
aria-hidden="true"
/>
Yes
</span>
) : (
"No (Whisper-family only)"
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">FAQ</h2>
<div className="space-y-4">
{faqs.map((item) => (
<div
key={item.q}
className="rounded-[12px] border border-border bg-background p-5"
>
<h3 className="mb-2 font-semibold">{item.q}</h3>
<p className="text-sm text-muted-foreground">{item.a}</p>
</div>
))}
</div>
</section>
<section className="rounded-[12px] border border-border bg-muted p-8">
<h2 className="mb-4 font-display text-2xl font-semibold">
Dictate in your languages
</h2>
<p className="mb-6 text-muted-foreground">
Install Vocalinux, keep the default whisper.cpp engine with a
multilingual model, and add a shortcut for each language you write in.
Names and jargon in any language can go in the{" "}
<Link href="/custom-dictionary/">custom dictionary</Link>.
</p>
<div className="flex flex-wrap gap-4">
<Link
href="/install/"
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
>
Install Vocalinux
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Link>
<Link
href="/languages/"
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted"
>
Supported languages
</Link>
</div>
</section>
</SeoSubpageShell>
);
}
+9 -5
View File
@@ -368,10 +368,14 @@ export default function HomePage() {
</div>
<div className="workbench-meta">
<span className="chip">whisper.cpp default</span>
<span className="chip">Faster Whisper</span>
<Link className="chip" href="/faster-whisper/">
Faster Whisper
</Link>
<span className="chip">Whisper</span>
<span className="chip">VOSK</span>
<span className="chip">Parakeet</span>
<Link className="chip" href="/parakeet/">
Parakeet
</Link>
<span className="chip">Remote API</span>
</div>
</div>
@@ -1035,12 +1039,12 @@ export default function HomePage() {
</span>
iPhone + Android
</span>
<span className="chip">Android beta / iOS TestFlight</span>
<span className="chip">Android on Google Play / iOS TestFlight</span>
</div>
<h3>VocaPhone</h3>
<p>
Android beta and iOS TestFlight. On-device first, gateway
optional.
Android on Google Play and iOS TestFlight. On-device first,
gateway optional.
</p>
</a>
<a
+661
View File
@@ -0,0 +1,661 @@
import Link from "next/link";
import { type Metadata } from "next";
import {
CheckCircle2,
ChevronRight,
Cpu,
Download,
Globe,
Lock,
XCircle,
} from "lucide-react";
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
const installCommands = [
"curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh -o /tmp/vl.sh",
"bash /tmp/vl.sh --auto --engine=parakeet",
];
const setupSteps = [
{
title: "Install the Parakeet engine",
text: "Parakeet is an optional extra. Download the installer and run it with --engine=parakeet. The interactive engine menu does not list Parakeet, so pass the flag. The installer adds sherpa-onnx and points your config at the Parakeet engine.",
},
{
title: "Open Settings → Speech Model",
text: "Open Settings from the tray icon and go to Speech Model. Expand Advanced to see the Engine and Model size pickers.",
},
{
title: "Pick Parakeet and a model",
text: "Set Engine to Parakeet, then choose v3-european, v2-english or orukeet-v0.1.0 under Model size. Picking only selects the model; nothing downloads yet.",
},
{
title: "Confirm the download",
text: "If the model is not on disk, the info card shows its size and a Download button. The transfer starts only after you confirm it. Each file is checked against a pinned SHA-256 before Parakeet loads it.",
},
{
title: "Dictate",
text: "Hold your dictation shortcut (Right Alt by default on new installs), speak, and release. Text is typed into the focused app.",
},
];
const models = [
{
id: "v3-european",
name: "Parakeet TDT 0.6B v3 (int8)",
languages: "25 European languages",
size: "about 639 MB",
notes: "Default and recommended Parakeet model.",
},
{
id: "v2-english",
name: "Parakeet TDT 0.6B v2 (int8)",
languages: "English only",
size: "about 630 MB",
notes: "Pick this if you only dictate in English.",
},
{
id: "orukeet-v0.1.0",
name: "Orukeet v0.1.0 (int8)",
languages: "25 European languages",
size: "about 641 MiB",
notes: "Optional, added in v0.18.0. Weights licensed CC BY-SA 4.0.",
},
];
const packages = [
{
name: "install.sh / source install",
parakeet: true,
note: "Use --engine=parakeet",
},
{
name: "AUR (vocalinux)",
parakeet: true,
note: "Listed as an optional dependency: python-sherpa-onnx",
},
{
name: "AppImage (and AUR vocalinux-bin)",
parakeet: false,
note: "Ships whisper.cpp only",
},
{ name: "Flatpak", parakeet: false, note: "Ships whisper.cpp only" },
{ name: "Snap", parakeet: false, note: "Ships whisper.cpp only" },
{
name: ".deb / .rpm",
parakeet: false,
note: "Thin packages; whisper.cpp on CPU",
},
{
name: "Nix flake",
parakeet: false,
note: "Parakeet extra is not packaged",
},
];
const comparison = [
{
topic: "Runtime",
parakeet: "sherpa-onnx running NVIDIA NeMo transducer models",
whisperCpp: "whisper.cpp through pywhispercpp",
},
{
topic: "Hardware",
parakeet: "CPU",
whisperCpp: "Vulkan (AMD, Intel, NVIDIA), CUDA, or CPU",
},
{
topic: "Languages",
parakeet: "25 European languages (v3) or English (v2)",
whisperCpp: "Full language catalog plus Auto-detect",
},
{
topic: "Language picker",
parakeet: "Ignored; language is treated as auto",
whisperCpp: "Used, including Follow keyboard layout",
},
{
topic: "Model choice",
parakeet: "Three models of roughly the same size",
whisperCpp: "Tiny to Large v3, with quantized and .en variants",
},
{
topic: "Custom dictionary terms",
parakeet: "Not applied (corrections still work)",
whisperCpp: "Terms bias recognition; corrections work",
},
{
topic: "Packages",
parakeet: "install.sh, source, AUR optional dependency",
whisperCpp: "Every package",
},
];
const faqs = [
{
question: "Is Parakeet better than whisper.cpp?",
answer:
"It depends on your languages and hardware. Parakeet is a strong CPU option if you dictate in English or one of the 25 European languages its v3 model covers. whisper.cpp stays the default because it covers the whole language catalog, can use a Vulkan or CUDA GPU, and ships in every package. Try both on your own voice and machine.",
},
{
question: "Does Parakeet send my audio anywhere?",
answer:
"No. Parakeet is a local engine. After the model download, recognition runs on your machine and audio is not sent to a server. Only the Remote API engine and VocaGateway send audio to an endpoint you configure.",
},
{
question: "Why is the Language picker greyed out or ignored with Parakeet?",
answer:
"Parakeet's language coverage comes from the model, not from the Vocalinux language catalog. Vocalinux treats the language as auto for Parakeet, so pick v2-english for English only or v3-european for the European set. Follow keyboard layout is not available for Parakeet.",
},
{
question: "Can I use Parakeet from the AppImage, Flatpak or Snap?",
answer:
"No. Those packages ship whisper.cpp only. Use the install.sh installer or a source install with --engine=parakeet, or the AUR package with python-sherpa-onnx installed.",
},
{
question: "What is Orukeet?",
answer:
"Orukeet v0.1.0 is an optional model for the Parakeet engine, added in v0.18.0. It runs on the same local sherpa-onnx recognizer, covers 25 European languages, takes about 641 MiB, and its weights are licensed CC BY-SA 4.0. Vocalinux downloads it from a pinned Hugging Face revision and checks every file against bundled SHA-256 pins.",
},
];
export const metadata: Metadata = buildPageMetadata({
title: "Parakeet Speech Recognition on Linux | Vocalinux",
description:
"Run NVIDIA Parakeet TDT speech recognition offline on Linux with Vocalinux: install, pick v3-european, v2-english or Orukeet, languages, packages and fixes.",
path: "/parakeet",
keywords: [
"Parakeet speech recognition Linux",
"Parakeet TDT 0.6B Linux",
"NVIDIA Parakeet offline dictation",
"sherpa-onnx Parakeet",
"Orukeet model",
"Parakeet vs whisper.cpp",
"offline voice typing Linux CPU",
],
});
export default function ParakeetPage() {
const jsonLd = [
{
"@context": "https://schema.org",
"@type": "TechArticle",
headline: "Parakeet speech recognition on Linux",
description:
"How to install and use the Parakeet speech engine in Vocalinux on Linux, including model variants, languages, package availability and troubleshooting.",
dateModified: "2026-11-01",
author: {
"@type": "Person",
name: "Jatin K Malik",
url: "https://github.com/jatinkrmalik",
},
publisher: {
"@type": "Organization",
name: "Vocalinux",
logo: {
"@type": "ImageObject",
url: absoluteUrl("/vocalinux.png"),
},
},
mainEntityOfPage: absoluteUrl("/parakeet"),
},
{
"@context": "https://schema.org",
"@type": "HowTo",
name: "Set up the Parakeet engine in Vocalinux",
step: setupSteps.map((step, index) => ({
"@type": "HowToStep",
position: index + 1,
name: step.title,
text: step.text,
})),
},
{
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: { "@type": "Answer", text: faq.answer },
})),
},
];
return (
<SeoSubpageShell>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<section>
<p className="subpage-kicker">
<Cpu className="h-4 w-4" aria-hidden="true" />
Added in v0.17.0
</p>
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
Parakeet speech recognition on Linux
</h1>
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
Parakeet is an optional local speech engine in Vocalinux. It runs
NVIDIA&apos;s Parakeet TDT 0.6B models on your CPU through sherpa-onnx
and types what you say into the focused app, with no cloud
transcription. It suits people who dictate in English or a European
language and want a CPU engine with a short list of models to choose
from.
</p>
</section>
<section className="mb-12 grid gap-4 sm:grid-cols-3">
<div className="rounded-[12px] border border-border bg-background p-5">
<Lock className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<p className="mb-2 font-semibold text-foreground">On-device</p>
<p className="text-sm text-muted-foreground">
After the one-time model download, recognition runs locally. Audio
stays on your machine.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Globe className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<p className="mb-2 font-semibold text-foreground">
25 European languages
</p>
<p className="text-sm text-muted-foreground">
The default v3-european model covers 25 European languages.
v2-english is English only.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Download className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<p className="mb-2 font-semibold text-foreground">
Verified downloads
</p>
<p className="text-sm text-muted-foreground">
Models come from pinned Hugging Face revisions and are checked
against SHA-256 pins before loading.
</p>
</div>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
What is Parakeet?
</h2>
<div className="max-w-4xl space-y-4 text-muted-foreground">
<p>
Parakeet is a family of automatic speech recognition models from
NVIDIA&apos;s NeMo toolkit. Vocalinux uses the TDT 0.6B models,
which are transducer models: they turn audio straight into text
tokens rather than generating text the way Whisper does. Vocalinux
loads int8 ONNX exports of these models with{" "}
<a
href="https://github.com/k2-fsa/sherpa-onnx"
className="text-primary underline"
rel="noopener noreferrer"
>
sherpa-onnx
</a>
, a speech runtime that runs on an ordinary CPU.
</p>
<p>
Parakeet arrived in Vocalinux v0.17.0 alongside Faster Whisper.
whisper.cpp is still the default engine; Parakeet is something you
opt into.
</p>
</div>
</section>
<section className="mb-12">
<h2 className="mb-6 font-display text-2xl font-semibold">
Install and select Parakeet
</h2>
<div className="mb-6 rounded-lg bg-[color:var(--terminal)] p-4">
{installCommands.map((command) => (
<code
key={command}
className="block text-sm text-[color:var(--terminal-fg)]"
>
{command}
</code>
))}
</div>
<ol className="space-y-4">
{setupSteps.map((step, index) => (
<li
key={step.title}
className="rounded-[12px] border border-border bg-background p-5"
>
<div className="mb-2 flex items-center gap-3">
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
{index + 1}
</span>
<h3 className="font-semibold">{step.title}</h3>
</div>
<p className="ml-10 text-sm text-muted-foreground">{step.text}</p>
</li>
))}
</ol>
<p className="mt-4 text-sm text-muted-foreground">
To start once with Parakeet from a terminal, run{" "}
<code className="rounded bg-muted px-1">
vocalinux --engine parakeet
</code>
. In{" "}
<code className="rounded bg-muted px-1">
~/.config/vocalinux/config.json
</code>{" "}
the engine name is{" "}
<code className="rounded bg-muted px-1">parakeet</code>.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Parakeet models and Orukeet
</h2>
<p className="mb-4 text-sm text-muted-foreground">
The Model size picker lists three models for the Parakeet engine.
Sizes are the catalog values Vocalinux shows before a download.
</p>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th className="pb-3 pr-4 font-semibold">Model ID</th>
<th className="pb-3 pr-4 font-semibold">Model</th>
<th className="pb-3 pr-4 font-semibold">Languages</th>
<th className="pb-3 pr-4 font-semibold">Download</th>
<th className="pb-3 font-semibold">Notes</th>
</tr>
</thead>
<tbody>
{models.map((model) => (
<tr key={model.id} className="border-b border-border">
<td className="py-3 pr-4 font-mono text-xs">{model.id}</td>
<td className="py-3 pr-4">{model.name}</td>
<td className="py-3 pr-4 text-muted-foreground">
{model.languages}
</td>
<td className="py-3 pr-4 text-muted-foreground">
{model.size}
</td>
<td className="py-3 text-muted-foreground">{model.notes}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="mb-2 mt-6 font-semibold">
About the optional Orukeet model
</h3>
<p className="text-sm text-muted-foreground">
Orukeet v0.1.0 was added in v0.18.0. It uses the same local
sherpa-onnx recognizer and covers 25 European languages; the
recommended Parakeet model and the default whisper.cpp engine are
unchanged. Vocalinux downloads it from a pinned revision of{" "}
<a
href="https://huggingface.co/oruk/orukeet"
className="text-primary underline"
rel="noopener noreferrer"
>
oruk/orukeet on Hugging Face
</a>
, checks every file against bundled SHA-256 pins, and checks the
publisher&apos;s release manifest against those same pins. Its CC
BY-SA 4.0 weights license and NVIDIA attribution are saved with the
model.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">Languages</h2>
<div className="max-w-4xl space-y-4 text-muted-foreground">
<p>
With Parakeet, the model decides the language, not the Language
picker. Vocalinux treats the language as auto for this engine, so a
language you chose for whisper.cpp is not passed to Parakeet. Choose
v2-english if you only speak English, or v3-european (or Orukeet)
for the 25-language European set.
</p>
<p>
Two related features do not apply to Parakeet:{" "}
<strong>Follow keyboard layout</strong> works with whisper.cpp,
Whisper, Faster Whisper and the Remote API only, and custom
dictionary <em>terms</em> bias only the Whisper-family engines.
Transcript corrections from the custom dictionary do run after
Parakeet. See{" "}
<Link href="/languages/" className="text-primary underline">
languages by engine
</Link>
,{" "}
<Link
href="/multilingual-dictation/"
className="text-primary underline"
>
multilingual dictation
</Link>{" "}
and{" "}
<Link href="/custom-dictionary/" className="text-primary underline">
custom dictionary
</Link>
.
</p>
</div>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Hardware and packages
</h2>
<p className="mb-4 text-sm text-muted-foreground">
Parakeet runs on the CPU. Vocalinux&apos;s Vulkan and CUDA
acceleration applies to whisper.cpp, so a GPU does not speed up
Parakeet (see{" "}
<Link href="/gpu-acceleration/" className="text-primary underline">
GPU acceleration
</Link>
). Plan for roughly 640 MB of disk per Parakeet model, stored under{" "}
<code className="rounded bg-muted px-1">
~/.local/share/vocalinux/models/
</code>
.
</p>
<h3 className="mb-3 font-semibold">Which packages include Parakeet</h3>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th className="pb-3 pr-4 font-semibold">Install method</th>
<th className="pb-3 pr-4 font-semibold">Parakeet</th>
<th className="pb-3 font-semibold">Notes</th>
</tr>
</thead>
<tbody>
{packages.map((pkg) => (
<tr key={pkg.name} className="border-b border-border">
<td className="py-3 pr-4 font-medium">{pkg.name}</td>
<td className="py-3 pr-4">
{pkg.parakeet ? (
<span className="inline-flex items-center gap-1.5 text-primary">
<CheckCircle2 className="h-4 w-4" aria-hidden="true" />
Yes
</span>
) : (
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
<XCircle className="h-4 w-4" aria-hidden="true" />
No
</span>
)}
</td>
<td className="py-3 text-muted-foreground">{pkg.note}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
Parakeet vs whisper.cpp
</h2>
<p className="mb-4 max-w-4xl text-muted-foreground">
Both are local engines. The differences are about languages, hardware
and packaging rather than a single winner. For every engine side by
side, see the{" "}
<Link href="/compare/" className="text-primary underline">
engine comparison
</Link>
; for whisper.cpp sizes, see the{" "}
<Link href="/whisper-model-guide/" className="text-primary underline">
Whisper model guide
</Link>
.
</p>
<div className="overflow-x-auto rounded-[12px] border border-border bg-background p-6">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th className="pb-3 pr-4 font-semibold">
<span className="sr-only">Topic</span>
</th>
<th className="pb-3 pr-4 font-semibold">Parakeet</th>
<th className="pb-3 font-semibold">whisper.cpp</th>
</tr>
</thead>
<tbody>
{comparison.map((row) => (
<tr key={row.topic} className="border-b border-border">
<th scope="row" className="py-3 pr-4 font-medium">
{row.topic}
</th>
<td className="py-3 pr-4 text-muted-foreground">
{row.parakeet}
</td>
<td className="py-3 text-muted-foreground">
{row.whisperCpp}
</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mt-4 max-w-4xl text-sm text-muted-foreground">
Pick Parakeet if you dictate in English or a supported European
language on a CPU-only machine and installed with install.sh, from
source or from the AUR. Stay on whisper.cpp if you use a language
outside that set, want GPU acceleration, rely on custom dictionary
terms, or installed the AppImage, Flatpak, Snap, .deb or .rpm.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Troubleshooting
</h2>
<div className="space-y-5 text-sm text-muted-foreground">
<div>
<h3 className="mb-1 font-semibold text-foreground">
Parakeet is missing from the Engine list
</h3>
<p>
Settings lists only engines whose libraries are installed.
Parakeet appears once sherpa-onnx can be imported. Re-run the
installer with{" "}
<code className="rounded bg-muted px-1">--engine=parakeet</code>.
Installs of this engine through install.sh were broken before
v0.18.4, so use a current installer; on Arch, install
python-sherpa-onnx.
</p>
</div>
<div>
<h3 className="mb-1 font-semibold text-foreground">
The model will not load after an interrupted download
</h3>
<p>
Vocalinux refuses to load Parakeet files that fail checksum
verification. In Settings → Speech Model, delete the leftover
model from <strong>Unused downloads</strong> and download it
again.
</p>
</div>
<div>
<h3 className="mb-1 font-semibold text-foreground">
Text comes out in the wrong language
</h3>
<p>
The Language picker does not steer Parakeet. If you only dictate
English, switch the model to v2-english. For a language outside
the European set, use whisper.cpp.
</p>
</div>
<div>
<h3 className="mb-1 font-semibold text-foreground">
Anything else
</h3>
<p>
Run{" "}
<code className="rounded bg-muted px-1">vocalinux --debug</code>{" "}
and check{" "}
<code className="rounded bg-muted px-1">
~/.local/share/vocalinux/vocalinux.log
</code>
, then see the{" "}
<Link href="/troubleshooting/" className="text-primary underline">
troubleshooting guide
</Link>
.
</p>
</div>
</div>
</section>
<section className="mb-12">
<h2 className="mb-6 font-display text-2xl font-semibold">
Parakeet FAQ
</h2>
<div className="space-y-4">
{faqs.map((faq) => (
<div
key={faq.question}
className="rounded-[12px] border border-border bg-background p-5"
>
<h3 className="mb-2 font-semibold">{faq.question}</h3>
<p className="text-sm text-muted-foreground">{faq.answer}</p>
</div>
))}
</div>
</section>
<section className="rounded-[12px] border border-border bg-muted p-8">
<h2 className="mb-4 font-display text-2xl font-semibold">
Try Parakeet on your machine
</h2>
<p className="mb-6 text-muted-foreground">
Install Vocalinux with the Parakeet engine, or start with whisper.cpp
and compare. Also see{" "}
<Link href="/faster-whisper/" className="text-primary underline">
Faster Whisper on Linux
</Link>
, the other CPU engine added in v0.17.0.
</p>
<div className="flex flex-wrap gap-4">
<Link
href="/install/"
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
>
Install Vocalinux
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Link>
<Link
href="/compare/"
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted"
>
Compare engines
</Link>
</div>
</section>
</SeoSubpageShell>
);
}
+446
View File
@@ -0,0 +1,446 @@
import Link from "next/link";
import { type Metadata } from "next";
import type { ReactNode } from "react";
import {
AlertTriangle,
ChevronRight,
Headphones,
Mic,
Volume2,
} from "lucide-react";
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
import { TerminalBlock } from "@/components/terminal-block";
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
const PAGE_PATH = "/pipewire";
const PAGE_TITLE = "PipeWire Audio Capture for Linux Voice Dictation";
const PAGE_DESCRIPTION =
"How Vocalinux records from microphones and PipeWire system-audio sources, how it differs from PortAudio, and how to fix wrong-mic, silence and Bluetooth issues.";
const captureRows = [
{
path: "Microphones (PortAudio)",
lists:
"Input-capable ALSA and PulseAudio devices that PortAudio enumerates, plus System Default",
records: "A PortAudio stream, downmixed to mono for the speech engine",
use: "Normal dictation into a mic or headset",
},
{
path: "System audio (PipeWire)",
lists:
"Every PipeWire Audio/Sink node, physical or virtual, shown as “System audio: …”",
records: "pw-record on that sink's monitor, raw 16-bit mono at 16 kHz",
use: "Transcribing what is playing through an output device",
},
];
const steps = [
{
name: "Open the Audio page",
text: "Open Vocalinux Settings, go to Audio and find Input Device. Use the refresh button next to the list after plugging in a headset.",
},
{
name: "Pick a source",
text: "Choose System Default to follow your desktop sound settings, a named microphone to pin that device, or a System audio entry to record what plays through that output.",
},
{
name: "Run the Microphone Test",
text: "Click Test next to Microphone Test. It records 2 seconds and the level meter shows whether any signal arrived.",
},
{
name: "Try a real dictation",
text: "Use Test Dictation in the Settings sidebar footer, or your dictation shortcut, to confirm the engine hears speech from the chosen source.",
},
];
const faqs = [
{
question: "Does Vocalinux record microphones through PipeWire?",
answer:
"Microphones are listed and opened through PortAudio, including System Default. The native PipeWire path, pw-dump plus pw-record, is used for the System audio entries, because PortAudio cannot reach a sink monitor.",
},
{
question: "Can I capture audio from one application only?",
answer:
"No. A System audio entry records the monitor of a whole output device, so everything playing through that sink is mixed into the recording. Per-application stream capture is out of scope in the current code.",
},
{
question: "Why are there no System audio entries in my device list?",
answer:
"Vocalinux lists them only when both pw-dump and pw-record are on the PATH and the PipeWire daemon answers. If either tool is missing, the list shows microphones only and nothing else changes.",
},
{
question: "Does system-audio capture send anything online?",
answer:
"Not with a local engine. Audio from either path goes to the engine you selected, and local engines run on your machine. If you switch to the Remote API engine or a VocaGateway, audio is sent to the endpoint you configured.",
},
{
question: "Which version added PipeWire capture?",
answer:
"PipeWire system-audio capture was added in v0.18.0. Check your version with vocalinux --version.",
},
];
export const metadata: Metadata = buildPageMetadata({
title: `${PAGE_TITLE} | Vocalinux`,
description: PAGE_DESCRIPTION,
path: PAGE_PATH,
keywords: [
"pipewire voice dictation linux",
"pipewire speech to text",
"linux dictation system audio",
"pw-record transcription",
"portaudio vs pipewire",
"vocalinux microphone not working",
"bluetooth headset dictation linux",
],
});
function InlineCode({ children }: { children: ReactNode }) {
return (
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[0.85em] text-foreground">
{children}
</code>
);
}
export default function PipeWirePage() {
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "TechArticle",
headline: PAGE_TITLE,
description: PAGE_DESCRIPTION,
dateModified: "2026-11-01",
author: {
"@type": "Person",
name: "Jatin K Malik",
url: "https://github.com/jatinkrmalik",
},
publisher: {
"@type": "Organization",
name: "Vocalinux",
logo: {
"@type": "ImageObject",
url: absoluteUrl("/vocalinux.png"),
},
},
mainEntityOfPage: absoluteUrl(PAGE_PATH),
},
{
"@type": "HowTo",
name: "Choose an audio source for Vocalinux dictation",
step: steps.map((step, index) => ({
"@type": "HowToStep",
position: index + 1,
name: step.name,
text: step.text,
})),
},
{
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: { "@type": "Answer", text: faq.answer },
})),
},
],
};
return (
<SeoSubpageShell>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<section>
<p className="subpage-kicker">Added in v0.18.0</p>
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
PipeWire audio capture in Vocalinux
</h1>
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
Vocalinux records from two kinds of source: microphones, opened
through PortAudio, and system-audio sources that come straight from
PipeWire. This page explains what each one is for, how to pick the
right source, and what to check when dictation hears the wrong device
or nothing at all.
</p>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Two capture paths: PortAudio and PipeWire
</h2>
<p className="mb-4 text-sm text-muted-foreground">
PortAudio enumerates ALSA and PulseAudio devices, not PipeWire nodes.
That works well for microphones, but it cannot see a sink&apos;s
monitor, the stream of everything playing through an output. Since
v0.18.0 Vocalinux asks PipeWire directly: it lists sinks with{" "}
<InlineCode>pw-dump</InlineCode> and records the chosen one with{" "}
<InlineCode>pw-record --raw</InlineCode>. PipeWire converts the audio
to the format Vocalinux asks for inside its own graph, so no format
negotiation is needed.
</p>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th scope="col" className="pb-3 pr-4 font-semibold">
Path
</th>
<th scope="col" className="pb-3 pr-4 font-semibold">
What it lists
</th>
<th scope="col" className="pb-3 pr-4 font-semibold">
How it records
</th>
<th scope="col" className="pb-3 font-semibold">
Use it for
</th>
</tr>
</thead>
<tbody>
{captureRows.map((row) => (
<tr key={row.path} className="border-b border-border align-top">
<th scope="row" className="py-3 pr-4 font-medium">
{row.path}
</th>
<td className="py-3 pr-4 text-muted-foreground">
{row.lists}
</td>
<td className="py-3 pr-4 text-muted-foreground">
{row.records}
</td>
<td className="py-3 text-muted-foreground">{row.use}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="mb-2 mt-6 font-semibold">Who this is for</h3>
<p className="text-sm text-muted-foreground">
Most people dictate into a microphone and never need to change
anything. System audio is for the cases a microphone cannot cover
well, such as transcribing speech from a video or a call that plays
through your speakers or headphones. For recorded files, the{" "}
<Link
href="/transcribe-audio-files/"
className="text-primary underline"
>
file transcription mode
</Link>{" "}
is usually the better tool.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
How to choose a source
</h2>
<ol className="space-y-4">
{steps.map((step, index) => (
<li
key={step.name}
className="rounded-[12px] border border-border bg-background p-5"
>
<div className="mb-2 flex items-center gap-3">
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
{index + 1}
</span>
<h3 className="font-semibold">{step.name}</h3>
</div>
<p className="ml-10 text-sm text-muted-foreground">{step.text}</p>
</li>
))}
</ol>
<div className="mt-6 grid gap-4 md:grid-cols-3">
<div className="rounded-[12px] border border-border bg-background p-5">
<Mic className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h3 className="mb-2 font-semibold">System Default</h3>
<p className="text-sm text-muted-foreground">
Leaves valid default and virtual inputs under the sound
server&apos;s control, so the mic you pick in desktop sound
settings is the one Vocalinux hears.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Headphones
className="mb-3 h-6 w-6 text-primary"
aria-hidden="true"
/>
<h3 className="mb-2 font-semibold">A named microphone</h3>
<p className="text-sm text-muted-foreground">
A valid explicit choice always wins over System Default. Vocalinux
saves the device name and matches it again by name when indexes
shift after a replug.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Volume2 className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h3 className="mb-2 font-semibold">System audio: …</h3>
<p className="text-sm text-muted-foreground">
Records one output device&apos;s monitor. Two sinks with the same
description get the PipeWire node name added in brackets so you
can tell them apart.
</p>
</div>
</div>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Settings and limits
</h2>
<h3 className="mb-2 font-semibold">Where the choice is stored</h3>
<p className="mb-4 text-sm text-muted-foreground">
The selection lives in{" "}
<InlineCode>~/.config/vocalinux/config.json</InlineCode> under{" "}
<InlineCode>audio.device_index</InlineCode> and{" "}
<InlineCode>audio.device_name</InlineCode>. A{" "}
<InlineCode>null</InlineCode> index means System Default.
</p>
<h3 className="mb-2 font-semibold">Gotchas</h3>
<ul className="list-disc space-y-2 pl-5 text-sm text-muted-foreground">
<li>
System audio needs both <InlineCode>pw-dump</InlineCode> and{" "}
<InlineCode>pw-record</InlineCode>. Without them the entries are
simply not listed.
</li>
<li>
If a saved System audio sink disappears, Vocalinux refuses to record
instead of quietly switching to a microphone, because that would
capture the wrong audio. Pick a source again.
</li>
<li>
A sink monitor records the full mix for that output, including
notification sounds and any other app playing at the time.
</li>
<li>
The Flatpak release notes list PipeWire system-audio capture as a
native-install path. Inside the Snap, Settings → Audio lists the
sound server&apos;s microphones through{" "}
<InlineCode>pactl</InlineCode> instead.
</li>
</ul>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
<AlertTriangle className="h-5 w-5 text-primary" aria-hidden="true" />
Troubleshooting
</h2>
<h3 className="mb-2 font-semibold">
Dictation uses the wrong microphone
</h3>
<p className="mb-3 text-sm text-muted-foreground">
Select the mic by name in Settings → Audio → Input Device rather than
relying on System Default. On ALSA-only setups where PortAudio
substitutes some other hardware for the default input, Vocalinux
prefers an input-capable <InlineCode>pulse</InlineCode> device so it
follows your desktop&apos;s choice, and logs a warning when none
exists. Run with debug logging to see which device was opened:
</p>
<div className="mb-6">
<TerminalBlock
command="vocalinux --debug"
displayCommand="vocalinux --debug"
/>
</div>
<h3 className="mb-2 font-semibold">
Silence, or a Microphone Stalled notification
</h3>
<p className="mb-3 text-sm text-muted-foreground">
Check mute state and levels in your system sound settings, then list
capture hardware:
</p>
<div className="mb-3">
<TerminalBlock command="arecord -l" displayCommand="arecord -l" />
</div>
<p className="mb-6 text-sm text-muted-foreground">
A Microphone Stalled notification means the selected input stopped
delivering audio and Vocalinux stopped that attempt. Use Microphone
Test, or choose another Input Device. If a stale device index is the
problem, quit Vocalinux and set{" "}
<InlineCode>audio.device_index</InlineCode> to{" "}
<InlineCode>null</InlineCode> in <InlineCode>config.json</InlineCode>.
With a System audio source, remember that a silent output produces
silence: start the playback first.
</p>
<h3 className="mb-2 font-semibold">Bluetooth headsets</h3>
<p className="text-sm text-muted-foreground">
Bluetooth headset microphones use a separate capture profile that is
sensitive to repeated opening. Vocalinux opens the stream once per
session and pauses briefly between failed attempts on Bluetooth
devices. If capture still fails, try a wired microphone to check
whether the Bluetooth capture path is the cause.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">FAQ</h2>
<div className="space-y-4">
{faqs.map((faq) => (
<div
key={faq.question}
className="rounded-[12px] border border-border bg-background p-5"
>
<h3 className="mb-2 font-semibold">{faq.question}</h3>
<p className="text-sm text-muted-foreground">{faq.answer}</p>
</div>
))}
</div>
<p className="mt-4 text-sm text-muted-foreground">
More fixes on the{" "}
<Link href="/troubleshooting/" className="text-primary underline">
troubleshooting page
</Link>
. How audio is handled with local and remote engines is covered in{" "}
<Link href="/offline/" className="text-primary underline">
offline dictation
</Link>{" "}
and{" "}
<Link href="/remote-api/" className="text-primary underline">
Remote API
</Link>
. Speech detection itself is explained in{" "}
<Link
href="/voice-activity-detection/"
className="text-primary underline"
>
voice activity detection
</Link>
.
</p>
</section>
<section className="rounded-[12px] border border-border bg-muted p-8">
<h2 className="mb-4 font-display text-2xl font-semibold">
Pick a source and dictate
</h2>
<p className="mb-6 text-muted-foreground">
Install Vocalinux, choose your microphone or a system-audio source in
Settings → Audio, and run the Microphone Test before your first
dictation.
</p>
<div className="flex flex-wrap gap-4">
<Link
href="/install/"
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
>
Install Vocalinux
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Link>
<Link
href="/troubleshooting/"
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted"
>
Troubleshooting guide
</Link>
</div>
</section>
</SeoSubpageShell>
);
}
+407
View File
@@ -0,0 +1,407 @@
import Link from "next/link";
import { type Metadata } from "next";
import {
CheckCircle2,
ChevronRight,
Lock,
Mic,
Terminal,
Users,
} from "lucide-react";
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
import { TerminalBlock } from "@/components/terminal-block";
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
const PAGE_PATH = "/transcribe-audio-files";
const cliCommand = "vocalinux --transcribe-file meeting.wav";
const howToSteps = [
{
name: "Open the tray entry",
text: "Click the Vocalinux tray icon and choose Transcribe Audio File…",
},
{
name: "Pick a recording",
text: "Select a WAV file, or an MP3, OGG, FLAC, M4A or Opus file if ffmpeg is installed, and click Open.",
},
{
name: "Download TinyDiarize once",
text: "The first time, Vocalinux asks to download the TinyDiarize model (about 465 MB, one time). Click Download. It is only used for file transcription and never becomes your dictation model.",
},
{
name: "Wait for the transcript",
text: "A notification says the file is being transcribed. Long files can take a while. When it finishes, a Transcript window opens with timestamps and speaker labels.",
},
{
name: "Copy or save it",
text: "Use Copy to put the full transcript on the clipboard, or Save As to write it to a text file (transcript.txt by default).",
},
];
const formatRows = [
{
input: "WAV, 16-bit, 16 kHz, mono or stereo",
path: "Read directly. No extra tools needed.",
},
{
input: "Any other WAV",
path: "Decoded with ffmpeg to mono 16 kHz first.",
},
{
input: "MP3, OGG, FLAC, M4A, Opus",
path: "Offered in the tray file picker when ffmpeg is installed. Decoded with ffmpeg.",
},
{
input: "Other files (CLI)",
path: "Handed to ffmpeg, so anything your ffmpeg build can decode.",
},
];
const faqs = [
{
q: "Can it label speakers in languages other than English?",
a: "No. The TinyDiarize model Vocalinux uses, small.en-tdrz, is English-only. Speaker labels and the transcript itself are for English audio.",
},
{
q: "Does Speaker 1 always mean the same person?",
a: "No. TinyDiarize marks the points where the speaker changes, and Vocalinux numbers each new turn in order. A person who speaks again later gets a new number, so treat the labels as turn markers and rename them yourself.",
},
{
q: "Why does the CLI say the model is not downloaded?",
a: "vocalinux --transcribe-file never downloads anything. Run Transcribe Audio File… from the tray once and accept the TinyDiarize download, then the CLI can use the model.",
},
{
q: "Is my audio uploaded anywhere?",
a: "No. The file is decoded and transcribed on your machine with whisper.cpp, even if your dictation engine is set to a remote API. The only network use is the one-time model download.",
},
{
q: "Why can I only pick WAV files in the tray?",
a: "ffmpeg is not installed or not on your PATH. Without it, the file picker only offers WAV. Install ffmpeg from your distribution's packages to enable MP3, OGG, FLAC, M4A and Opus.",
},
];
export const metadata: Metadata = buildPageMetadata({
title: "Transcribe Audio Files on Linux Offline with Speakers | Vocalinux",
description:
"Transcribe an audio file on Linux offline with Vocalinux: tray or --transcribe-file, local TinyDiarize speaker labels (English), formats, output and limits.",
path: PAGE_PATH,
keywords: [
"transcribe audio file Linux offline",
"speaker diarization Linux",
"TinyDiarize",
"offline meeting transcription Linux",
"whisper.cpp speaker labels",
"transcribe WAV MP3 Linux",
],
});
export default function TranscribeAudioFilesPage() {
const articleJsonLd = {
"@context": "https://schema.org",
"@type": "TechArticle",
headline: "Transcribe Audio Files on Linux Offline with Speaker Labels",
description:
"How to transcribe recordings locally with Vocalinux from the tray or the --transcribe-file CLI, and how TinyDiarize speaker labels work.",
dateModified: "2026-11-01",
author: {
"@type": "Person",
name: "Jatin K Malik",
url: "https://github.com/jatinkrmalik",
},
publisher: {
"@type": "Organization",
name: "Vocalinux",
logo: {
"@type": "ImageObject",
url: absoluteUrl("/vocalinux.png"),
},
},
mainEntityOfPage: absoluteUrl(PAGE_PATH),
};
const howToJsonLd = {
"@context": "https://schema.org",
"@type": "HowTo",
name: "Transcribe an audio file with speaker labels in Vocalinux",
step: howToSteps.map((step, index) => ({
"@type": "HowToStep",
position: index + 1,
name: step.name,
text: step.text,
})),
};
const faqJsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((item) => ({
"@type": "Question",
name: item.q,
acceptedAnswer: { "@type": "Answer", text: item.a },
})),
};
return (
<SeoSubpageShell>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(howToJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<section>
<p className="subpage-kicker">Added in v0.18.0</p>
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
Transcribe audio files offline, with speaker labels
</h1>
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
Vocalinux can turn a recorded meeting, interview or voice memo into a
text transcript on your own machine. It uses TinyDiarize, a
whisper.cpp model that also marks where the speaker changes, so the
transcript comes out as timestamped blocks labelled Speaker 1, Speaker
2 and so on. Start it from the tray or from a terminal.
</p>
</section>
<section className="mb-12 grid gap-4 sm:grid-cols-3">
<div className="rounded-[12px] border border-border bg-background p-5">
<Mic className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h3 className="mb-2 font-semibold">
For recordings, not live dictation
</h3>
<p className="text-sm text-muted-foreground">
File transcription is separate from the dictation pipeline. It never
types into other apps and does not change your dictation model.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Users className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h3 className="mb-2 font-semibold">Speaker turns, English only</h3>
<p className="text-sm text-muted-foreground">
The model is <code>small.en-tdrz</code>. It is English-only, so use
it for English recordings.
</p>
</div>
<div className="rounded-[12px] border border-border bg-background p-5">
<Lock className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
<h3 className="mb-2 font-semibold">Local processing</h3>
<p className="text-sm text-muted-foreground">
Audio is decoded and transcribed on your computer. Nothing is sent
to a server.
</p>
</div>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
From the tray
</h2>
<ol className="space-y-4">
{howToSteps.map((step, index) => (
<li
key={step.name}
className="rounded-[12px] border border-border bg-background p-5"
>
<div className="mb-2 flex items-center gap-3">
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
{index + 1}
</span>
<h3 className="font-semibold">{step.name}</h3>
</div>
<p className="ml-10 text-sm text-muted-foreground">{step.text}</p>
</li>
))}
</ol>
<p className="mt-4 text-sm text-muted-foreground">
The model download is checked against a pinned SHA-256 digest before
use, like every other Vocalinux model. The status line under the
transcript reports how many blocks were found and the highest speaker
number.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
<Terminal className="h-5 w-5 text-primary" aria-hidden="true" />
From the command line
</h2>
<p className="mb-4 text-muted-foreground">
<code>--transcribe-file PATH</code> transcribes one file, prints the
diarized transcript to standard output and exits:
</p>
<TerminalBlock
command={cliCommand}
displayCommand={cliCommand}
label="terminal"
/>
<ul className="mt-4 space-y-2 text-sm text-muted-foreground">
<li>
It needs no display and no tray, and it runs alongside a Vocalinux
instance that is already running, so it works over SSH or from a
script.
</li>
<li>
It does not download the model. Fetch TinyDiarize once through the
tray entry first, otherwise the command stops with a message saying
the model is not downloaded yet.
</li>
<li>
Because the transcript goes to standard output, you can redirect it
to a file with your shell. Errors go to standard error, prefixed
with <code>vocalinux: could not transcribe</code>, and the exit
status is 1. A file with no detectable speech prints{" "}
<code>vocalinux: no speech detected</code>.
</li>
</ul>
</section>
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Supported input formats
</h2>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-border">
<th className="pb-3 pr-4 font-semibold">Input</th>
<th className="pb-3 font-semibold">How Vocalinux reads it</th>
</tr>
</thead>
<tbody>
{formatRows.map((row) => (
<tr key={row.input} className="border-b border-border">
<td className="py-3 pr-4 font-medium">{row.input}</td>
<td className="py-3 text-muted-foreground">{row.path}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mt-4 text-sm text-muted-foreground">
Vocalinux does not install ffmpeg for you. Without it, only 16-bit 16
kHz WAV files can be transcribed, and the tray picker&apos;s filter
says so.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
Output format
</h2>
<p className="mb-4 text-muted-foreground">
Each block is one speaker turn: a <code>[HH:MM:SS]</code> timestamp
for where it starts, the speaker label, then the text. Blocks are
separated by a blank line. The Transcript window, Copy, Save As and
the CLI all use the same layout. The lines below show the shape only;
they are not real output.
</p>
<pre className="subpage-terminal overflow-x-auto p-4">
<code>{`[00:00:00] Speaker 1: <first turn>
[00:00:14] Speaker 2: <second turn>
[00:00:31] Speaker 3: <third turn>`}</code>
</pre>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">
How speaker labels work
</h2>
<p className="mb-4 text-muted-foreground">
TinyDiarize is a small, English-only whisper.cpp model whose output
includes a speaker-turn marker wherever it detects a change of voice.
Vocalinux runs it in whisper.cpp&apos;s tinydiarize mode and splits
the text at each marker. Each new block gets the next number.
</p>
<p className="text-muted-foreground">
That makes the labels positional, not identities. In a two-person
interview the transcript reads Speaker 1, Speaker 2, Speaker 3,
Speaker 4 as the two people take turns, rather than alternating
between 1 and 2. Turn boundaries are useful for reading and editing;
assigning names is up to you.
</p>
</section>
<section className="bg-primary/5 mb-12 rounded-[12px] border border-border p-6">
<h2 className="mb-4 font-display text-2xl font-semibold">
Privacy and limitations
</h2>
<ul className="space-y-3 text-sm text-muted-foreground">
{[
"Local: ffmpeg decoding and whisper.cpp transcription both run on your machine. File transcription always uses the local TinyDiarize model, even when dictation is set to the Remote API engine.",
"Network: only the one-time model download, from a pinned upstream revision.",
"Storage: the transcript is not written to disk unless you click Save As or redirect the CLI output.",
"English only, and speaker numbers do not identify people.",
"The model is about 465 MB, so the first run needs that much free disk space plus download time.",
"Long files take a while. The tray shows a notification while it works.",
].map((item) => (
<li key={item} className="flex items-start gap-2">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary"
aria-hidden="true"
/>
<span>{item}</span>
</li>
))}
</ul>
<p className="mt-4 text-sm text-muted-foreground">
More on what stays on your machine:{" "}
<Link href="/offline/">offline dictation</Link> and the{" "}
<Link href="/privacy/">privacy policy</Link>.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 font-display text-2xl font-semibold">FAQ</h2>
<div className="space-y-4">
{faqs.map((item) => (
<div
key={item.q}
className="rounded-[12px] border border-border bg-background p-5"
>
<h3 className="mb-2 font-semibold">{item.q}</h3>
<p className="text-sm text-muted-foreground">{item.a}</p>
</div>
))}
</div>
</section>
<section className="rounded-[12px] border border-border bg-muted p-8">
<h2 className="mb-4 font-display text-2xl font-semibold">
Transcribe your first file
</h2>
<p className="mb-6 text-muted-foreground">
File transcription is available from v0.18.0; check yours with{" "}
<code>vocalinux --version</code>. For live dictation model choices,
see the <Link href="/whisper-model-guide/">Whisper model guide</Link>;
to fix names the model mishears in dictation, use the{" "}
<Link href="/custom-dictionary/">custom dictionary</Link>.
</p>
<div className="flex flex-wrap gap-4">
<Link
href="/install/"
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
>
Install Vocalinux
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Link>
<Link
href="/dictation-pad/"
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted"
>
Dictation Pad
</Link>
</div>
</section>
</SeoSubpageShell>
);
}
+5 -5
View File
@@ -34,7 +34,7 @@ const waylandFeatures = [
{
title: "Desktop Agnostic",
description:
"Works with GNOME, KDE Plasma, Sway, Hyprland, river, and other Wayland compositors.",
"Full support on GNOME and KDE Plasma, good support on sway, and experimental support on Hyprland.",
icon: Zap,
},
{
@@ -61,19 +61,19 @@ const compositorSupport = [
{
name: "Sway",
method: "IBus / wtype",
status: "Full support",
status: "Good support",
notes: "i3-compatible Wayland compositor",
},
{
name: "Hyprland",
method: "IBus / wtype",
status: "Full support",
notes: "Dynamic tiling Wayland compositor",
status: "Experimental",
notes: "May need wtype configuration; see the Hyprland and sway guide",
},
{
name: "river",
method: "IBus / wtype",
status: "Full support",
status: "Untested",
notes: "Dynamic tiling Wayland compositor",
},
{
+16
View File
@@ -36,6 +36,22 @@ const pageTitles: Record<string, string> = {
"/troubleshooting": "Troubleshooting",
"/shortcuts": "Voice Commands",
"/alternatives": "Alternatives",
"/parakeet": "Parakeet",
"/faster-whisper": "Faster Whisper",
"/dictation-pad": "Dictation Pad",
"/transcribe-audio-files": "Transcribe Audio Files",
"/custom-dictionary": "Custom Dictionary",
"/multilingual-dictation": "Multilingual Dictation",
"/pipewire": "PipeWire",
"/global-shortcuts": "Global Shortcuts",
"/hyprland-sway": "Hyprland and Sway",
"/local-gateway": "Local Gateway",
"/flatpak": "Flatpak",
"/install/nixos": "NixOS",
"/install/debian": "Debian",
"/install/opensuse": "openSUSE",
"/install/linux-mint": "Linux Mint",
"/install/pop-os": "Pop!_OS",
};
function titleFromSegment(segment: string): string {
+10 -1
View File
@@ -40,8 +40,17 @@ export const buildPageMetadata = ({
}: PageMetadataOptions): Metadata => {
const canonical = absoluteUrl(path);
// The root layout applies the "%s | Vocalinux" template. Titles that already
// carry the brand suffix are emitted as-is so search results do not show
// "| Vocalinux | Vocalinux".
const pageTitle: Metadata["title"] = new RegExp(
`[|\\-–—]\\s*${SITE_NAME}$`,
).test(title)
? { absolute: title }
: title;
return {
title,
title: pageTitle,
description,
keywords,
alternates: {
+12
View File
@@ -58,6 +58,8 @@ export const footerGroups: { title: string; links: NavLink[] }[] = [
{ href: "/install/ubuntu/", label: "Ubuntu" },
{ href: "/install/fedora/", label: "Fedora" },
{ href: "/install/arch/", label: "Arch" },
{ href: "/install/debian/", label: "Debian" },
{ href: "/flatpak/", label: "Flatpak" },
],
},
{
@@ -70,6 +72,16 @@ export const footerGroups: { title: string; links: NavLink[] }[] = [
{ href: "/remote-api/", label: "Remote API" },
],
},
{
title: "Features",
links: [
{ href: "/dictation-pad/", label: "Dictation Pad" },
{ href: "/transcribe-audio-files/", label: "Transcribe audio files" },
{ href: "/custom-dictionary/", label: "Custom dictionary" },
{ href: "/global-shortcuts/", label: "Global shortcuts" },
{ href: "/parakeet/", label: "Parakeet engine" },
],
},
{
title: "Project",
links: [
+1 -1
View File
@@ -1870,7 +1870,7 @@ nextjs-portal {
}
.footer-inner {
grid-template-columns: minmax(0, 1.4fr) repeat(4, minmax(0, 1fr));
grid-template-columns: minmax(0, 1.2fr) repeat(5, minmax(0, 1fr));
}
}