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:
+97
-1
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 “autostart”.
|
||||
</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
|
||||
|
||||
@@ -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' 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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'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'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>
|
||||
);
|
||||
}
|
||||
@@ -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'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'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>
|
||||
);
|
||||
}
|
||||
@@ -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-<version>-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 “IBus Wayland”. 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
@@ -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’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>
|
||||
|
||||
@@ -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’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’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’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 “IBus Wayland”. 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
@@ -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'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>"layout"</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'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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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'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'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'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'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>
|
||||
);
|
||||
}
|
||||
@@ -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'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'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'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'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'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>
|
||||
);
|
||||
}
|
||||
@@ -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'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'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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user