chore: import vocalinux project
Import the vocalinux source tree (tray app, Next.js site in web/, packaging, installers, docs and tests) on top of the Gitea template commit. The project README replaces the template README, and the template CI files (.gitea/workflows/ci.yml, .jscpd.json, CI.md) are kept.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('_headers File - Cache and Security Headers', () => {
|
||||
const headersPath = path.join(process.cwd(), 'public', '_headers');
|
||||
let headersContent: string;
|
||||
|
||||
beforeAll(() => {
|
||||
headersContent = fs.readFileSync(headersPath, 'utf-8');
|
||||
});
|
||||
|
||||
it('exists in the public directory', () => {
|
||||
expect(fs.existsSync(headersPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('contains cache headers for static assets', () => {
|
||||
expect(headersContent).toContain('Cache-Control');
|
||||
});
|
||||
|
||||
describe('Static Assets Caching', () => {
|
||||
it('sets long cache for _next/static files (1 year)', () => {
|
||||
expect(headersContent).toContain('/_next/static/*');
|
||||
expect(headersContent).toContain('max-age=31536000');
|
||||
expect(headersContent).toContain('immutable');
|
||||
});
|
||||
|
||||
it('caches font files for 1 year', () => {
|
||||
expect(headersContent).toContain('.woff');
|
||||
expect(headersContent).toContain('.woff2');
|
||||
expect(headersContent).toContain('.ttf');
|
||||
expect(headersContent).toContain('.otf');
|
||||
});
|
||||
|
||||
it('caches image files for 1 year', () => {
|
||||
expect(headersContent).toContain('.png');
|
||||
expect(headersContent).toContain('.jpg');
|
||||
expect(headersContent).toContain('.jpeg');
|
||||
expect(headersContent).toContain('.gif');
|
||||
expect(headersContent).toContain('.svg');
|
||||
expect(headersContent).toContain('.ico');
|
||||
expect(headersContent).toContain('.webp');
|
||||
expect(headersContent).toContain('.avif');
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTML and Dynamic Content', () => {
|
||||
it('sets short cache for HTML files with revalidation', () => {
|
||||
expect(headersContent).toContain('/*.html');
|
||||
expect(headersContent).toContain('max-age=0');
|
||||
expect(headersContent).toContain('must-revalidate');
|
||||
});
|
||||
|
||||
it('sets 24-hour cache for JSON files', () => {
|
||||
expect(headersContent).toContain('/*.json');
|
||||
expect(headersContent).toContain('max-age=86400');
|
||||
});
|
||||
});
|
||||
|
||||
describe('JavaScript and CSS', () => {
|
||||
it('caches JavaScript files for 1 year', () => {
|
||||
expect(headersContent).toContain('/*.js');
|
||||
expect(headersContent).toMatch(/\.js\s*\n\s*Cache-Control:[^\n]*max-age=31536000/);
|
||||
});
|
||||
|
||||
it('caches CSS files for 1 year', () => {
|
||||
expect(headersContent).toContain('/*.css');
|
||||
expect(headersContent).toMatch(/\.css\s*\n\s*Cache-Control:[^\n]*max-age=31536000/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Security Headers', () => {
|
||||
it('sets X-Content-Type-Options to nosniff', () => {
|
||||
expect(headersContent).toContain('X-Content-Type-Options: nosniff');
|
||||
});
|
||||
|
||||
it('sets X-Frame-Options to DENY', () => {
|
||||
expect(headersContent).toContain('X-Frame-Options: DENY');
|
||||
});
|
||||
|
||||
it('sets X-XSS-Protection', () => {
|
||||
expect(headersContent).toContain('X-XSS-Protection: 1; mode=block');
|
||||
});
|
||||
|
||||
it('sets Referrer-Policy', () => {
|
||||
expect(headersContent).toContain('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Favicon and App Icons', () => {
|
||||
it('caches favicon files', () => {
|
||||
expect(headersContent).toContain('/favicon.ico');
|
||||
expect(headersContent).toContain('/apple-touch-icon.png');
|
||||
});
|
||||
|
||||
it('caches Android Chrome icons', () => {
|
||||
expect(headersContent).toContain('/android-chrome-');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { OptimizedImage, VocalinuxLogo } from '../../components/optimized-image';
|
||||
|
||||
// Mock Next.js Image component to capture loading behavior
|
||||
const mockImage = jest.fn();
|
||||
jest.mock('next/image', () => ({
|
||||
__esModule: true,
|
||||
default: (props: any) => {
|
||||
mockImage(props);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { priority, loading, ...imgProps } = props;
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
return <img {...imgProps} alt={props.alt} data-priority={priority} data-loading={loading} />;
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Lazy Loading Behavior', () => {
|
||||
beforeEach(() => {
|
||||
mockImage.mockClear();
|
||||
});
|
||||
|
||||
describe('OptimizedImage Component', () => {
|
||||
const defaultProps = {
|
||||
src: '/test.png',
|
||||
alt: 'Test',
|
||||
width: 100,
|
||||
height: 100,
|
||||
};
|
||||
|
||||
it('lazy loads images by default (no priority prop)', () => {
|
||||
render(<OptimizedImage {...defaultProps} />);
|
||||
|
||||
// Check that the image is rendered
|
||||
const img = screen.getByAltText('Test');
|
||||
expect(img).toBeInTheDocument();
|
||||
|
||||
// By default, Next.js Image uses lazy loading when priority is not set
|
||||
expect(mockImage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
priority: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('eager loads images when priority is true', () => {
|
||||
render(<OptimizedImage {...defaultProps} priority />);
|
||||
|
||||
expect(mockImage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
priority: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('passes loading behavior to Next.js Image', () => {
|
||||
render(<OptimizedImage {...defaultProps} priority={false} />);
|
||||
|
||||
const call = mockImage.mock.calls[0][0];
|
||||
expect(call).toHaveProperty('priority');
|
||||
});
|
||||
});
|
||||
|
||||
describe('VocalinuxLogo Component', () => {
|
||||
it('lazy loads the circular mark by default', () => {
|
||||
render(<VocalinuxLogo />);
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toHaveAttribute('src', '/brand/vocalinux-mark-circle.svg');
|
||||
expect(img).toHaveAttribute('loading', 'lazy');
|
||||
});
|
||||
|
||||
it('eager loads the mark when priority is set', () => {
|
||||
render(<VocalinuxLogo priority />);
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toHaveAttribute('loading', 'eager');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance Best Practices', () => {
|
||||
it('maintains aspect ratio with explicit width and height', () => {
|
||||
render(<OptimizedImage src="/test.png" alt="Test" width={200} height={150} />);
|
||||
|
||||
const img = screen.getByAltText('Test');
|
||||
expect(img).toHaveAttribute('width', '200');
|
||||
expect(img).toHaveAttribute('height', '150');
|
||||
});
|
||||
|
||||
it('prevents layout shift with explicit dimensions', () => {
|
||||
render(<VocalinuxLogo width={64} height={64} />);
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toHaveAttribute('width', '64');
|
||||
expect(img).toHaveAttribute('height', '64');
|
||||
});
|
||||
});
|
||||
|
||||
describe('WebP Lazy Loading Integration', () => {
|
||||
it('provides WebP source even for lazy-loaded images', () => {
|
||||
const { container } = render(
|
||||
<OptimizedImage src="/hero.png" alt="Hero" width={800} height={400} priority={false} />
|
||||
);
|
||||
|
||||
// Even lazy-loaded images should have WebP support
|
||||
const source = container.querySelector('source');
|
||||
expect(source).toHaveAttribute('srcset', '/hero.webp');
|
||||
expect(source).toHaveAttribute('type', 'image/webp');
|
||||
});
|
||||
|
||||
it('ships the circular mark as an SVG', () => {
|
||||
render(<VocalinuxLogo width={32} height={32} />);
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toHaveAttribute('src', '/brand/vocalinux-mark-circle.svg');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('Next.js Config - PageSpeed Optimizations', () => {
|
||||
const configPath = path.join(process.cwd(), 'next.config.js');
|
||||
let configContent: string;
|
||||
|
||||
beforeAll(() => {
|
||||
configContent = fs.readFileSync(configPath, 'utf-8');
|
||||
});
|
||||
|
||||
it('exists in the project root', () => {
|
||||
expect(fs.existsSync(configPath)).toBe(true);
|
||||
});
|
||||
|
||||
describe('Image Configuration', () => {
|
||||
it('configures image formats for optimization', () => {
|
||||
expect(configContent).toContain("formats: ['image/avif', 'image/webp']");
|
||||
});
|
||||
|
||||
it('has unoptimized images for static export', () => {
|
||||
expect(configContent).toContain('unoptimized: true');
|
||||
});
|
||||
|
||||
it('configures remote patterns for external images', () => {
|
||||
expect(configContent).toContain('remotePatterns');
|
||||
expect(configContent).toContain('protocol:');
|
||||
expect(configContent).toContain('hostname:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Experimental Optimizations', () => {
|
||||
it('enables optimizePackageImports for lucide-react', () => {
|
||||
expect(configContent).toContain('optimizePackageImports');
|
||||
expect(configContent).toContain('lucide-react');
|
||||
});
|
||||
|
||||
it('enables optimizePackageImports for framer-motion', () => {
|
||||
expect(configContent).toContain('framer-motion');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Build Configuration', () => {
|
||||
it('outputs to distDir: out for static export', () => {
|
||||
expect(configContent).toContain("distDir: 'out'");
|
||||
});
|
||||
|
||||
it('disables production browser source maps', () => {
|
||||
expect(configContent).toContain('productionBrowserSourceMaps: false');
|
||||
});
|
||||
|
||||
it('configures static export', () => {
|
||||
expect(configContent).toContain("output: 'export'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('SWC Configuration', () => {
|
||||
it('has webpack configuration for custom optimizations', () => {
|
||||
expect(configContent).toContain('webpack:');
|
||||
});
|
||||
|
||||
it('configures TerserPlugin for console removal', () => {
|
||||
expect(configContent).toContain('TerserPlugin');
|
||||
expect(configContent).toContain('pure_funcs');
|
||||
expect(configContent).toContain('console.log');
|
||||
});
|
||||
|
||||
it('provides node polyfills for browser', () => {
|
||||
expect(configContent).toContain('resolve.fallback');
|
||||
expect(configContent).toContain('fs: false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TypeScript Configuration', () => {
|
||||
it('ignores TypeScript build errors (for CI/CD compatibility)', () => {
|
||||
expect(configContent).toContain('typescript:');
|
||||
expect(configContent).toContain('ignoreBuildErrors: true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ESLint Configuration', () => {
|
||||
it('ignores ESLint during builds', () => {
|
||||
expect(configContent).toContain('eslint:');
|
||||
expect(configContent).toContain('ignoreDuringBuilds: true');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* Structural checks for the screenshots gallery: page source, public assets,
|
||||
* nav discoverability, and homepage link. Exercises the real shipped files
|
||||
* under web/ rather than re-implementing gallery data in the test.
|
||||
*/
|
||||
describe("Screenshots page and assets", () => {
|
||||
const webRoot = process.cwd();
|
||||
const pagePath = path.join(webRoot, "src/app/screenshots/page.tsx");
|
||||
const shellPath = path.join(webRoot, "src/components/seo-subpage-shell.tsx");
|
||||
const homePath = path.join(webRoot, "src/app/page.tsx");
|
||||
const publicScreenshotsDir = path.join(webRoot, "public/screenshots");
|
||||
const darkScreenshotsDir = path.join(publicScreenshotsDir, "dark");
|
||||
|
||||
const expectedAssets = [
|
||||
"00-transcription.png",
|
||||
"02-system-tray.png",
|
||||
"03-log-viewer.png",
|
||||
"05-about-view.png",
|
||||
"settings-speech-engine.png",
|
||||
"settings-recognition.png",
|
||||
"settings-audio.png",
|
||||
"settings-performance.png",
|
||||
"settings-general.png",
|
||||
"settings-advanced.png",
|
||||
];
|
||||
|
||||
let pageSource: string;
|
||||
let shellSource: string;
|
||||
let homeSource: string;
|
||||
|
||||
beforeAll(() => {
|
||||
pageSource = fs.readFileSync(pagePath, "utf-8");
|
||||
shellSource = fs.readFileSync(shellPath, "utf-8");
|
||||
homeSource = fs.readFileSync(homePath, "utf-8");
|
||||
});
|
||||
|
||||
it("ships a dedicated /screenshots page module with lightbox gallery", () => {
|
||||
expect(fs.existsSync(pagePath)).toBe(true);
|
||||
expect(pageSource).toMatch(/export default function ScreenshotsPage/);
|
||||
expect(pageSource).toContain('path: "/screenshots"');
|
||||
expect(pageSource).toContain("Vocalinux Screenshots");
|
||||
expect(pageSource).toContain("ScreenshotGallery");
|
||||
const galleryPath = path.join(
|
||||
webRoot,
|
||||
"src/components/screenshot-gallery.tsx",
|
||||
);
|
||||
expect(fs.existsSync(galleryPath)).toBe(true);
|
||||
const gallerySource = fs.readFileSync(galleryPath, "utf-8");
|
||||
expect(gallerySource).toContain('role="dialog"');
|
||||
expect(gallerySource).toContain("aria-modal");
|
||||
expect(gallerySource).toContain("View larger");
|
||||
});
|
||||
|
||||
it("references every public screenshot asset from the page source", () => {
|
||||
for (const file of expectedAssets) {
|
||||
const assetPath = path.join(publicScreenshotsDir, file);
|
||||
expect(fs.existsSync(assetPath)).toBe(true);
|
||||
expect(fs.statSync(assetPath).size).toBeGreaterThan(1000);
|
||||
expect(pageSource).toContain(`/screenshots/${file}`);
|
||||
|
||||
const darkPath = path.join(darkScreenshotsDir, file);
|
||||
expect(fs.existsSync(darkPath)).toBe(true);
|
||||
expect(fs.statSync(darkPath).size).toBeGreaterThan(1000);
|
||||
expect(pageSource).toContain(`/screenshots/dark/${file}`);
|
||||
}
|
||||
expect(fs.existsSync(path.join(publicScreenshotsDir, "settings-shortcuts.png"))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("links to the gallery and only embeds a few product shots on home", () => {
|
||||
// Craft home shows a handful of real app shots, then /screenshots/ for the rest.
|
||||
expect(homeSource).toContain('href="/screenshots/"');
|
||||
expect(homeSource).toContain("/screenshots/00-transcription.png");
|
||||
expect(homeSource).not.toContain("/screenshots/05-about-view.png");
|
||||
expect(homeSource).not.toContain("/screenshots/settings-recognition.png");
|
||||
expect(homeSource).not.toContain("/screenshots/settings-audio.png");
|
||||
expect(homeSource).not.toContain("/screenshots/settings-performance.png");
|
||||
expect(homeSource).not.toContain("/screenshots/settings-advanced.png");
|
||||
});
|
||||
|
||||
it("exposes Screenshots in the shared site nav", () => {
|
||||
const navSource = fs.readFileSync(
|
||||
path.join(webRoot, "src/lib/site-nav.ts"),
|
||||
"utf-8",
|
||||
);
|
||||
expect(navSource).toContain('href: "/screenshots/"');
|
||||
expect(navSource).toContain('label: "Screenshots"');
|
||||
expect(shellSource).toContain("SiteChrome");
|
||||
});
|
||||
|
||||
it("includes captioned settings and product sections", () => {
|
||||
expect(pageSource).toContain("Speech Model");
|
||||
expect(pageSource).toContain("Dictation");
|
||||
expect(pageSource).toContain("Audio");
|
||||
expect(pageSource).toContain("Performance");
|
||||
expect(pageSource).toContain("Application");
|
||||
expect(pageSource).toContain("Advanced");
|
||||
expect(pageSource).toContain("Dictation in action");
|
||||
expect(pageSource).toContain("About");
|
||||
expect(pageSource).toContain("update checker");
|
||||
expect(pageSource).not.toContain("Shortcuts & Hotkeys");
|
||||
expect(pageSource).not.toContain("About dialog");
|
||||
expect(pageSource).not.toContain("Settings overview");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
describe("SEO Assets", () => {
|
||||
const robotsPath = path.join(process.cwd(), "public", "robots.txt");
|
||||
const sitemapPath = path.join(process.cwd(), "public", "sitemap.xml");
|
||||
|
||||
let robotsContent: string;
|
||||
let sitemapContent: string;
|
||||
|
||||
beforeAll(() => {
|
||||
robotsContent = fs.readFileSync(robotsPath, "utf-8");
|
||||
sitemapContent = fs.readFileSync(sitemapPath, "utf-8");
|
||||
});
|
||||
|
||||
it("contains sitemap reference in robots.txt", () => {
|
||||
expect(robotsContent).toContain(
|
||||
"Sitemap: https://vocalinux.com/sitemap.xml",
|
||||
);
|
||||
});
|
||||
|
||||
it("blocks crawler access to technical paths", () => {
|
||||
expect(robotsContent).toContain("Disallow: /api/");
|
||||
expect(robotsContent).toContain("Disallow: /out/");
|
||||
});
|
||||
|
||||
it("does not block Next.js static assets needed for rendering", () => {
|
||||
expect(robotsContent).not.toContain("Disallow: /_next/");
|
||||
});
|
||||
|
||||
it("does not blanket-block query-string URLs", () => {
|
||||
// Blocking all query strings causes GSC "Indexed, though blocked by robots.txt"
|
||||
// for pages discovered with UTM/tracking params. Use canonicals instead.
|
||||
const disallowRules = robotsContent
|
||||
.split("\n")
|
||||
.filter((line) => line.trimStart().startsWith("Disallow:"));
|
||||
expect(disallowRules.some((line) => /\/\*\?\*/.test(line))).toBe(false);
|
||||
});
|
||||
|
||||
it("includes core landing pages in sitemap", () => {
|
||||
const expectedUrls = [
|
||||
"https://vocalinux.com/",
|
||||
"https://vocalinux.com/install/",
|
||||
"https://vocalinux.com/install/ubuntu/",
|
||||
"https://vocalinux.com/install/fedora/",
|
||||
"https://vocalinux.com/install/arch/",
|
||||
"https://vocalinux.com/compare/",
|
||||
"https://vocalinux.com/remote-api/",
|
||||
"https://vocalinux.com/voice-activity-detection/",
|
||||
"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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* @jest-environment node
|
||||
*
|
||||
* Structural + real-browser layout checks for the marketing home page.
|
||||
*
|
||||
* 1. Structural: shipped CSS/markup include overflow guards (break-all, min-w-0).
|
||||
* 2. Browser: when BASE_URL is set (or local :3456 is up), playwright-core + system
|
||||
* Chrome measures scrollWidth vs clientWidth at three viewports.
|
||||
*
|
||||
* Run browser leg:
|
||||
* BASE_URL=http://127.0.0.1:3456 npm test -- --testPathPatterns=viewport-layout
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const root = path.resolve(__dirname, "../../..");
|
||||
const pageTsx = fs.readFileSync(path.join(root, "src/app/page.tsx"), "utf8");
|
||||
const terminalTsx = fs.readFileSync(
|
||||
path.join(root, "src/components/terminal-block.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const chromeTsx = fs.readFileSync(
|
||||
path.join(root, "src/components/site-chrome.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const globalsCss = fs.readFileSync(
|
||||
path.join(root, "src/styles/globals.css"),
|
||||
"utf8",
|
||||
);
|
||||
const homeSource = `${pageTsx}\n${terminalTsx}\n${chromeTsx}`;
|
||||
|
||||
const CHROME =
|
||||
process.env.CHROME_PATH ||
|
||||
["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium"].find(
|
||||
(p) => fs.existsSync(p),
|
||||
);
|
||||
|
||||
async function resolveBaseUrl(): Promise<string | null> {
|
||||
if (process.env.BASE_URL) return process.env.BASE_URL.replace(/\/$/, "");
|
||||
try {
|
||||
const res = await fetch("http://127.0.0.1:3456/", { signal: AbortSignal.timeout(2000) });
|
||||
if (res.ok) return "http://127.0.0.1:3456";
|
||||
} catch {
|
||||
/* offline */
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
describe("marketing home layout guards (shipped source)", () => {
|
||||
it("terminal and main constrain long monospace without page-wide overflow", () => {
|
||||
expect(globalsCss).toMatch(/overflow-x:\s*clip/);
|
||||
expect(globalsCss).toMatch(/\.terminal-panel pre/);
|
||||
expect(globalsCss).toMatch(/overflow-wrap:\s*anywhere|break-all|word-break/);
|
||||
expect(homeSource).toMatch(/break-all/);
|
||||
expect(homeSource).toMatch(/min-w-0/);
|
||||
expect(homeSource).toMatch(/overflow-x-clip|overflow-x-auto/);
|
||||
});
|
||||
|
||||
it("hero uses solid display type (no gradient text slop)", () => {
|
||||
expect(pageTsx).not.toMatch(/bg-clip-text/);
|
||||
expect(pageTsx).not.toMatch(/bg-gradient-to-r from-primary via-teal/);
|
||||
expect(pageTsx).toMatch(/font-display/);
|
||||
expect(pageTsx).toMatch(/TerminalBlock|terminal-panel/);
|
||||
});
|
||||
|
||||
it("uses the Voca family paper workbench tokens", () => {
|
||||
expect(globalsCss).toMatch(/--paper:\s*#f4f1e8/);
|
||||
expect(globalsCss).toMatch(/--brand:\s*#0f6b57/);
|
||||
expect(globalsCss).not.toMatch(/linear-gradient|radial-gradient|conic-gradient/);
|
||||
expect(pageTsx).not.toMatch(/linear-gradient|radial-gradient|conic-gradient/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("marketing home three-viewport overflow (live browser)", () => {
|
||||
const viewports = [
|
||||
{ name: "desktop", width: 1280, height: 800 },
|
||||
{ name: "tablet", width: 768, height: 1024 },
|
||||
{ name: "mobile", width: 390, height: 844 },
|
||||
];
|
||||
|
||||
it("has no horizontal overflow and keeps hero + install terminal in bounds", async () => {
|
||||
const baseUrl = await resolveBaseUrl();
|
||||
if (!baseUrl) {
|
||||
console.warn("Skipping live browser leg: no BASE_URL and :3456 not up");
|
||||
return;
|
||||
}
|
||||
if (!CHROME) {
|
||||
console.warn("Skipping live browser leg: no Chrome binary");
|
||||
return;
|
||||
}
|
||||
|
||||
const { chromium } = await import("playwright-core");
|
||||
const browser = await chromium.launch({
|
||||
executablePath: CHROME,
|
||||
headless: true,
|
||||
args: ["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"],
|
||||
});
|
||||
|
||||
try {
|
||||
for (const vp of viewports) {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: vp.width, height: vp.height },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.goto(`${baseUrl}/`, { waitUntil: "networkidle", timeout: 60000 });
|
||||
await page.waitForSelector("h1");
|
||||
|
||||
const metrics = await page.evaluate(() => {
|
||||
const de = document.documentElement;
|
||||
const hero = document.querySelector("h1");
|
||||
const terminal = document.querySelector(".terminal-panel");
|
||||
const heroRect = hero?.getBoundingClientRect();
|
||||
const termRect = terminal?.getBoundingClientRect();
|
||||
const inBounds = (r?: DOMRect) =>
|
||||
!!r &&
|
||||
r.width > 0 &&
|
||||
r.height > 0 &&
|
||||
r.left >= -1 &&
|
||||
r.right <= de.clientWidth + 1;
|
||||
return {
|
||||
overflowX: de.scrollWidth > de.clientWidth + 1,
|
||||
scrollWidth: de.scrollWidth,
|
||||
clientWidth: de.clientWidth,
|
||||
heroInBounds: inBounds(heroRect),
|
||||
terminalInBounds: inBounds(termRect),
|
||||
hasGradientText: !!document.querySelector('[class*="bg-clip-text"]'),
|
||||
hasVocalinux: document.body.innerText.includes("Vocalinux"),
|
||||
};
|
||||
});
|
||||
|
||||
expect({
|
||||
viewport: vp.name,
|
||||
...metrics,
|
||||
}).toEqual(
|
||||
expect.objectContaining({
|
||||
overflowX: false,
|
||||
heroInBounds: true,
|
||||
terminalInBounds: true,
|
||||
hasGradientText: false,
|
||||
hasVocalinux: true,
|
||||
}),
|
||||
);
|
||||
|
||||
if (vp.width < 768) {
|
||||
await page.getByRole("button", { name: /toggle menu/i }).click();
|
||||
const howVisible = await page
|
||||
.locator("header")
|
||||
.getByRole("link", { name: "How it works" })
|
||||
.isVisible();
|
||||
expect(howVisible).toBe(true);
|
||||
}
|
||||
|
||||
await context.close();
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}, 120000);
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Cpu,
|
||||
RotateCcw,
|
||||
Server,
|
||||
Settings,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const whisperControls = [
|
||||
{
|
||||
name: "No Timestamps",
|
||||
detail:
|
||||
"Disables timestamp token generation to reduce timestamp-related hallucinations.",
|
||||
},
|
||||
{
|
||||
name: "No Context",
|
||||
detail:
|
||||
"Stops whisper.cpp from conditioning on previous text when past context causes error loops.",
|
||||
},
|
||||
{
|
||||
name: "Temperature",
|
||||
detail:
|
||||
"Controls decoding randomness. The default 0.0 keeps dictation deterministic.",
|
||||
},
|
||||
{
|
||||
name: "Temperature Increment",
|
||||
detail:
|
||||
"Sets fallback steps for retry behavior, or disables fallback entirely with -1.0.",
|
||||
},
|
||||
{
|
||||
name: "Entropy Threshold",
|
||||
detail: "Helps detect repetition loops during decoding.",
|
||||
},
|
||||
{
|
||||
name: "Logprob Threshold",
|
||||
detail: "Triggers fallback when average token confidence is too low.",
|
||||
},
|
||||
{
|
||||
name: "No-Speech Threshold",
|
||||
detail: "Controls how strongly whisper.cpp treats audio as silence.",
|
||||
},
|
||||
{
|
||||
name: "Initial Prompt",
|
||||
detail:
|
||||
"Adds names, jargon, punctuation style, or other context to steer transcription.",
|
||||
},
|
||||
];
|
||||
|
||||
const useCases = [
|
||||
"Suppress repeated phrases or timestamp artifacts in long dictation sessions.",
|
||||
"Bias transcription toward project names, technical jargon, or personal vocabulary.",
|
||||
"Keep whisper.cpp deterministic for notes, code comments, and messages.",
|
||||
"Move Remote Server settings out of the way until power-user mode is enabled.",
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Advanced Whisper.cpp Settings for Linux Dictation",
|
||||
description:
|
||||
"Tune Vocalinux advanced whisper.cpp settings for anti-hallucination, decoding thresholds, initial prompts, and Remote API configuration.",
|
||||
path: "/advanced-settings",
|
||||
keywords: [
|
||||
"whisper.cpp advanced settings",
|
||||
"whisper anti hallucination linux dictation",
|
||||
"vocalinux advanced settings",
|
||||
"whisper.cpp no speech threshold",
|
||||
],
|
||||
});
|
||||
|
||||
export default function AdvancedSettingsPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "TechArticle",
|
||||
headline: "Advanced Whisper.cpp Settings for Linux Dictation",
|
||||
description:
|
||||
"Guide to Vocalinux advanced settings for whisper.cpp decoding, anti-hallucination controls, and Remote API configuration.",
|
||||
dateModified: "2026-06-11",
|
||||
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("/advanced-settings"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="border-primary/30 bg-primary/10 mb-4 inline-flex items-center gap-2 rounded-full border px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<SlidersHorizontal className="h-4 w-4" />
|
||||
Advanced settings
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Advanced Whisper.cpp Settings for Linux Dictation
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Vocalinux keeps the default setup simple, but power users can unlock
|
||||
advanced whisper.cpp decoding controls for anti-hallucination tuning,
|
||||
initial prompts, Remote API server configuration, Vulkan GPU device
|
||||
selection, and optional model keep-alive unload.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 md:grid-cols-3">
|
||||
{[
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: "Anti-hallucination controls",
|
||||
description:
|
||||
"Tune timestamp, context, confidence, and silence thresholds when a specific setup needs stricter decoding.",
|
||||
},
|
||||
{
|
||||
icon: Server,
|
||||
title: "Remote Server options",
|
||||
description:
|
||||
"Configure the Remote API URL, optional bearer token, and endpoint format from the same advanced area.",
|
||||
},
|
||||
{
|
||||
icon: RotateCcw,
|
||||
title: "Resettable power-user mode",
|
||||
description:
|
||||
"Advanced options are opt-in and can be reset when experimentation produces worse dictation.",
|
||||
},
|
||||
].map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<article
|
||||
key={item.title}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<span className="bg-primary/10 mb-4 inline-flex rounded-[12px] p-3">
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</span>
|
||||
<h2 className="mb-2 text-xl font-semibold">{item.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{item.description}
|
||||
</p>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-5 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Cpu className="h-5 w-5 text-primary" />
|
||||
Whisper.cpp Decoding Controls
|
||||
</h2>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{whisperControls.map((control) => (
|
||||
<article
|
||||
key={control.name}
|
||||
className="rounded-[12px] border border-border bg-muted p-5"
|
||||
>
|
||||
<h3 className="mb-2 text-lg font-semibold">{control.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">{control.detail}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 lg:grid-cols-[1fr_1.1fr]">
|
||||
<div className="rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Settings className="h-5 w-5 text-primary" />
|
||||
When to Use Advanced Settings
|
||||
</h2>
|
||||
<ul className="space-y-3 text-sm text-muted-foreground">
|
||||
{useCases.map((useCase) => (
|
||||
<li key={useCase} className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>{useCase}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<AlertTriangle className="h-5 w-5 text-muted-foreground" />
|
||||
Keep Defaults Unless You Need Them
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The default whisper.cpp settings are chosen for reliable everyday
|
||||
dictation. Advanced values can improve niche workflows, but they can
|
||||
also reduce accuracy if pushed too far. Change one value at a time
|
||||
and reset if quality drops.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-primary/20 bg-primary/5 rounded-[12px] border p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Related Guides</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Link href="/voice-activity-detection/" className="group">
|
||||
<h3 className="font-semibold">Silero VAD</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Learn how speech and silence are filtered before recognition.
|
||||
</p>
|
||||
<span className="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
VAD guide <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
<Link href="/remote-api/" className="group">
|
||||
<h3 className="font-semibold">Remote API</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Configure self-hosted or compatible remote transcription servers.
|
||||
</p>
|
||||
<span className="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
Remote guide <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
<Link href="/whisper-model-guide/" className="group">
|
||||
<h3 className="font-semibold">Whisper model guide</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Choose model sizes before tuning advanced decoding behavior.
|
||||
</p>
|
||||
<span className="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
Model guide <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { CheckCircle2, ChevronRight, DollarSign, Lock, Server, Shield, XCircle, Zap } from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const alternatives = [
|
||||
{
|
||||
name: "Dragon Naturally Speaking",
|
||||
type: "Proprietary Desktop",
|
||||
pros: ["High accuracy", "Industry standard", "Advanced commands"],
|
||||
cons: ["No Linux support", "Expensive subscription", "Closed source", "Cloud dependency"],
|
||||
pricing: "$500/year+",
|
||||
},
|
||||
{
|
||||
name: "Otter.ai",
|
||||
type: "Cloud Service",
|
||||
pros: ["Good accuracy", "Meeting transcription", "Collaboration features"],
|
||||
cons: ["Cloud-only (no offline)", "Subscription required", "Privacy concerns", "No Linux desktop app"],
|
||||
pricing: "$10-30/month",
|
||||
},
|
||||
{
|
||||
name: "Google Voice Typing",
|
||||
type: "Cloud Service",
|
||||
pros: ["Free", "Good accuracy", "Multi-language"],
|
||||
cons: ["Requires Chrome/Google Docs", "Cloud-only", "Privacy concerns", "No Linux desktop integration"],
|
||||
pricing: "Free",
|
||||
},
|
||||
{
|
||||
name: "Speechnotes",
|
||||
type: "Web App",
|
||||
pros: ["Free tier available", "Simple to use", "No installation"],
|
||||
cons: ["Browser-only", "Limited offline support", "No desktop integration", "Privacy concerns"],
|
||||
pricing: "$10/month pro",
|
||||
},
|
||||
{
|
||||
name: "Windows Voice Typing",
|
||||
type: "OS Built-in",
|
||||
pros: ["Free", "Integrated with Windows", "Decent accuracy"],
|
||||
cons: ["Windows only", "No Linux support", "Limited customization", "Cloud processing"],
|
||||
pricing: "Free (Windows only)",
|
||||
},
|
||||
];
|
||||
|
||||
const comparisonTable = [
|
||||
{ feature: "Linux Support", vocalinux: true, dragon: false, otter: false, google: true },
|
||||
{ feature: "Local engines", vocalinux: true, dragon: false, otter: false, google: false },
|
||||
{ feature: "Open Source", vocalinux: true, dragon: false, otter: false, google: false },
|
||||
{ feature: "Free Forever", vocalinux: true, dragon: false, otter: false, google: true },
|
||||
{ feature: "Desktop Integration", vocalinux: true, dragon: true, otter: false, google: false },
|
||||
{ feature: "Wayland Support", vocalinux: true, dragon: false, otter: false, google: false },
|
||||
{ feature: "Custom Shortcuts", vocalinux: true, dragon: true, otter: false, google: false },
|
||||
{ feature: "Voice Commands", vocalinux: true, dragon: true, otter: true, google: true },
|
||||
{ feature: "Multi-language", vocalinux: true, dragon: true, otter: true, google: true },
|
||||
{ feature: "GPU Acceleration", vocalinux: true, dragon: false, otter: false, google: false },
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Linux Alternatives to Dragon, Otter.ai and Speechnotes",
|
||||
description:
|
||||
"Looking for Dragon NaturallySpeaking, Otter.ai or Google Voice Typing on Linux? Compare offline Linux dictation options, including Vocalinux and Nerd Dictation.",
|
||||
path: "/alternatives",
|
||||
keywords: [
|
||||
"vocalinux alternatives",
|
||||
"dragon naturally speaking linux alternative",
|
||||
"otter.ai alternative",
|
||||
"linux speech to text comparison",
|
||||
"offline voice dictation",
|
||||
],
|
||||
});
|
||||
|
||||
export default function AlternativesPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Vocalinux vs Dragon, Otter, Google - Voice Dictation Comparison",
|
||||
description:
|
||||
"Comprehensive comparison of Vocalinux with other voice dictation solutions for Linux users.",
|
||||
dateModified: "2026-02-19",
|
||||
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("/alternatives"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="subpage-kicker">
|
||||
<Zap className="h-4 w-4" />
|
||||
Comparison
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Vocalinux vs The Alternatives
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Looking for voice dictation on Linux? Compare Vocalinux with Dragon, Otter.ai, Google Voice
|
||||
Typing, and other alternatives. See why Vocalinux is the best choice for Linux users.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-primary/20 bg-primary/5 p-6">
|
||||
<h2 className="mb-4 text-xl font-semibold">Why Vocalinux for Linux?</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Lock className="h-5 w-5 text-primary" />
|
||||
<span className="font-medium">Local engines</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="h-5 w-5 text-primary" />
|
||||
<span className="font-medium">Privacy First</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<DollarSign className="h-5 w-5 text-primary" />
|
||||
<span className="font-medium">Free Forever</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="h-5 w-5 text-primary" />
|
||||
<span className="font-medium">Linux Native</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6 overflow-x-auto">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Feature Comparison</h2>
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="pb-3 text-sm font-semibold">Feature</th>
|
||||
<th className="pb-3 text-sm font-semibold">Vocalinux</th>
|
||||
<th className="pb-3 text-sm font-semibold">Dragon</th>
|
||||
<th className="pb-3 text-sm font-semibold">Otter.ai</th>
|
||||
<th className="pb-3 text-sm font-semibold">Google</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-sm">
|
||||
{comparisonTable.map((row) => (
|
||||
<tr key={row.feature} className="border-b border-border">
|
||||
<td className="py-3 font-medium">{row.feature}</td>
|
||||
<td className="py-3">
|
||||
{row.vocalinux ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-primary" aria-label="Yes" />
|
||||
) : (
|
||||
<XCircle
|
||||
className="h-5 w-5 text-[#c45c5c]"
|
||||
aria-label="No"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3">
|
||||
{row.dragon ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-primary" aria-label="Yes" />
|
||||
) : (
|
||||
<XCircle
|
||||
className="h-5 w-5 text-[#c45c5c]"
|
||||
aria-label="No"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3">
|
||||
{row.otter ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-primary" aria-label="Yes" />
|
||||
) : (
|
||||
<XCircle
|
||||
className="h-5 w-5 text-[#c45c5c]"
|
||||
aria-label="No"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3">
|
||||
{row.google ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-primary" aria-label="Yes" />
|
||||
) : (
|
||||
<XCircle
|
||||
className="h-5 w-5 text-[#c45c5c]"
|
||||
aria-label="No"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section className="space-y-6">
|
||||
<h2 className="font-display text-2xl font-semibold">Detailed Comparison</h2>
|
||||
{alternatives.map((alt) => (
|
||||
<article
|
||||
key={alt.name}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold">{alt.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">{alt.type}</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-muted px-3 py-1 text-sm font-medium bg-muted">
|
||||
{alt.pricing}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="rounded-[12px] border border-border bg-muted/40 p-4">
|
||||
<p className="mb-2 text-sm font-semibold text-primary">Pros</p>
|
||||
<ul className="space-y-1.5">
|
||||
{alt.pros.map((pro) => (
|
||||
<li
|
||||
key={pro}
|
||||
className="flex items-start gap-2 text-sm text-muted-foreground"
|
||||
>
|
||||
<CheckCircle2 className="mt-0.5 h-3.5 w-3.5 shrink-0 text-primary" />
|
||||
{pro}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[12px] border border-border bg-muted/40 p-4">
|
||||
<p className="mb-2 text-sm font-semibold text-[#c45c5c]">
|
||||
Cons
|
||||
</p>
|
||||
<ul className="space-y-1.5">
|
||||
{alt.cons.map((con) => (
|
||||
<li
|
||||
key={con}
|
||||
className="flex items-start gap-2 text-sm text-muted-foreground"
|
||||
>
|
||||
<XCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#c45c5c]" />
|
||||
{con}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 font-display text-2xl font-semibold">Read the Nerd Dictation comparison</h2>
|
||||
<p className="mb-5 text-muted-foreground">
|
||||
Want a direct Linux-focused breakdown of CLI vs GUI workflows, Wayland behavior, and GPU
|
||||
support? Read our dedicated page.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/vs-nerd-dictation/"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Read the Nerd Dictation comparison
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</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 hover:bg-muted"
|
||||
>
|
||||
Engine comparison
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">The Bottom Line</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
If you are on Linux and need voice dictation, Vocalinux is a strong option: local
|
||||
engines, no usage telemetry, and desktop integration, at no cost. See the{" "}
|
||||
<Link href="/vs-nerd-dictation/" className="font-semibold hover:underline">
|
||||
nerd-dictation comparison
|
||||
</Link>{" "}
|
||||
for another free offline Linux tool.
|
||||
</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" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/privacy/"
|
||||
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 hover:bg-muted"
|
||||
>
|
||||
<Lock className="h-4 w-4" />
|
||||
Our Privacy Commitment
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { CheckCircle2, ChevronRight, Loader2, Play, Power, Settings, Terminal } from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const desktopEnvironments = [
|
||||
{
|
||||
name: "GNOME",
|
||||
autostartMethod: "XDG Autostart",
|
||||
supported: true,
|
||||
notes: "Default on Ubuntu, Fedora, Debian",
|
||||
},
|
||||
{
|
||||
name: "KDE Plasma",
|
||||
autostartMethod: "XDG Autostart",
|
||||
supported: true,
|
||||
notes: "Full support via System Settings",
|
||||
},
|
||||
{
|
||||
name: "Xfce",
|
||||
autostartMethod: "XDG Autostart",
|
||||
supported: true,
|
||||
notes: "Settings → Session and Startup",
|
||||
},
|
||||
{
|
||||
name: "Cinnamon",
|
||||
autostartMethod: "XDG Autostart",
|
||||
supported: true,
|
||||
notes: "System Settings → Startup Applications",
|
||||
},
|
||||
{
|
||||
name: "MATE",
|
||||
autostartMethod: "XDG Autostart",
|
||||
supported: true,
|
||||
notes: "Control Center → Startup Applications",
|
||||
},
|
||||
{
|
||||
name: "LXQt",
|
||||
autostartMethod: "XDG Autostart",
|
||||
supported: true,
|
||||
notes: "Preferences → LXQt Settings → Autostart",
|
||||
},
|
||||
{
|
||||
name: "Sway",
|
||||
autostartMethod: "XDG Autostart / config",
|
||||
supported: true,
|
||||
notes: "Use dex or exec in config",
|
||||
},
|
||||
{
|
||||
name: "Hyprland",
|
||||
autostartMethod: "XDG Autostart / config",
|
||||
supported: true,
|
||||
notes: "exec-once in config or XDG",
|
||||
},
|
||||
];
|
||||
|
||||
const howItWorks = [
|
||||
{
|
||||
step: 1,
|
||||
title: "Enable in Vocalinux",
|
||||
description:
|
||||
"Answer yes on the first-run prompt, or open Settings → General and turn on 'Start on Login'.",
|
||||
code: null,
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: "Desktop Entry Created",
|
||||
description: "Vocalinux creates a .desktop file in your autostart directory:",
|
||||
code: "~/.config/autostart/vocalinux.desktop",
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Auto-launch on Login",
|
||||
description: "Your desktop environment reads this file and launches Vocalinux when you log in.",
|
||||
code: null,
|
||||
},
|
||||
];
|
||||
|
||||
const managementMethods = [
|
||||
{
|
||||
method: "First-run prompt",
|
||||
description: "The welcome dialog asks once whether to start on login",
|
||||
icon: Power,
|
||||
},
|
||||
{
|
||||
method: "Settings Dialog",
|
||||
description: "Open Settings → General → Start on Login",
|
||||
icon: Settings,
|
||||
},
|
||||
{
|
||||
method: "Command Line",
|
||||
description: "Create/remove the autostart file manually",
|
||||
icon: Terminal,
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Autostart Voice Dictation on Linux Login | Vocalinux",
|
||||
description:
|
||||
"Configure Vocalinux to start automatically when you log in to Linux. XDG autostart support for GNOME, KDE, Xfce, and all major desktop environments.",
|
||||
path: "/autostart",
|
||||
keywords: [
|
||||
"Linux autostart voice dictation",
|
||||
"auto start on login Linux",
|
||||
"XDG autostart desktop entry",
|
||||
"GNOME startup applications",
|
||||
"KDE autostart",
|
||||
"voice dictation auto launch",
|
||||
"Linux desktop autostart",
|
||||
],
|
||||
});
|
||||
|
||||
export default function AutostartPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
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-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("/autostart"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="subpage-kicker">
|
||||
<Power className="h-4 w-4" />
|
||||
New in v0.7.0
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Autostart Voice Dictation on Login
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Have Vocalinux ready to go the moment you log in. Uses the Linux-standard XDG autostart
|
||||
mechanism - works with GNOME, KDE, Xfce, and all major desktop environments.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-4 sm:grid-cols-3">
|
||||
{managementMethods.map((method) => {
|
||||
const Icon = method.icon;
|
||||
return (
|
||||
<div
|
||||
key={method.method}
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<Icon className="mb-3 h-6 w-6 text-primary" />
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">{method.method}</h2>
|
||||
<p className="text-sm text-muted-foreground">{method.description}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">How It Works</h2>
|
||||
<div className="space-y-4">
|
||||
{howItWorks.map((step) => (
|
||||
<div
|
||||
key={step.step}
|
||||
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">
|
||||
{step.step}
|
||||
</span>
|
||||
<h3 className="font-semibold">{step.title}</h3>
|
||||
</div>
|
||||
<p className="ml-10 text-sm text-muted-foreground">{step.description}</p>
|
||||
{step.code && (
|
||||
<div className="ml-10 mt-2 rounded-lg bg-[color:var(--terminal)] p-2">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">{step.code}</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Desktop Environment Compatibility</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Vocalinux uses the XDG Autostart specification, which is supported by virtually all Linux
|
||||
desktop environments:
|
||||
</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">Desktop</th>
|
||||
<th className="pb-3 pr-4 font-semibold">Method</th>
|
||||
<th className="pb-3 pr-4 font-semibold">Supported</th>
|
||||
<th className="pb-3 font-semibold">Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{desktopEnvironments.map((de) => (
|
||||
<tr key={de.name} className="border-b border-border">
|
||||
<td className="py-3 pr-4 font-medium">{de.name}</td>
|
||||
<td className="py-3 pr-4 text-muted-foreground">{de.autostartMethod}</td>
|
||||
<td className="py-3 pr-4">
|
||||
{de.supported ? (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
Yes
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground"> - </span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 text-muted-foreground">{de.notes}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</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
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Vocalinux autostart uses <strong>XDG desktop entries</strong>, not systemd services. This
|
||||
means it starts as a regular desktop application in your graphical session - which is the
|
||||
correct way to launch GUI apps. No background services, no root required.
|
||||
</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">Manual Setup (Advanced)</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
If you prefer to manage autostart manually or need to customize the startup behavior:
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="mb-1 text-sm font-medium">Enable autostart:</p>
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-3">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">
|
||||
mkdir -p ~/.config/autostart && cp /usr/share/applications/vocalinux.desktop
|
||||
~/.config/autostart/
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1 text-sm font-medium">Disable autostart:</p>
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-3">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">rm ~/.config/autostart/vocalinux.desktop</code>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1 text-sm font-medium">Start minimized (skip welcome dialog):</p>
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-3">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">
|
||||
vocalinux --start-minimized
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Ready to Automate?</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Install Vocalinux and enable autostart. Your voice dictation will be ready the moment you
|
||||
log in.
|
||||
</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 Now
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/faq/"
|
||||
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 hover:bg-muted"
|
||||
>
|
||||
Common Questions
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,688 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
BookOpen,
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Clock,
|
||||
Download,
|
||||
Sparkles,
|
||||
Tag,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const releases = [
|
||||
{
|
||||
version: "v0.18.5",
|
||||
date: "2026-10-27",
|
||||
type: "stable",
|
||||
highlights: [
|
||||
"A stop pressed while a recording is starting can no longer leave the microphone recording forever (PR #1023)",
|
||||
"A microphone that stalls mid-read can no longer hang dictation in PROCESSING; capture reads happen outside the buffer lock (PR #1023)",
|
||||
"Keyboards survive suspend and the GNOME lock screen again: the evdev grab is released when forwarding cannot run (PR #1025)",
|
||||
"Dictation Pad and Settings no longer bind to other windows or travel workspaces together on Wayland (PR #1024)",
|
||||
"Ctrl+mouse-wheel zoom works again in Firefox and other apps (PR #967)",
|
||||
"install.sh gains an upgrade mode that keeps your existing configuration and models (PR #1026)",
|
||||
"Paste-injection copies are marked sensitive on wl-clipboard 2.2+, so clipboard managers that honor the hint keep dictated text out of history (PR #956); AUR source publication is gated on attested sdists (PR #1020)",
|
||||
"Tray prefers libayatana-appindicator (PR #1019), the Snap lists PulseAudio microphones (PR #1018), and the Wayland fallback overlay survives dock closes (PR #1017)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.18.4",
|
||||
date: "2026-10-21",
|
||||
type: "stable",
|
||||
highlights: [
|
||||
"Long push-to-talk dictations are queued in segments before the buffer cap, so nothing after the first two minutes is lost (PR #976)",
|
||||
"Model downloads only start after you confirm them, Cancel cancels the whole change without freezing Settings, and switching models keeps your language settings (PR #1006)",
|
||||
"Test Dictation always shows its own result; the Dictation Pad routing toggle moves to Settings > Dictation (PR #1005)",
|
||||
"The GPU picker follows ggml's device list and CPU runs use most of your cores instead of 2 threads (PR #1001)",
|
||||
"Press the recording hotkey again to cancel a slow transcription; nothing is typed or saved (PR #1004)",
|
||||
"Wayland fixes: unique RemoteDesktop portal request tokens (PR #996), xdotool for XWayland apps outside IBus (PR #1003), and the first recording keeps focus on GNOME (PR #998)",
|
||||
"Flatpak bundles onnxruntime for Silero VAD (PR #1002) and uses the X11 hotkey listener on X11 sessions (PR #997); keyboards with pointer axes like the Logitech K860 work again (PR #978)",
|
||||
"Parakeet installs work again (PR #1000), update checks follow the build channel and can be turned off (PR #999), and Slovenian is supported (PR #995)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.18.3",
|
||||
date: "2026-10-17",
|
||||
type: "stable",
|
||||
highlights: [
|
||||
"\"new line\" and \"new paragraph\" voice commands insert real line breaks, standalone and mid-utterance, with no stray punctuation left behind (PR #958, #962)",
|
||||
"Hotkeys work again on GNOME Shell X11: the evdev backend is preferred there over pynput (PR #963)",
|
||||
"Quitting no longer collapses multi-layout keyboards; the IBus engine restore is skipped when a real engine is live (PR #963)",
|
||||
"Snap, Flatpak, AppImage and .deb/.rpm builds compile pywhispercpp at the portable x86-64 baseline, ending SIGILL crashes on pre-Haswell CPUs (PR #959)",
|
||||
"Snap recipe drops the denied `uinput` plug so new revisions clear Store review; Wayland typing uses the RemoteDesktop portal (PR #960)",
|
||||
"Transcribe File's modal download dialog releases its grab once TinyDiarize lands, so the transcript's Copy and Save As buttons work (PR #965)",
|
||||
"Flatpak remote publish warns loudly when unconfigured instead of skipping silently (PR #961); README gains install-format badges and a Nix section (PR #949)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.18.2",
|
||||
date: "2026-10-12",
|
||||
type: "stable",
|
||||
highlights: [
|
||||
"Dictation overlay orb pulses and ripples with live microphone level (PR #940)",
|
||||
"Nix flake package with NixOS and Home Manager modules (PR #936), plus Vulkan GPU inference for pywhispercpp on Nix (PR #942)",
|
||||
"Held keyboards are left ungrabbed at startup, so typing during launch cannot strand a key (PR #941)",
|
||||
"Bundled pywhispercpp builds drop -march=native, fixing AppImage and Flatpak crashes on older CPUs (PR #939)",
|
||||
"Piped curl | bash install keeps the script on stdin and prompts via /dev/tty (PR #935)",
|
||||
"Installer covers cairo headers on openSUSE, Void, Solus and Alpine; evdev monitors remapper virtual keyboards ungrabbed (PR #937, #932)",
|
||||
"CI gates the remote curl | bash install path end to end (PR #898)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.18.1",
|
||||
date: "2026-10-08",
|
||||
type: "stable",
|
||||
highlights: [
|
||||
"Settings > Proxy page for model downloads and update checks behind SOCKS5 or HTTP CONNECT proxies, with optional auth (PR #909, fixes #655)",
|
||||
"Opt-in JSONL transcript persistence: a transcript survives restarts and failed text injections instead of being lost (PR #907, fixes #758)",
|
||||
"Native .deb and .rpm packages for Debian/Ubuntu and Fedora, x86_64 and aarch64, attached to the GitHub Release (PR #913)",
|
||||
"Slimmer tray menu: Start on Login and About now live only in Settings (PR #905, fixes #654)",
|
||||
"Speech Model Advanced pickers stage changes until you confirm Download instead of starting a fetch per click (PR #908, fixes #894)",
|
||||
"Dictation Pad no longer ghosts away after sitting idle under a Wayland compositor (PR #912, fixes #896)",
|
||||
"evdev keyboard discovery stops grabbing Logitech mice, Goodix touchpads, and power buttons as keyboards (PR #902, #917, #918, fixes #900, #914, #915)",
|
||||
"KDE Plasma 6 Wayland: KWin VirtualKeyboard detection fixed, so dictated letter case stops scrambling (PR #919, fixes #911)",
|
||||
"Text injection ends option parsing before typed text, so a chunk starting with a hyphen no longer truncates dictation (PR #922, fixes #921)",
|
||||
"Startup reuses an on-disk same-size whisper.cpp weight instead of re-resolving it (PR #923, fixes #916)",
|
||||
"uninstall.sh no longer deletes files in the directory you ran it from (PR #901, fixes #897)",
|
||||
"Settings > Advanced Initial Prompt accepts typed input again, and transcript and log saves no longer double the .txt extension (PR #927)",
|
||||
"deb declares python3-socks, rpm declares python3-pysocks; snap attach keeps checksums on failure; remote curl | bash install gated end to end (PR #920, #899, #906)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.18.0",
|
||||
date: "2026-10-02",
|
||||
type: "stable",
|
||||
highlights: [
|
||||
"Dictation Pad: in-app window receives dictation and you copy text out by hand, so Wayland injection quirks cannot touch it (PR #887, fixes #726)",
|
||||
"Hotkey suppression: evdev grabs the dictation shortcut and forwards everything else through a uinput clone; the key no longer types into the focused app, and still works when /dev/uinput is write-only (PR #873, #893, fixes #871)",
|
||||
"PipeWire capture path with microphone and system-audio sources (PR #889, #883, fixes #751, #760)",
|
||||
"RemoteDesktop portal text injection on Wayland, no ydotoold needed (PR #885, fixes #750)",
|
||||
"Per-language dictation shortcuts, follow the active keyboard layout while dictating, and recent dictations in the tray menu (PR #880, #837, #487, fixes #805, #821)",
|
||||
"File transcription with TinyDiarize per-speaker labels via --transcribe-file and the tray (PR #884, fixes #756)",
|
||||
"Opt-in D-Bus activation so compositor global shortcuts and scripts can start dictation (PR #568, fixes #761)",
|
||||
"Combined custom dictionary with transcript corrections, postprocessing script hook, bilingual dictation candidates (PR #890, #479, #424)",
|
||||
"Floating glowing dictation overlay, lower other audio while dictating, keep recording while an idle model reloads (PR #516, #861, #851)",
|
||||
"config.json can pin the text-injection backend (PR #649, fixes #476)",
|
||||
"GNOME Wayland: Alt+Shift and Win+Space layout switching keeps working; IBus guard no longer flips GNOME/X11 to a US layout (PR #876, #827, fixes #848)",
|
||||
"Shortcut recorder learns unmapped F19/F24 and XF86-aliased F13-F23 (PR #844, fixes #843)",
|
||||
"Vosk refuses a model that would exceed available memory instead of tripping the OOM killer (PR #850, fixes #676)",
|
||||
"Model downloads cancel stalled fetches; the download dialog shows verifying instead of a stalled 100% (PR #888, #864, fixes #679, #863)",
|
||||
"Update checker falls back when GitHub API rate-limits (PR #846, fixes #845)",
|
||||
"Settings: scrollable category list, Dictation Tone grayed while sounds are off, Test Dictation artifact and missing transcription fixed (PR #886, #877, #853, fixes #678, #849, #847, #720)",
|
||||
"AUR vocalinux-bin ships the AppImage with per-arch digests; releases publish a signed self-hosted Flatpak remote; snap-promote workflow gates stable promotion; snap gains hardware-observe (PR #879, #875, #881, #858, fixes #817, #785, #783, #857)",
|
||||
"Installer: hash-pinned dependencies and build tools, sourced modules, generated distro package map (PR #856, #862, #872)",
|
||||
"Optional verified Orukeet model for Parakeet; VocaGateway runs locally from Settings > Advanced (PR #840, #774)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.17.0",
|
||||
date: "2026-09-16",
|
||||
type: "stable",
|
||||
highlights: [
|
||||
"Faster Whisper engine: CTranslate2 / INT8 Whisper on CPU, optional extra, installer --engine=faster_whisper (PR #543)",
|
||||
"Parakeet TDT 0.6B via sherpa-onnx: default v3-european bundle or v2-english, installer --engine=parakeet (PR #802)",
|
||||
"Speech Model simple setup: language + speed/accuracy first; engine/size under Advanced (PR #801)",
|
||||
"First run seeds recognition language from keyboard layout / locale (PR #796, #777)",
|
||||
"Language picker filters while the list is open (PR #798)",
|
||||
"Recommended model button sets size and specialization together and will not ignore a file already on disk (PR #797, #778)",
|
||||
"Localized punctuation voice commands for it/fr/de/es/pt/nl/pl/ru (PR #642, #640)",
|
||||
"Bare F1–F24 push-to-talk shortcuts (PR #815)",
|
||||
"Snap packaging with ydotool and uinput for native Wayland; the Store denied the uinput grant, so later revisions drop the plug and type via the RemoteDesktop portal (PR #519, #823, #822)",
|
||||
"Release workflow attaches Vocalinux-<version>-{x86_64,aarch64}.flatpak (PR #786, #784)",
|
||||
"XWayland clipboard paste instead of layout-garbled xdotool type (PR #680, #657)",
|
||||
"Layout-aware Ctrl+V paste; ydotool releases Ctrl after a timed-out paste (PR #788, #675, #658)",
|
||||
"Read WM_CLASS with xprop so xdotool stops dumping core on KDE Plasma Wayland (PR #807)",
|
||||
"Settings freeze on already-downloaded model, English whisper.cpp .en variant, PTT tray redraw, sink-wake audio cues, Parakeet decode survival (PR #790, #795, #780, #809, #804, #803)",
|
||||
"English-only models no longer hide other languages; picking Polish or auto-detect switches to the multilingual sibling (PR #826)",
|
||||
"Speech Model copy and Unused downloads as a sibling expander of Advanced; VocaWin on the About family grid (PR #826)",
|
||||
"install.sh: Faster Whisper is CPU CTranslate2/INT8; GPU backend does not install the CUDA toolkit (PR #826)",
|
||||
"HDA analog mics (3-8 channel) open at native layout instead of forced 2ch/48kHz; capture is downmixed to mono (PR #829, fixes #813)",
|
||||
"Native GTK installs (AUR / install.sh) follow OS dark/light; skipped when GTK_THEME is set so AppImage and user overrides stay in charge (PR #830, fixes #816)",
|
||||
"Lock-derived Flatpak/AUR deps, hash-pinned exports for every engine extra, published-release verification, install.sh distro matrix (PR #819, #828, #791, #810)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.16.2",
|
||||
date: "2026-09-05",
|
||||
type: "stable",
|
||||
highlights: [
|
||||
"KDE: skip leftover IBus when it is not the session IM so dictation types into Kate, browsers, and terminals (PR #753, fixes #752)",
|
||||
"Wayland shortcuts: wtype and ydotool deliver real chords instead of refusing or silently doing nothing (PR #715)",
|
||||
"IBus shortcuts: route X11_IBUS through xdotool and WAYLAND_IBUS through wtype/ydotool (PR #716)",
|
||||
"Voice command delete that: send real BackSpace key events instead of U+0008 text (PR #714)",
|
||||
"Installer: Fedora and Arch need glslc/shaderc packages, not glslang (PR #763, #604)",
|
||||
"Nightly: stamp version.py before python -m build so wheel metadata matches the filename (PR #762)",
|
||||
"Release integrity: checksums, signatures, and pinned builders for release artifacts (PR #759, epic #701 phase 5)",
|
||||
"CI: gate AUR PKGBUILD builds on every PR; test the distros the docs promise and fix docs drift (PR #772, #773)",
|
||||
"Site: VocaGateway family card is Beta; README logo, badges, and privacy copy (PR #765, #764)",
|
||||
"Deps: bump the github-actions group (PR #766)",
|
||||
"Snap 0.16.2 edge (rev 7) has no uinput plug: snap connect vocalinux:uinput fails, and dictation only reaches XWayland apps. Later revisions ship the RemoteDesktop portal instead; the Store denied the uinput grant (PR #823)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.16.1",
|
||||
date: "2026-08-30",
|
||||
type: "stable",
|
||||
highlights: [
|
||||
"Startup uses the engine's own model size instead of a leftover generic key; the model is saved only after it loaded (PR #684, #685, fixes #681)",
|
||||
"Unused leftover-model list shows every row; one ConfigManager so Settings writes stick (PR #686, #691, fixes #683, #689)",
|
||||
"Tray stays idle after leftover transcription on toggle stop, and the missing-model notification can download the recommended model (PR #741, #687)",
|
||||
"Terminals get Ctrl+Shift+V paste; GNOME XWayland layout is restored after scoped IBus inject (PR #734, #742)",
|
||||
"Installer requires Python 3.11, verifies model downloads, and no longer invents GPUs (PR #713, #736)",
|
||||
"AppImage built against a glibc floor that boots on Debian 12 through current Fedora (PR #743, #744)",
|
||||
"AUR: build against Arch extra setuptools 84, and skip context_params on AUR pywhispercpp 1.4 so the app starts",
|
||||
"Settings dropdowns share a width; About is quieter with family platform marks (PR #754)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.16.0",
|
||||
date: "2026-08-23",
|
||||
type: "stable",
|
||||
highlights: [
|
||||
"In-app update checker (stable/nightly) plus tray Update Available when GitHub has a newer release (PR #631, #645)",
|
||||
"New installs default to hold Right Alt push-to-talk; existing configs keep their shortcut (PR #648)",
|
||||
"Searchable language list and delete unused downloaded speech models (PR #672, #671)",
|
||||
"Family dictation tone picker in Settings → Audio: Lift, Flick, Ember, Step, Voca, Soft, Chirp, Scale, Drop, Glass, Off, plus Preview. New installs default to Voca. Catalog tones are the family preview WAVs, not synthesized files (PR #707, #708)",
|
||||
"License migrated to AGPL-3.0 (PR #660)",
|
||||
"App icon, tray states, and site favicons use the shared Voca family mic instead of the old Linux rounded-rect (PR #704)",
|
||||
"Installer hardened with Justfile and uv lockfiles; distro python3-gi required (no pip sdist of PyGObject). Epic #701 remains open (PR #700, #705, #706)",
|
||||
"Test Dictation no longer reports no speech when recognition never started (PR #702)",
|
||||
"IBus/X11 reliability: restorable scoped injection, engine restore after teardown, XKB layout restore (PR #623, #643, #665)",
|
||||
"Audio, clipboard, and AppImage/GPU packaging fixes (PR #629, #673, #588, #646, #674, #637)",
|
||||
"Settings About page groups this app, the VocaHQ family, and talk-to-us links (PR #718)",
|
||||
"vocalinux.com restyled to the Voca family workbench; Discord and X links point at VocaHQ (PR #728, #729, #722, #717)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.15.0",
|
||||
date: "2026-07-28",
|
||||
type: "stable",
|
||||
highlights: [
|
||||
"Searchable sidebar settings with live search replace the seven-tab notebook (PR #601)",
|
||||
"AppImage packages for x86_64 and aarch64 on GitHub Releases (PR #573, #602)",
|
||||
"Expanded speech-language catalog (~33 languages + Auto-detect), including Hungarian; VOSK only lists languages with official models (PR #616, fixes #565)",
|
||||
"Settings: dictation status / mic level / Test Dictation / Close live in the sidebar footer (PR #618)",
|
||||
"Settings: Custom Shortcut Record/Set controls show again (PR #619)",
|
||||
"Languages: English (India) maps to Whisper code en for whisper.cpp / Whisper / remote API (PR #617)",
|
||||
"Auto-capitalize after sentence punctuation; trailing space so the next utterance does not glue on (PR #554, #608)",
|
||||
"Auto-pause competing apps and idle model keep-alive unload for battery/GPU headroom (PR #592, closes #445, #591)",
|
||||
"Vulkan: auto-select discrete GPU and pick a device in Advanced settings (PR #590, closes #589)",
|
||||
"Wayland: use IBus on previously unbridged compositors when ibus-wayland is running (PR #614, closes #607)",
|
||||
"IBus: keep engine teardown correct when parent destroy fails (PR #613, fixes #606)",
|
||||
"CLI: vocalinux --version (PR #563, closes #555)",
|
||||
"Settings info notices flattened; Bluetooth mic heap-corruption fix; KDE unbridged-IBus skip when ibus-wayland is absent; xdotool focus preserve; installer/AUR fixes (PR #615, #599, #577, #564, #583, #569, #597, #579, #586)",
|
||||
"Marketing site redesign; languages page per-engine badges; robots.txt indexing fix (PR #582, #616, #610)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.14.2",
|
||||
date: "2026-07-17",
|
||||
type: "stable",
|
||||
highlights: [
|
||||
"IBus: restore engine process launch after Flatpak XDG path import so the engine no longer dies with ImportError and falls back to ydotool/clipboard paste (PR #534)",
|
||||
"IBus: wait for FocusIn before commit on scoped injection so the first dictation of a session is not dropped on GNOME Wayland (PR #533, fixes #523)",
|
||||
"Settings UI: notebook tabs scroll so the dialog fits the monitor; wheel events from unfocused combos/spins reach the tab scroller (PR #538, #541)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.14.1",
|
||||
date: "2026-07-17",
|
||||
type: "stable",
|
||||
highlights: [
|
||||
"Flatpak packaging for universal distribution: whisper.cpp engine, XDG sandbox paths, global hotkeys via evdev, Wayland text injection via wl-copy + ydotool (PR #484, closes #167)",
|
||||
"AUR package and CI publish path for Arch Linux (PR #518)",
|
||||
"Layout-aware combo keys so custom shortcuts work on non-US keyboard layouts (PR #514)",
|
||||
"Installer fix for sg not found on Ubuntu 26.04 / Debian 13 (PR #524)",
|
||||
"Text injection treats XIM none as unset (PR #512)",
|
||||
"Website screenshot gallery refresh and Dependabot npm alert fixes (PR #521, #515)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.14.0-beta",
|
||||
date: "2026-07-13",
|
||||
type: "beta",
|
||||
highlights: [
|
||||
"Configurable modifier+key hotkeys: set custom shortcuts with any combination of Ctrl, Alt, Shift, and Super plus a letter/number key, e.g. Alt+R or Ctrl+Shift+V (PR #493)",
|
||||
"Remote API engine now supports FunASR/SenseVoice models via OpenAI-compatible endpoints; SenseVoice metadata labels are stripped before text injection (PR #468)",
|
||||
"GNOME Wayland IBus text injection restored when only a bare xkb engine is configured; engine restore fallback now picks the correct IM engine (PR #506, #500)",
|
||||
"KDE Plasma Wayland IBus text-injection path restored after recent compositor-detection regressions (PR #502)",
|
||||
"Wayland text injection now waits for held modifiers to release before typing, preventing accidental shortcut triggers and garbled output (PR #494)",
|
||||
"Shortcuts UI keeps preset and custom shortcut selection exclusive: selecting a preset clears the custom field, and setting a custom combo selects the Custom Shortcut preset (PR #509)",
|
||||
"whisper.cpp no longer defaults to all CPU cores on hybrid processors, improving UI responsiveness and battery life (PR #492)",
|
||||
"Fixed a crash on recording start when the selected audio device index no longer matches the current system enumeration (PR #499)",
|
||||
"Installer includes xsel as a fallback for the Wayland clipboard path when xclip is unavailable (PR #496)",
|
||||
"Removed an outdated long comment about whisper.cpp default thread counts (PR #505)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.13.0-beta",
|
||||
date: "2026-06-30",
|
||||
type: "beta",
|
||||
highlights: [
|
||||
"Guided whisper.cpp model selection: pick a size plus a specialization (English-only, quantized Q5/Q8, or Large v3 Turbo) through split Model Size and Specialization dropdowns with in-app guidance; the --model flag also accepts exact IDs like medium.en-q5_0 and large-v3-turbo (PR #465)",
|
||||
"Dictation now keeps a space between segments spoken with a pause in between, so words no longer run together after a silence (PR #464)",
|
||||
"Keyboard shortcuts now work on keyboards hotplugged after Vocalinux starts, with the evdev backend rescanning for new devices and recovering from disconnects (PR #467)",
|
||||
"Vocalinux now detects KDE Plasma Wayland sessions and points you to enable IBus Wayland for reliable text injection, surfaced during install and when wtype injection fails (PR #466)",
|
||||
"Wayland: fixed garbled text on non-US keyboard layouts (AZERTY/QWERTZ/Dvorak) and a clipboard-copy hang; ydotool now pastes through the clipboard, which is layout-independent (PR #480)",
|
||||
"Wayland: use wtype/ydotool instead of IBus on compositors that don't bridge it to native apps like COSMIC, Sway, and Hyprland, fixing silent text drops (PR #486)",
|
||||
"Wayland/IBus: require a real IM engine before using IBus, so a bare xkb layout no longer causes silent text drops on GNOME/Mutter and other compositors (#478)",
|
||||
"Wayland: keep the keyboard layout intact by not running setxkbmap, which was flipping XWayland apps to us after dictation (#474)",
|
||||
"Faster ydotool text injection via an explicit --key-delay (PR #488)",
|
||||
"Settings dialog height capped on high-resolution displays (PR #465)",
|
||||
"Refreshed website docs with new feature pages for Remote API, Silero VAD, advanced whisper.cpp settings, and desktop reliability, plus responsive layout polish (PR #470)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.12.0-beta",
|
||||
date: "2026-06-07",
|
||||
type: "beta",
|
||||
highlights: [
|
||||
"Remote API speech recognition engine with installation and configuration support (PR #335)",
|
||||
"Silero VAD drops silence-only buffers for cleaner dictation when ONNX Runtime support is available (PR #447)",
|
||||
"Thread safety hardening for Remote API, IBus, and text injection paths (PR #452)",
|
||||
"IBus preserves user engines for dead keys and captures the current engine during scoped activation (PR #457, #458)",
|
||||
"Remote Server settings now respect the Advanced toggle and the settings dialog fits lower-resolution screens (PR #454, #456)",
|
||||
"CUDA diagnostics now include auto-remediation and behavioral tests (PR #451)",
|
||||
"Corrected whisper.cpp and VOSK model download size metadata (PR #453)",
|
||||
"Startup now works without the pynput backend (PR #448)",
|
||||
"Remote API developer test server documentation (PR #455)",
|
||||
"Website speech demo browser support clarification (PR #449) and GitHub Sponsors funding configuration",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.11.0-beta",
|
||||
date: "2026-05-30",
|
||||
type: "beta",
|
||||
highlights: [
|
||||
"New Advanced Settings tab with whisper.cpp anti-hallucination parameters - temperature, no_speech_threshold, max segment length, and more (PR #415)",
|
||||
"IBus engine readiness probe at startup with hardened retries (PR #391)",
|
||||
"IBus runtime failure recovery without app restart (PR #411)",
|
||||
"IBus engine instance destruction handled on keyboard layout switch (fixes #388, closes #389)",
|
||||
"Preserve final speech on stop - no more truncated transcriptions (fixes #401)",
|
||||
"Play stop sound immediately on release and after audio thread joins (PR #426, #436)",
|
||||
"Repair pywhispercpp library loading in installer (PR #433)",
|
||||
"Reduce whisper.cpp CPU threads and ensure GPU backend builds in dev mode (PR #439)",
|
||||
"Correct openSUSE Tumbleweed dependencies with fallback handling (PR #418, #420)",
|
||||
"Harden Debian compatibility layer in installer (PR #437)",
|
||||
"Add Python 3.14 support and bump lxml>=6.1.0 (fixes #404)",
|
||||
"Validate pyproject.toml/setup.py content before entering local repo mode (fixes #396)",
|
||||
"Reuse existing whispercpp builds during install (PR #421)",
|
||||
"Refresh ldconfig after openSUSE typelib install, clarify python3XY placeholder convention (PR #438)",
|
||||
"Clean up runtime log noise and cache hardware detection",
|
||||
"Test coverage: recognition internals, IBus edge cases, CI notification suppression (PR #410, #414)",
|
||||
"Clarify PyPI installation requirements (PR #423)",
|
||||
"Dependency bumps: Next.js security updates (PR #399, #429), PostCSS",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.10.2-beta",
|
||||
date: "2026-04-08",
|
||||
type: "beta",
|
||||
highlights: [
|
||||
"Handle non-ASCII characters (á, é, ñ, etc.) with ydotool via clipboard paste fallback (fixes #362, PR #376)",
|
||||
"Detect IBus on Wayland without legacy env vars and fix text injection (PR #381)",
|
||||
"Start IBus engine process before checking registration to fix startup on some systems (fixes #360, PR #361)",
|
||||
"Add missing dependencies for Pop!_OS and Ubuntu 24.04+ including cmake, libcairo2-dev, libgirepository (PR #379)",
|
||||
"Systematic code quality refactor across 20 dimensions (PR #377)",
|
||||
"Clarify missing GNOME AppIndicator support on Debian (PR #385)",
|
||||
"Redesigned OG image for vocalinux.com - cleaner, professional, text-based layout (PR #392)",
|
||||
"Test coverage improvements: mock Notify module, tray degraded-startup, IBus socket-readiness branches (PR #384, #386, #390)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.10.1-beta",
|
||||
date: "2026-03-30",
|
||||
type: "beta",
|
||||
highlights: [
|
||||
"Bundled package resources to prevent missing system tray icons (fixes #349, PR #354)",
|
||||
"Stopped recognition before engine switches to prevent segfaults (fixes #350, PR #355)",
|
||||
"Added a dedicated Close button in Settings for better WM compatibility (fixes #323, PR #356)",
|
||||
"Preserved XKB layout state during Vocalinux IBus activation (fixes #292, PR #343)",
|
||||
"Auto-recover speech recognition after system suspend/resume via new D-Bus handler (fixes #367, PR #369)",
|
||||
"Restart keyboard shortcut backend after resume to keep shortcuts working (PR #371)",
|
||||
"Delayed keyboard restart to allow USB re-enumeration after resume (PR #372)",
|
||||
"Fixed premature transcription during push-to-talk silence (fixes #358, PR #359)",
|
||||
"Disabled copy-to-clipboard by default in Settings (PR #370)",
|
||||
"Maintenance updates: npm/yarn dependency refresh and brace-expansion dev dependency bump (PR #346, #357)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.10.0-beta",
|
||||
date: "2026-03-25",
|
||||
type: "beta",
|
||||
highlights: [
|
||||
"Generalized keyboard modifier alias matching across layouts for more reliable shortcuts",
|
||||
"Audio channel probing now validates device-supported sample rates before selection",
|
||||
"evdev now handles SYN_DROPPED to prevent stale modifier state",
|
||||
"IBus engine activation now uses register_component for stronger text-injection startup",
|
||||
"Settings dialog forces window decorations to prevent missing-titlebar behavior",
|
||||
"Tray icon refresh now uses icon names for better AppIndicator compatibility",
|
||||
"Coverage increased to 80%+ with additional IBus launch/main-entry tests",
|
||||
"Installer and CI polish: latest-tag fallback via GitHub API, Node 24 deploy, and path-filtered workflows",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.9.0-beta",
|
||||
date: "2026-03-14",
|
||||
type: "beta",
|
||||
highlights: [
|
||||
"Left/right modifier key distinction - choose Left Ctrl vs Right Ctrl for your shortcut",
|
||||
"Sound effects toggle - enable or disable audio feedback from Settings",
|
||||
"Wayland clipboard fallback - auto-copies text when virtual keyboard injection isn't available",
|
||||
"Display availability check - graceful error when running in headless environments",
|
||||
"Fixed unwanted leading space at the start of each new transcription session",
|
||||
"Fixed shortcut mode (toggle/push-to-talk) not applying on startup",
|
||||
"Improved Debian/pipx installation guidance and cross-distro error messages",
|
||||
"Grouped shortcut selector UI - shortcuts organised by Either/Left/Right side",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.8.0-beta",
|
||||
date: "2026-03-01",
|
||||
type: "beta",
|
||||
highlights: [
|
||||
"Push-to-talk shortcut mode (hold to speak, release to stop)",
|
||||
"Optional voice commands with VOSK auto-enable behavior",
|
||||
"Improved shortcut mode switching and callback reliability",
|
||||
"IBus active-method detection before text injection",
|
||||
"Audio hardware compatibility fixes (sample rate and channel count)",
|
||||
"Fedora startup dialog stability fix",
|
||||
"Web SEO expansion and homepage visual refresh",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.7.0-beta",
|
||||
date: "2026-02-22",
|
||||
type: "beta",
|
||||
highlights: [
|
||||
"Autostart on login support (XDG autostart)",
|
||||
"Tabbed settings dialog (Speech Engine, Recognition, Text Injection, Audio Feedback, General)",
|
||||
"Intel GPU compatibility detection - auto fallback to CPU for incompatible GPUs",
|
||||
"Single instance prevention - prevents multiple Vocalinux running",
|
||||
"Evdev device management - removes disconnected devices to prevent CPU spin",
|
||||
"Improved GPU detection - avoids false positives on systems without dev libraries",
|
||||
"IBus fallback - skip setup when daemon not running",
|
||||
"Fedora dnf check-update fix",
|
||||
"Web SEO - 7 new optimized pages",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.6.3-beta",
|
||||
date: "2026-02-19",
|
||||
type: "beta",
|
||||
highlights: [
|
||||
"Fixed installer default tag pointing to correct version",
|
||||
"Added missing psutil dependency for fresh installs",
|
||||
"Process check and interactive prompts in install/uninstall",
|
||||
"Removed leading space from first speech transcription",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.6.2-beta",
|
||||
date: "2026-02-18",
|
||||
type: "beta",
|
||||
highlights: [
|
||||
"Interactive backend selection (GPU/CPU)",
|
||||
"Enhanced welcome message",
|
||||
"Simplified install commands",
|
||||
"Better GPU support and Vulkan detection",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.6.0-beta",
|
||||
date: "2026-02-12",
|
||||
type: "beta",
|
||||
highlights: [
|
||||
"whisper.cpp as default engine",
|
||||
"Multi-language support with auto-detection",
|
||||
"System tray indicator",
|
||||
"Full Wayland support",
|
||||
"IBus text injection engine",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.5.0-beta",
|
||||
date: "2026-02-06",
|
||||
type: "beta",
|
||||
highlights: [
|
||||
"First beta release",
|
||||
"Stable core functionality",
|
||||
"Multiple speech engine support",
|
||||
"Improved text injection",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.4.1-alpha",
|
||||
date: "2026-01-29",
|
||||
type: "alpha",
|
||||
highlights: [
|
||||
"Language selector UI",
|
||||
"App drawer launch fix",
|
||||
"Better commit handling",
|
||||
"Improved update mechanism",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.4.0-alpha",
|
||||
date: "2026-01-29",
|
||||
type: "alpha",
|
||||
highlights: [
|
||||
"Multi-language support (French, German, Russian)",
|
||||
"Debian 13+ compatibility",
|
||||
"Python 3.12+ support",
|
||||
"Tag-based version selection in installer",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v0.3.0-alpha",
|
||||
date: "2026-01-21",
|
||||
type: "alpha",
|
||||
highlights: [
|
||||
"Initial public alpha",
|
||||
"Basic speech recognition",
|
||||
"X11 text injection",
|
||||
"VOSK engine support",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const getTypeStyles = (type: string) => {
|
||||
switch (type) {
|
||||
case "stable":
|
||||
return {
|
||||
badge:
|
||||
"bg-primary/10 text-primary",
|
||||
label: "Stable",
|
||||
};
|
||||
case "beta":
|
||||
return {
|
||||
badge:
|
||||
"bg-primary/10 text-primary",
|
||||
label: "Beta",
|
||||
};
|
||||
case "alpha":
|
||||
return {
|
||||
badge:
|
||||
"bg-muted text-muted-foreground",
|
||||
label: "Alpha",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
badge: "bg-muted text-muted-foreground",
|
||||
label: type,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Changelog and Release Notes",
|
||||
description:
|
||||
"Track Vocalinux release history and version updates. See what's new in each version of our Linux voice dictation software.",
|
||||
path: "/changelog",
|
||||
keywords: [
|
||||
"vocalinux changelog",
|
||||
"vocalinux release notes",
|
||||
"voice dictation linux updates",
|
||||
"speech to text linux versions",
|
||||
],
|
||||
});
|
||||
|
||||
export default function ChangelogPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Vocalinux Changelog - Release History",
|
||||
description:
|
||||
"Complete release history for Vocalinux, the offline voice dictation software for Linux.",
|
||||
dateModified: "2026-10-27",
|
||||
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("/changelog"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="border-primary/30 bg-primary/10 mb-4 inline-flex items-center gap-2 rounded-full border px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<Clock className="h-4 w-4" />
|
||||
Release History
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Vocalinux Changelog
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Track every release and see how Vocalinux has evolved. From initial
|
||||
alpha to stable releases, follow the journey of Linux voice dictation.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-6">
|
||||
{releases.map((release, index) => {
|
||||
const styles = getTypeStyles(release.type);
|
||||
return (
|
||||
<article
|
||||
key={release.version}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<div className="mb-4 flex flex-wrap items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="h-5 w-5 text-primary" />
|
||||
<span className="font-display text-2xl font-semibold">{release.version}</span>
|
||||
</div>
|
||||
<span
|
||||
className={`rounded-full px-3 py-1 text-xs font-semibold ${styles.badge}`}
|
||||
>
|
||||
{styles.label}
|
||||
</span>
|
||||
{index === 0 && (
|
||||
<span className="bg-primary/10 rounded-full px-3 py-1 text-xs font-semibold text-primary">
|
||||
Latest
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{release.date}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-2">
|
||||
{release.highlights.map((highlight) => (
|
||||
<li
|
||||
key={highlight}
|
||||
className="flex items-start gap-2 text-muted-foreground"
|
||||
>
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
{highlight}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{index === 0 && (
|
||||
<div className="mt-5 flex flex-wrap gap-3">
|
||||
{[
|
||||
{ href: "/remote-api/", label: "Remote API guide" },
|
||||
{
|
||||
href: "/voice-activity-detection/",
|
||||
label: "Silero VAD guide",
|
||||
},
|
||||
{ href: "/advanced-settings/", label: "Advanced settings" },
|
||||
{
|
||||
href: "/desktop-reliability/",
|
||||
label: "Reliability overview",
|
||||
},
|
||||
].map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="border-primary/30 bg-primary/10 hover:bg-primary/15 inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm font-semibold text-primary"
|
||||
>
|
||||
{link.label}
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 border-t border-border pt-4">
|
||||
<a
|
||||
href={`https://github.com/VocaHQ/vocalinux/releases/tag/${release.version}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
View release on GitHub
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Stay Updated</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li className="flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
Watch the repository on GitHub for release notifications
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Zap className="h-4 w-4 text-primary" />
|
||||
Re-run the installer to update to the latest version
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<BookOpen className="h-4 w-4 text-primary" />
|
||||
Check the{" "}
|
||||
<Link
|
||||
href="/install/"
|
||||
className="font-semibold text-primary hover:underline"
|
||||
>
|
||||
install guide
|
||||
</Link>{" "}
|
||||
for update instructions
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Cpu,
|
||||
Server,
|
||||
Sparkles,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const engineTable = [
|
||||
{
|
||||
engine: "whisper.cpp",
|
||||
speed: "Fastest startup + low latency",
|
||||
hardware: "CPU + AMD/Intel/NVIDIA GPU",
|
||||
accuracy: "High (best overall balance)",
|
||||
footprint: "Small models available (~74MB tiny)",
|
||||
bestFor: "Most users who want strong speed + quality",
|
||||
icon: Zap,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-muted",
|
||||
},
|
||||
{
|
||||
engine: "Whisper (OpenAI)",
|
||||
speed: "Slower install and startup",
|
||||
hardware: "CPU or NVIDIA CUDA",
|
||||
accuracy: "High",
|
||||
footprint: "Large dependency footprint (~2.3GB)",
|
||||
bestFor: "Users already standardized on PyTorch stack",
|
||||
icon: Sparkles,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
engine: "Faster Whisper",
|
||||
speed: "Fast CPU inference (CTranslate2 / INT8)",
|
||||
hardware: "CPU",
|
||||
accuracy: "High (Whisper-class)",
|
||||
footprint: "Optional extra; not in AppImage/Flatpak/Snap",
|
||||
bestFor: "CPU-only machines that want Whisper quality without PyTorch",
|
||||
icon: Sparkles,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
engine: "VOSK",
|
||||
speed: "Very fast realtime on low-end systems",
|
||||
hardware: "CPU",
|
||||
accuracy: "Good for lightweight use",
|
||||
footprint: "Very lightweight (~40MB model)",
|
||||
bestFor: "Older hardware and minimal-resource environments",
|
||||
icon: Cpu,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
engine: "Parakeet",
|
||||
speed: "Fast CPU (sherpa-onnx TDT 0.6B)",
|
||||
hardware: "CPU",
|
||||
accuracy: "High on bundled English / European languages",
|
||||
footprint: "Large download (~639MB v3-european default)",
|
||||
bestFor: "English or European dictation without the Whisper catalog picker",
|
||||
icon: Cpu,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-muted",
|
||||
},
|
||||
{
|
||||
engine: "Remote API",
|
||||
speed: "Depends on server + network latency",
|
||||
hardware: "Client CPU + remote Whisper server",
|
||||
accuracy: "Depends on remote model",
|
||||
footprint: "No local model required",
|
||||
bestFor: "Powerful LAN servers or shared transcription backends",
|
||||
icon: Server,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Vosk vs Whisper vs whisper.cpp: Linux Speech Engines",
|
||||
description:
|
||||
"Vosk vs Whisper vs whisper.cpp for offline Linux speech to text, plus Faster Whisper, Parakeet and Remote API: speed, accuracy, hardware needs and privacy.",
|
||||
path: "/compare",
|
||||
keywords: [
|
||||
"whisper.cpp vs vosk",
|
||||
"remote api speech recognition linux",
|
||||
"linux speech recognition comparison",
|
||||
"best voice dictation engine linux",
|
||||
"whisper vs vosk linux",
|
||||
],
|
||||
});
|
||||
|
||||
export default function CompareEnginesPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline:
|
||||
"Whisper.cpp vs Whisper vs VOSK vs Remote API for Linux Voice Dictation",
|
||||
description:
|
||||
"Technical comparison of local and remote speech recognition engines for Linux voice typing.",
|
||||
dateModified: "2026-06-11",
|
||||
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("/compare"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="border-primary/30 bg-primary/10 mb-4 inline-flex items-center gap-2 rounded-full border px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Speech Engine Comparison
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
whisper.cpp vs Whisper vs VOSK vs Remote API on Linux
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
If you are choosing a Linux speech-to-text engine, this page gives a
|
||||
practical side-by-side comparison focused on latency, hardware
|
||||
support, install footprint, privacy boundary, and real desktop usage.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4 md:hidden">
|
||||
{engineTable.map((row) => {
|
||||
const Icon = row.icon;
|
||||
return (
|
||||
<article
|
||||
key={row.engine}
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<span className={`inline-flex rounded-md p-2 ${row.iconBg}`}>
|
||||
<Icon className={`h-5 w-5 ${row.iconColor}`} />
|
||||
</span>
|
||||
<h2 className="text-xl font-semibold">{row.engine}</h2>
|
||||
</div>
|
||||
<dl className="grid gap-3 text-sm">
|
||||
{[
|
||||
["Speed", row.speed],
|
||||
["Hardware", row.hardware],
|
||||
["Accuracy", row.accuracy],
|
||||
["Footprint", row.footprint],
|
||||
["Best for", row.bestFor],
|
||||
].map(([label, value]) => (
|
||||
<div key={label} className="grid gap-1">
|
||||
<dt className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
{label}
|
||||
</dt>
|
||||
<dd className="text-foreground">{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="hidden overflow-x-auto rounded-[12px] border border-border bg-background md:block">
|
||||
<table className="w-full border-collapse text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="px-4 py-3 text-sm font-semibold">Engine</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Speed</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Hardware</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Accuracy</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Footprint</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Best for</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{engineTable.map((row) => {
|
||||
const Icon = row.icon;
|
||||
return (
|
||||
<tr
|
||||
key={row.engine}
|
||||
className="border-b border-border align-top"
|
||||
>
|
||||
<td className="px-4 py-4 font-semibold">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-flex rounded-md p-1.5 ${row.iconBg}`}
|
||||
>
|
||||
<Icon className={`h-4 w-4 ${row.iconColor}`} />
|
||||
</span>
|
||||
{row.engine}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">
|
||||
{row.speed}
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">
|
||||
{row.hardware}
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">
|
||||
{row.accuracy}
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">
|
||||
{row.footprint}
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">
|
||||
{row.bestFor}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section className="mt-8 rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
Switching Between Engines
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You can switch between whisper.cpp, Faster Whisper, Whisper, VOSK,
|
||||
Parakeet, and Remote API from Settings. Speech Model simple setup
|
||||
(v0.17.0) puts language and speed/accuracy first; engine choice lives
|
||||
under Advanced. v0.10.1+ safely stops recognition before switching.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mt-12 grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
When to pick whisper.cpp
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose whisper.cpp when you want the best speed-to-accuracy ratio
|
||||
and broad hardware support. It is the default in Vocalinux for a
|
||||
reason. Safe engine switching - v0.10.1+ stops recognition before
|
||||
switching to prevent crashes.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">When to pick Whisper</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose OpenAI Whisper if your environment already depends on
|
||||
PyTorch/CUDA workflows and you prefer that runtime profile.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">When to pick Faster Whisper</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose Faster Whisper for Whisper-class accuracy on CPU without the
|
||||
PyTorch stack. Optional extra; not bundled in AppImage, Flatpak, or Snap.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">When to pick VOSK</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose VOSK on older laptops, low-RAM systems, or lightweight VMs
|
||||
where small model size and minimal overhead matter most.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">When to pick Parakeet</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose Parakeet for English or European dictation on CPU via
|
||||
sherpa-onnx. It uses a bundled language set (v3-european default)
|
||||
instead of the Whisper catalog picker. Optional extra; not in
|
||||
AppImage, Flatpak, or Snap.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
When to pick Remote API
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose Remote API when a trusted server has stronger hardware,
|
||||
larger models, or a shared Whisper backend. Use local engines when
|
||||
your voice data must stay entirely on-device.
|
||||
</p>
|
||||
<Link
|
||||
href="/remote-api/"
|
||||
className="mt-4 inline-flex items-center gap-1 text-sm font-semibold text-primary hover:underline"
|
||||
>
|
||||
Remote setup <ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Next steps</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li className="flex gap-2">
|
||||
<CheckCircle2 className="mt-1 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
Install by distro:
|
||||
<Link
|
||||
href="/install/ubuntu/"
|
||||
className="font-semibold text-primary hover:underline"
|
||||
>
|
||||
Ubuntu
|
||||
</Link>
|
||||
,
|
||||
<Link
|
||||
href="/install/fedora/"
|
||||
className="font-semibold text-primary hover:underline"
|
||||
>
|
||||
Fedora
|
||||
</Link>
|
||||
,
|
||||
<Link
|
||||
href="/install/arch/"
|
||||
className="font-semibold text-primary hover:underline"
|
||||
>
|
||||
Arch Linux
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex gap-2">
|
||||
<CheckCircle2 className="mt-1 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
Use interactive install to detect your hardware and pick the best
|
||||
engine defaults.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex gap-2">
|
||||
<CheckCircle2 className="mt-1 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
After install, tune model size, VAD sensitivity, or Remote API
|
||||
settings for your preferred latency and accuracy level.
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -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" />
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Custom terms</h2>
|
||||
<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"
|
||||
/>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Transcript corrections</h2>
|
||||
<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" />
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Post-processing script</h2>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Keyboard,
|
||||
Languages,
|
||||
Monitor,
|
||||
Power,
|
||||
RefreshCw,
|
||||
ShieldCheck,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const reliabilityFeatures = [
|
||||
{
|
||||
title: "Suspend and resume recovery",
|
||||
description:
|
||||
"Vocalinux restarts recognition and shortcut backends after wake so laptop sleep cycles do not require a manual app restart.",
|
||||
icon: Power,
|
||||
},
|
||||
{
|
||||
title: "Safe engine switching",
|
||||
description:
|
||||
"Recognition stops before engine changes, preventing crashes while moving between whisper.cpp, Whisper, VOSK, or Remote API.",
|
||||
icon: RefreshCw,
|
||||
},
|
||||
{
|
||||
title: "Keyboard layout preservation",
|
||||
description:
|
||||
"IBus activation preserves the current XKB layout so dictation does not unexpectedly switch to US keyboard behavior.",
|
||||
icon: Keyboard,
|
||||
},
|
||||
{
|
||||
title: "IBus runtime recovery",
|
||||
description:
|
||||
"Readiness probes, scoped activation, and runtime recovery improve text injection without restarting the whole app.",
|
||||
icon: Monitor,
|
||||
},
|
||||
{
|
||||
title: "Non-ASCII fallback",
|
||||
description:
|
||||
"ydotool can fall back to clipboard paste for accented and non-ASCII characters on paths that cannot type them directly.",
|
||||
icon: Languages,
|
||||
},
|
||||
{
|
||||
title: "Tray and settings polish",
|
||||
description:
|
||||
"Bundled resources, searchable sidebar settings, and an explicit close button improve behavior across desktop environments.",
|
||||
icon: Wrench,
|
||||
},
|
||||
];
|
||||
|
||||
const releaseMap = [
|
||||
{
|
||||
version: "v0.18.0",
|
||||
highlights:
|
||||
"Dictation Pad for Wayland-safe dictation, evdev hotkey grab so the shortcut stops leaking into the focused app, PipeWire capture with system-audio sources, RemoteDesktop portal injection, per-language shortcuts and layout-follow, tray dictation history, TinyDiarize file transcription, opt-in D-Bus activation, custom dictionary with corrections, postprocessing hook, floating overlay, audio ducking, GNOME Wayland layout-switching fix, Vosk OOM guard, cancellable model downloads, AUR -bin AppImage package, self-hosted Flatpak remote, and snap hardware-observe.",
|
||||
},
|
||||
{
|
||||
version: "v0.17.0",
|
||||
highlights:
|
||||
"Faster Whisper and Parakeet engines, Speech Model simple setup, first-run system language, Snap ydotool/uinput, workflow Flatpak bundles, XWayland clipboard paste, xprop WM_CLASS instead of crashing xdotool, HDA analog mics at native layout, native GTK OS dark/light, English-only models no longer trap other languages, and PTT tray/audio cue fixes.",
|
||||
},
|
||||
{
|
||||
version: "v0.16.2",
|
||||
highlights:
|
||||
"Skip leftover IBus on KDE so dictation types, Wayland and IBus shortcuts via wtype/ydotool, real BackSpace for delete that, Fedora/Arch glslc installer fix, nightly version stamp, and release integrity pins.",
|
||||
},
|
||||
{
|
||||
version: "v0.16.1",
|
||||
highlights:
|
||||
"Per-engine model at startup, leftover tray idle after toggle stop, Ctrl+Shift+V paste in terminals, GNOME XWayland layout after scoped IBus inject, Python 3.11 installer floor with verified downloads, and AppImage glibc that boots on Debian 12.",
|
||||
},
|
||||
{
|
||||
version: "v0.16.0",
|
||||
highlights:
|
||||
"Update checker with tray notifications, Right Alt PTT default for new installs, searchable language list, unused model deletion, Test Dictation missing-model message, IBus/XKB restore fixes, safer audio capture, clipboard restore, installer python3-gi requirement, and AppImage/GPU packaging hardening.",
|
||||
},
|
||||
{
|
||||
version: "v0.15.0",
|
||||
highlights:
|
||||
"Searchable settings with sidebar dictation footer, ibus-wayland on previously unbridged compositors, safer IBus teardown, Bluetooth mic capture crash fix, custom-shortcut Record/Set restore, and xdotool focus preserve.",
|
||||
},
|
||||
{
|
||||
version: "v0.12.0-beta",
|
||||
highlights:
|
||||
"Thread safety hardening for Remote API, IBus, and text injection plus better settings behavior.",
|
||||
},
|
||||
{
|
||||
version: "v0.11.0-beta",
|
||||
highlights:
|
||||
"IBus readiness probes, runtime recovery, final speech preservation, installer hardening, and distro compatibility fixes.",
|
||||
},
|
||||
{
|
||||
version: "v0.10.2-beta",
|
||||
highlights:
|
||||
"Non-ASCII ydotool fallback, IBus detection on Wayland, startup fixes, and Pop!_OS/Ubuntu dependency coverage.",
|
||||
},
|
||||
{
|
||||
version: "v0.10.1-beta",
|
||||
highlights:
|
||||
"Suspend/resume recovery, safe engine switching, keyboard layout preservation, tray resources, and push-to-talk reliability.",
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Linux Desktop Dictation Reliability - IBus, Wayland, Suspend",
|
||||
description:
|
||||
"See how Vocalinux handles Linux desktop reliability across IBus, Wayland, suspend/resume recovery, keyboard layouts, tray resources, and non-ASCII text injection.",
|
||||
path: "/desktop-reliability",
|
||||
keywords: [
|
||||
"linux dictation reliability",
|
||||
"ibus voice dictation wayland",
|
||||
"vocalinux suspend resume",
|
||||
"wayland text injection reliability",
|
||||
],
|
||||
});
|
||||
|
||||
export default function DesktopReliabilityPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "TechArticle",
|
||||
headline: "Linux Desktop Dictation Reliability",
|
||||
description:
|
||||
"Reliability improvements in Vocalinux for IBus, Wayland, suspend/resume, keyboard layout preservation, and text injection.",
|
||||
dateModified: "2026-10-02",
|
||||
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("/desktop-reliability"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="border-primary/30 bg-primary/10 mb-4 inline-flex items-center gap-2 rounded-full border px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
Desktop Reliability
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Reliable Voice Dictation on Real Linux Desktops
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Recent Vocalinux beta releases focused heavily on the hard parts of
|
||||
Linux desktop dictation: Wayland text injection, IBus lifecycle
|
||||
behavior, suspend/resume recovery, keyboard layouts, non-ASCII text,
|
||||
and settings compatibility.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{reliabilityFeatures.map((feature) => {
|
||||
const Icon = feature.icon;
|
||||
return (
|
||||
<article
|
||||
key={feature.title}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<span className="bg-primary/10 mb-4 inline-flex rounded-[12px] p-3">
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</span>
|
||||
<h2 className="mb-2 text-xl font-semibold">{feature.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{feature.description}
|
||||
</p>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-5 font-display text-2xl font-semibold">Reliability Work by Release</h2>
|
||||
<div className="space-y-4">
|
||||
{releaseMap.map((release) => (
|
||||
<article
|
||||
key={release.version}
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<h3 className="mb-2 text-lg font-semibold">{release.version}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{release.highlights}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
</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>
|
||||
<ul className="space-y-3 text-sm text-muted-foreground">
|
||||
{[
|
||||
"Dictation should survive laptop lid closes, docking changes, and USB re-enumeration.",
|
||||
"Text should appear in the focused app without breaking existing IBus engines or keyboard layouts.",
|
||||
"Accented characters should be handled even when a low-level injection backend cannot type them directly.",
|
||||
"Settings should fit smaller displays and window managers with different decoration behavior.",
|
||||
].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" />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border-primary/20 bg-primary/5 rounded-[12px] border p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Where to Debug Issues</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
If dictation works in one app but not another, start with the
|
||||
display server and input method path. Wayland sessions often depend
|
||||
on IBus setup, while X11 paths typically use xdotool or compatible
|
||||
fallbacks.
|
||||
</p>
|
||||
<Link
|
||||
href="/troubleshooting/"
|
||||
className="inline-flex items-center gap-1 text-sm font-semibold text-primary hover:underline"
|
||||
>
|
||||
Open troubleshooting guide <ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-primary/20 bg-primary/5 rounded-[12px] border p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Related Guides</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Link href="/wayland/" className="group">
|
||||
<h3 className="font-semibold">Wayland support</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Understand IBus, wtype, ydotool, and compositor-specific setup.
|
||||
</p>
|
||||
<span className="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
Wayland guide <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
<Link href="/shortcuts/" className="group">
|
||||
<h3 className="font-semibold">Shortcut reliability</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Configure toggle, push-to-talk, and left/right modifier behavior.
|
||||
</p>
|
||||
<span className="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
Shortcut guide <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
<Link href="/changelog/" className="group">
|
||||
<h3 className="font-semibold">Release history</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Review each beta release and the reliability fixes it included.
|
||||
</p>
|
||||
<span className="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
Changelog <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -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" />
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Who it is for</h2>
|
||||
<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" />
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">How text gets out</h2>
|
||||
<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" />
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">What stays the same</h2>
|
||||
<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,255 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
BookOpen,
|
||||
ChevronRight,
|
||||
CircleHelp,
|
||||
Cpu,
|
||||
Laptop,
|
||||
Lock,
|
||||
Mic,
|
||||
Volume2,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const faqCategories = [
|
||||
{
|
||||
category: "Getting Started",
|
||||
icon: Zap,
|
||||
iconColor: "text-primary",
|
||||
questions: [
|
||||
{
|
||||
q: "How do I install Vocalinux?",
|
||||
a: "Run the one-liner installer: curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh | bash. The installer handles dependencies and configuration. You can also download an AppImage from GitHub Releases if you prefer a no-root portable binary (host xdotool/wtype/ydotool still required for text injection), or grab a .deb, .rpm, Snap, or Flatpak package for your distro.",
|
||||
},
|
||||
{
|
||||
q: "Which Linux distributions are supported?",
|
||||
a: "Vocalinux works on Ubuntu 24.04+, Debian 12+, Fedora 39+, Arch Linux, Manjaro, and most other Linux distributions. See our install guides for distro-specific instructions.",
|
||||
},
|
||||
{
|
||||
q: "What are the system requirements?",
|
||||
a: "You need Python 3.11+, a working microphone, and about 500MB of disk space. For GPU acceleration with whisper.cpp, you need Vulkan support (AMD, Intel, or NVIDIA).",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Privacy & Security",
|
||||
icon: Lock,
|
||||
iconColor: "text-primary",
|
||||
questions: [
|
||||
{
|
||||
q: "Does Vocalinux work offline?",
|
||||
a: "Local engines (whisper.cpp, Whisper, and VOSK) run on your Linux machine. Remote API is optional and only talks to a server you configure.",
|
||||
},
|
||||
{
|
||||
q: "Does Vocalinux send any data to external servers?",
|
||||
a: "Local engines do not send voice data externally. Remote API mode sends audio only to the transcription server URL you configure.",
|
||||
},
|
||||
{
|
||||
q: "Is my voice data stored anywhere?",
|
||||
a: "Only temporarily during transcription. Audio is processed in real-time and discarded immediately. No voice recordings are saved to disk.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Speech Recognition",
|
||||
icon: Mic,
|
||||
iconColor: "text-primary",
|
||||
questions: [
|
||||
{
|
||||
q: "Which speech recognition engines are available?",
|
||||
a: "Vocalinux supports whisper.cpp (default), OpenAI Whisper, VOSK, and Remote API for compatible self-hosted or trusted transcription servers.",
|
||||
},
|
||||
{
|
||||
q: "How accurate is the transcription?",
|
||||
a: "Accuracy depends on the engine and model size. whisper.cpp with medium or large models provides excellent accuracy for English and 99+ other languages.",
|
||||
},
|
||||
{
|
||||
q: "Can I use Vocalinux in languages other than English?",
|
||||
a: "Yes! whisper.cpp and Whisper support 99+ languages with automatic language detection. VOSK supports several languages with separate models.",
|
||||
},
|
||||
{
|
||||
q: "What is Silero VAD?",
|
||||
a: "Silero VAD is neural voice activity detection. Vocalinux uses it when ONNX Runtime support is available to drop silence-only buffers before recognition.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Usage & Features",
|
||||
icon: Volume2,
|
||||
iconColor: "text-primary",
|
||||
questions: [
|
||||
{
|
||||
q: "How do I start and stop dictation?",
|
||||
a: "By default, hold Right Alt (Option on Mac-layout keyboards) to speak and release to stop (push-to-talk). You can switch to toggle mode (double-tap a key to start/stop) or change the key in Settings.",
|
||||
},
|
||||
{
|
||||
q: "Does Vocalinux work with Wayland?",
|
||||
a: "Yes. Vocalinux supports both X11 and Wayland, including IBus-based flows on Wayland with improved active-method detection for more reliable text injection.",
|
||||
},
|
||||
{
|
||||
q: "What happens when I close my laptop lid?",
|
||||
a: "Vocalinux v0.10.1+ automatically recovers speech recognition after system suspend/resume. The app detects when your system wakes up and restarts the recognition engine and keyboard shortcuts. No manual restart needed - just start dictating as usual.",
|
||||
},
|
||||
{
|
||||
q: "Does Vocalinux preserve my keyboard layout?",
|
||||
a: "Yes! v0.10.1+ preserves your XKB keyboard layout when activating IBus for text injection. Unlike other tools that switch to US layout, Vocalinux keeps your configured layout so you can dictate without unexpected character changes.",
|
||||
},
|
||||
{
|
||||
q: "Can I use voice commands?",
|
||||
a: "Yes. Vocalinux supports commands like 'new line', 'delete that', 'select all', and more. Voice commands are optional and can be enabled or disabled in Settings.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Performance",
|
||||
icon: Cpu,
|
||||
iconColor: "text-primary",
|
||||
questions: [
|
||||
{
|
||||
q: "How much RAM does Vocalinux need?",
|
||||
a: "Minimum 4GB RAM for basic use. For larger models, 8GB+ recommended. VOSK is the most memory-efficient option for systems with limited RAM.",
|
||||
},
|
||||
{
|
||||
q: "Can I use GPU acceleration?",
|
||||
a: "Yes. whisper.cpp supports GPU acceleration via Vulkan on AMD, Intel, and NVIDIA. On multi-GPU machines Vocalinux prefers a discrete device when possible; you can pick a device under Advanced settings.",
|
||||
},
|
||||
{
|
||||
q: "What model size should I use?",
|
||||
a: "Start with 'tiny' for fastest performance, 'base' or 'small' for good balance, or 'medium'/'large' for best accuracy. Larger models need more RAM.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Linux Dictation FAQ: Offline, Wayland, GPU and Privacy",
|
||||
description:
|
||||
"Does Vocalinux work offline or on Wayland? How much RAM does it need, and which engines and languages does it support? Answers on install, privacy and GPU use.",
|
||||
path: "/faq",
|
||||
keywords: [
|
||||
"vocalinux faq",
|
||||
"linux voice dictation help",
|
||||
"speech to text linux questions",
|
||||
"offline dictation faq",
|
||||
],
|
||||
});
|
||||
|
||||
export default function FaqPage() {
|
||||
const faqJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
dateModified: "2026-06-11",
|
||||
mainEntity: faqCategories.flatMap((cat) =>
|
||||
cat.questions.map((q) => ({
|
||||
"@type": "Question",
|
||||
name: q.q,
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: q.a,
|
||||
},
|
||||
})),
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="border-primary/30 bg-primary/10 mb-4 inline-flex items-center gap-2 rounded-full border px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<CircleHelp className="h-4 w-4" />
|
||||
Help Center
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Frequently Asked Questions
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Find answers to common questions about Vocalinux. From installation to
|
||||
advanced features, we've got you covered.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-8">
|
||||
{faqCategories.map((category) => {
|
||||
const Icon = category.icon;
|
||||
return (
|
||||
<article
|
||||
key={category.category}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<h2 className="mb-6 flex items-center gap-3 font-display text-2xl font-semibold">
|
||||
<span className={`rounded-[12px] bg-muted p-2 bg-muted`}>
|
||||
<Icon className={`h-5 w-5 ${category.iconColor}`} />
|
||||
</span>
|
||||
{category.category}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
{category.questions.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border-b border-border pb-6 last:border-0 last:pb-0"
|
||||
>
|
||||
<h3 className="mb-2 text-lg font-semibold">{item.q}</h3>
|
||||
<p className="text-muted-foreground">{item.a}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Still have questions?</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Laptop className="h-4 w-4 text-primary" />
|
||||
Check the{" "}
|
||||
<Link
|
||||
href="/install/"
|
||||
className="font-semibold text-primary hover:underline"
|
||||
>
|
||||
install guide
|
||||
</Link>{" "}
|
||||
for setup help
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<BookOpen className="h-4 w-4 text-primary" />
|
||||
See the{" "}
|
||||
<Link
|
||||
href="/troubleshooting/"
|
||||
className="font-semibold text-primary hover:underline"
|
||||
>
|
||||
troubleshooting guide
|
||||
</Link>{" "}
|
||||
for common issues
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<ChevronRight className="h-4 w-4 text-primary" />
|
||||
<a
|
||||
href="https://github.com/VocaHQ/vocalinux/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold text-primary hover:underline"
|
||||
>
|
||||
Open an issue on GitHub
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</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,300 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
Braces,
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Code2,
|
||||
FileCode2,
|
||||
FileText,
|
||||
MessageSquare,
|
||||
Terminal,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const useCases = [
|
||||
{
|
||||
title: "Code Comments & Documentation",
|
||||
description:
|
||||
"Dictate inline comments, JSDoc, docstrings, and README sections without leaving your editor.",
|
||||
example: "This function validates user input and returns true if valid",
|
||||
icon: FileCode2,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Git Commit Messages",
|
||||
description:
|
||||
"Speak your commit messages naturally - no more brief, unhelpful commits because typing is slow.",
|
||||
example: "fix authentication timeout when server is under heavy load",
|
||||
icon: Terminal,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Pull Request Descriptions",
|
||||
description:
|
||||
"Write detailed PR descriptions explaining your changes, rationale, and testing approach.",
|
||||
example: "This PR refactors the user service to improve testability and adds error handling",
|
||||
icon: FileText,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Technical Documentation",
|
||||
description:
|
||||
"Create API docs, architecture guides, and technical specifications at speaking speed.",
|
||||
example: "The API accepts a JSON payload with the following fields",
|
||||
icon: FileText,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Code Review Comments",
|
||||
description:
|
||||
"Write thorough code review feedback without the fatigue of typing detailed explanations.",
|
||||
example: "Consider extracting this logic into a separate function for reusability",
|
||||
icon: MessageSquare,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Pair Programming Notes",
|
||||
description:
|
||||
"Dictate notes while pairing - keep your hands on the keyboard for code, use voice for meta.",
|
||||
example: "Remember to add unit tests for the edge case we just discussed",
|
||||
icon: Zap,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
];
|
||||
|
||||
const voiceCommands = [
|
||||
{ command: "new line", action: "Insert line break" },
|
||||
{ command: "tab", action: "Insert tab character" },
|
||||
{ command: "open parenthesis", action: "Type (" },
|
||||
{ command: "close parenthesis", action: "Type )" },
|
||||
{ command: "open bracket", action: "Type [" },
|
||||
{ command: "close bracket", action: "Type ]" },
|
||||
{ command: "open brace", action: "Type {" },
|
||||
{ command: "close brace", action: "Type }" },
|
||||
{ command: "equals", action: "Type =" },
|
||||
{ command: "semicolon", action: "Type ;" },
|
||||
{ command: "colon", action: "Type :" },
|
||||
{ command: "quote", action: "Type \"" },
|
||||
{ command: "single quote", action: "Type '" },
|
||||
{ command: "underscore", action: "Type _" },
|
||||
{ command: "slash", action: "Type /" },
|
||||
{ command: "backslash", action: "Type \\" },
|
||||
{ command: "period / dot", action: "Type ." },
|
||||
{ command: "comma", action: "Type ," },
|
||||
];
|
||||
|
||||
const benefits = [
|
||||
{
|
||||
title: "Reduce RSI Risk",
|
||||
description: "Developers type 8+ hours daily. Voice dictation lets you rest your hands while staying productive.",
|
||||
},
|
||||
{
|
||||
title: "Maintain Flow State",
|
||||
description: "Speak your thoughts as they come - no context switching to the keyboard for comments and docs.",
|
||||
},
|
||||
{
|
||||
title: "Better Documentation",
|
||||
description: "When documentation is easy to write, you write more of it. Thorough docs become the default.",
|
||||
},
|
||||
{
|
||||
title: "Universal Compatibility",
|
||||
description: "Works in VS Code, JetBrains IDEs, Vim, terminal, browser - every Linux application.",
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Voice Dictation for Developers & Coders | Vocalinux",
|
||||
description:
|
||||
"Dictate code comments, documentation, and commit messages hands-free. Voice dictation for Linux developers - works in VS Code, JetBrains, terminal, and all apps.",
|
||||
path: "/for-developers",
|
||||
keywords: [
|
||||
"voice dictation for developers",
|
||||
"voice coding Linux",
|
||||
"dictate code comments",
|
||||
"hands-free programming",
|
||||
"developer productivity voice",
|
||||
"code documentation voice",
|
||||
"Linux voice programming",
|
||||
"VS Code voice dictation",
|
||||
],
|
||||
});
|
||||
|
||||
export default function ForDevelopersPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Voice Dictation for Developers & Coders",
|
||||
description:
|
||||
"How Linux developers use Vocalinux for code comments, documentation, and commit messages.",
|
||||
dateModified: "2026-02-22",
|
||||
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("/for-developers"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="subpage-kicker">
|
||||
<Code2 className="h-4 w-4" />
|
||||
For Developers
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Voice Dictation for Developers
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Write code comments, documentation, commit messages, and more - hands-free. Keep your hands
|
||||
on the keyboard for code, use your voice for everything else.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{benefits.map((benefit) => (
|
||||
<div
|
||||
key={benefit.title}
|
||||
className="rounded-[12px] border border-border bg-background p-5 text-center"
|
||||
>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">{benefit.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">{benefit.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Developer Use Cases</h2>
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{useCases.map((useCase) => {
|
||||
const Icon = useCase.icon;
|
||||
return (
|
||||
<article
|
||||
key={useCase.title}
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-3">
|
||||
<div className={`rounded-lg p-2 ${useCase.iconBg}`}>
|
||||
<Icon className={`h-4 w-4 ${useCase.iconColor}`} />
|
||||
</div>
|
||||
<h3 className="font-semibold">{useCase.title}</h3>
|
||||
</div>
|
||||
<p className="mb-3 text-sm text-muted-foreground">{useCase.description}</p>
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-3">
|
||||
<code className="text-xs text-[color:var(--terminal-fg)]">"{useCase.example}"</code>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</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">Helpful Voice Commands for Coding</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Use these voice commands for punctuation and symbols commonly used in code:
|
||||
</p>
|
||||
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{voiceCommands.map((cmd) => (
|
||||
<div
|
||||
key={cmd.command}
|
||||
className="flex items-center justify-between rounded-lg border border-border bg-muted px-3 py-2"
|
||||
>
|
||||
<kbd className="rounded bg-muted px-2 py-0.5 text-xs font-mono bg-muted">
|
||||
"{cmd.command}"
|
||||
</kbd>
|
||||
<span className="text-xs text-muted-foreground">{cmd.action}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
See the <Link href="/shortcuts/" className="text-primary hover:underline">full voice commands reference</Link> for more.
|
||||
</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 Voice Coding - Voice Documentation
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Vocalinux is designed for <strong>dictating text</strong>, not writing code syntax. For
|
||||
actual code, your keyboard is still faster. But for comments, docs, messages, and
|
||||
notes - voice is a game-changer. Use both together for maximum productivity.
|
||||
</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">Works in Every IDE</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
{[
|
||||
"VS Code",
|
||||
"JetBrains (IntelliJ, PyCharm, WebStorm)",
|
||||
"Vim / Neovim",
|
||||
"Emacs",
|
||||
"Sublime Text",
|
||||
"Terminal / Shell",
|
||||
"GitHub Web",
|
||||
"GitLab",
|
||||
"Jupyter Notebooks",
|
||||
].map((ide) => (
|
||||
<div
|
||||
key={ide}
|
||||
className="flex items-center gap-2 rounded-lg border border-border bg-muted px-3 py-2"
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm">{ide}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
Vocalinux works system-wide. If the app accepts text input, Vocalinux can dictate into it.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Start Dictating Your Docs</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Install Vocalinux and start writing better documentation with less effort. Your future
|
||||
self (and your team) will thank you.
|
||||
</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 Now
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/rsi-prevention/"
|
||||
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 hover:bg-muted"
|
||||
>
|
||||
RSI Prevention
|
||||
</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,259 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Keyboard,
|
||||
Monitor,
|
||||
Settings2,
|
||||
Waypoints,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const comparisonRows = [
|
||||
{
|
||||
feature: "Default display stack",
|
||||
gnome: "Wayland by default on most modern GNOME distributions",
|
||||
kde: "Wayland or X11, with X11 still common in some setups",
|
||||
icon: Monitor,
|
||||
},
|
||||
{
|
||||
feature: "Text injection approach",
|
||||
gnome: "IBus-first for native Wayland dictation",
|
||||
kde: "IBus-first on Wayland, with xdotool fallback on X11",
|
||||
icon: Keyboard,
|
||||
},
|
||||
{
|
||||
feature: "Input method setup",
|
||||
gnome: "Typically straightforward with existing IBus defaults",
|
||||
kde: "May need explicit IBus selection in System Settings",
|
||||
icon: Settings2,
|
||||
},
|
||||
{
|
||||
feature: "Tray indicator behavior",
|
||||
gnome: "Usually needs AppIndicator extension for tray visibility",
|
||||
kde: "Native system tray support out of the box",
|
||||
icon: Waypoints,
|
||||
},
|
||||
{
|
||||
feature: "Troubleshooting profile",
|
||||
gnome: "Extension + IBus engine activation checks",
|
||||
kde: "Session type checks (X11 vs Wayland) + input method routing",
|
||||
icon: Wrench,
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Voice Typing on GNOME vs KDE Plasma: Linux Desktop Comparison",
|
||||
description:
|
||||
"Compare voice dictation on GNOME vs KDE Plasma. How desktop environment affects text injection, IBus setup, and Wayland compatibility for Linux voice typing.",
|
||||
path: "/gnome-kde",
|
||||
keywords: [
|
||||
"voice typing GNOME linux",
|
||||
"voice dictation KDE plasma",
|
||||
"linux desktop voice recognition comparison",
|
||||
"GNOME vs KDE voice input",
|
||||
],
|
||||
});
|
||||
|
||||
export default function GnomeVsKdePage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Voice Typing on GNOME vs KDE Plasma: Linux Desktop Comparison",
|
||||
description:
|
||||
"Compare voice dictation on GNOME vs KDE Plasma. How desktop environment affects text injection, IBus setup, and Wayland compatibility for Linux voice typing.",
|
||||
dateModified: "2026-03-30",
|
||||
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("/gnome-kde"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="subpage-kicker">
|
||||
<Monitor className="h-4 w-4" />
|
||||
Linux Desktop Comparison
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Voice Typing on GNOME vs KDE Plasma: Linux Desktop Comparison
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
GNOME and KDE Plasma both support excellent Linux voice dictation, but the desktop environment
|
||||
changes how text injection behaves, how IBus is configured, and what setup steps you need for a
|
||||
smooth daily workflow.
|
||||
</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">Why desktop environment matters for voice dictation</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Voice typing quality is not just about the speech engine. Your desktop environment determines
|
||||
session type defaults, tray behavior, and input method routing. That affects whether dictation
|
||||
appears reliably in every app, especially under Wayland.
|
||||
</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">GNOME: what to expect</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<strong>Wayland is the default</strong> on modern GNOME sessions, so IBus-backed text
|
||||
injection is usually the primary path.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<strong>IBus integration is central</strong> for reliable cross-app dictation behavior on
|
||||
Wayland.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<strong>Tray icon often needs AppIndicator support</strong> via GNOME extension, because
|
||||
legacy tray behavior differs from KDE.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<strong>Known issues and workarounds:</strong> if the tray icon is missing, install/enable
|
||||
AppIndicator extension; if text does not appear in apps, verify IBus daemon/engine state.
|
||||
</span>
|
||||
</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">KDE Plasma: what to expect</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<strong>X11 vs Wayland matters</strong>: Plasma supports both widely, so behavior can differ
|
||||
between sessions.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<strong>IBus setup may be more explicit</strong> compared with GNOME defaults, depending on
|
||||
distro and prior input method configuration.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<strong>System Settings integration is strong</strong>, making it easier to inspect session
|
||||
type, input method behavior, and tray settings from one place.
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 overflow-x-auto rounded-[12px] border border-border bg-background">
|
||||
<h2 className="px-4 pb-2 pt-5 font-display text-2xl font-semibold">GNOME vs KDE Plasma comparison</h2>
|
||||
<table className="w-full border-collapse text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="px-4 py-3 text-sm font-semibold">Feature</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">GNOME</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">KDE Plasma</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{comparisonRows.map((row) => {
|
||||
const Icon = row.icon;
|
||||
return (
|
||||
<tr key={row.feature} className="border-b border-border align-top">
|
||||
<td className="px-4 py-4 font-semibold">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="rounded-md bg-primary/10 p-1.5">
|
||||
<Icon className="h-4 w-4 text-primary" />
|
||||
</span>
|
||||
{row.feature}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.gnome}</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.kde}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 md:grid-cols-2">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">When GNOME is a better fit</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose GNOME when you want a Wayland-first environment with familiar IBus defaults and a
|
||||
minimal, focused workflow. Add AppIndicator extension once and day-to-day dictation is
|
||||
typically very consistent.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">When KDE Plasma is a better fit</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose KDE Plasma when you want deeper system configurability, flexible X11/Wayland session
|
||||
choices, and richer control over desktop behaviors while tuning a dictation setup.
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Helpful guides</h2>
|
||||
<div className="grid gap-3 text-sm text-muted-foreground sm:grid-cols-3">
|
||||
<Link href="/wayland/" className="font-semibold text-primary hover:underline">
|
||||
Wayland setup and architecture
|
||||
</Link>
|
||||
<Link href="/troubleshooting/" className="font-semibold text-primary hover:underline">
|
||||
Troubleshooting checklist
|
||||
</Link>
|
||||
<Link href="/install/" className="font-semibold text-primary hover:underline">
|
||||
Install guide by distro
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[12px] border border-primary/20 bg-primary/5 p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Ready to set up voice typing on your desktop?</h2>
|
||||
<p className="mb-6 max-w-3xl text-muted-foreground">
|
||||
Install Vocalinux and get reliable offline voice dictation on GNOME or KDE Plasma, with the
|
||||
right text injection path for your session type.
|
||||
</p>
|
||||
<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" />
|
||||
</Link>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Cpu,
|
||||
Gauge,
|
||||
MemoryStick,
|
||||
Monitor,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const gpuBackends = [
|
||||
{
|
||||
name: "Vulkan",
|
||||
supportedGPUs: "AMD, Intel, NVIDIA",
|
||||
description:
|
||||
"Cross-platform GPU API supported by all major GPU vendors. The default and most compatible backend for whisper.cpp.",
|
||||
advantages: [
|
||||
"Works with AMD, Intel, and NVIDIA GPUs",
|
||||
"No vendor-specific drivers required",
|
||||
"Excellent Linux support",
|
||||
"Best option for AMD and Intel GPUs",
|
||||
],
|
||||
icon: Monitor,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
name: "CUDA",
|
||||
supportedGPUs: "NVIDIA only",
|
||||
description:
|
||||
"NVIDIA's proprietary GPU compute platform. High performance but requires NVIDIA GPU and CUDA toolkit.",
|
||||
advantages: [
|
||||
"Excellent performance on NVIDIA hardware",
|
||||
"Mature ecosystem and tooling",
|
||||
"Works with PyTorch-based Whisper engine",
|
||||
],
|
||||
icon: Cpu,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
recommended: false,
|
||||
},
|
||||
{
|
||||
name: "CPU",
|
||||
supportedGPUs: "All systems",
|
||||
description:
|
||||
"Pure CPU processing. Slower but works on any system. Automatic fallback when GPU is unavailable.",
|
||||
advantages: [
|
||||
"Works on any hardware",
|
||||
"No GPU requirements",
|
||||
"Predictable performance",
|
||||
"Lowest power consumption option",
|
||||
],
|
||||
icon: MemoryStick,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
recommended: false,
|
||||
},
|
||||
];
|
||||
|
||||
const modelPerformance = [
|
||||
{
|
||||
model: "tiny",
|
||||
size: "~74MB",
|
||||
cpuSpeed: "~0.5s",
|
||||
gpuSpeed: "~0.2s",
|
||||
accuracy: "Good for real-time",
|
||||
recommended: "Real-time dictation, older hardware",
|
||||
},
|
||||
{
|
||||
model: "base",
|
||||
size: "~141MB",
|
||||
cpuSpeed: "~0.8s",
|
||||
gpuSpeed: "~0.3s",
|
||||
accuracy: "Better accuracy",
|
||||
recommended: "Balanced speed/accuracy",
|
||||
},
|
||||
{
|
||||
model: "small",
|
||||
size: "~465MB",
|
||||
cpuSpeed: "~1.5s",
|
||||
gpuSpeed: "~0.5s",
|
||||
accuracy: "High accuracy",
|
||||
recommended: "Most users, good balance",
|
||||
},
|
||||
{
|
||||
model: "medium",
|
||||
size: "~1.5GB",
|
||||
cpuSpeed: "~4s",
|
||||
gpuSpeed: "~1.2s",
|
||||
accuracy: "Very high accuracy",
|
||||
recommended: "Accuracy-focused use, with GPU",
|
||||
},
|
||||
{
|
||||
model: "large",
|
||||
size: "~3.0GB",
|
||||
cpuSpeed: "~8s",
|
||||
gpuSpeed: "~2.5s",
|
||||
accuracy: "Best accuracy",
|
||||
recommended: "Maximum accuracy, requires GPU",
|
||||
},
|
||||
];
|
||||
|
||||
const hardwareRequirements = [
|
||||
{
|
||||
category: "Minimum (CPU-only)",
|
||||
ram: "4GB RAM",
|
||||
storage: "200MB",
|
||||
gpu: "None required",
|
||||
performance: "Functional but slower processing",
|
||||
},
|
||||
{
|
||||
category: "Recommended (with GPU)",
|
||||
ram: "8GB RAM",
|
||||
storage: "500MB",
|
||||
gpu: "Any Vulkan-compatible GPU",
|
||||
performance: "Real-time dictation with small/base models",
|
||||
},
|
||||
{
|
||||
category: "Optimal",
|
||||
ram: "16GB+ RAM",
|
||||
storage: "2GB+",
|
||||
gpu: "NVIDIA RTX or AMD RX series",
|
||||
performance: "Real-time with any model including large",
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Vulkan GPU Speech to Text on Linux: AMD, Intel, NVIDIA",
|
||||
description:
|
||||
"Run whisper.cpp speech recognition on your GPU with Vulkan on Linux: AMD, Intel and NVIDIA. CPU vs GPU speed, model sizes, and automatic CPU fallback.",
|
||||
path: "/gpu-acceleration",
|
||||
keywords: [
|
||||
"GPU voice dictation",
|
||||
"Vulkan speech recognition",
|
||||
"AMD GPU dictation",
|
||||
"NVIDIA CUDA voice typing",
|
||||
"Intel GPU speech to text",
|
||||
"hardware accelerated dictation",
|
||||
"Linux GPU speech recognition",
|
||||
"whisper.cpp GPU",
|
||||
],
|
||||
});
|
||||
|
||||
export default function GpuAccelerationPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "GPU Accelerated Voice Dictation for Linux",
|
||||
description:
|
||||
"Complete guide to GPU acceleration in Vocalinux. Vulkan support for AMD, Intel, and NVIDIA GPUs.",
|
||||
dateModified: "2026-02-22",
|
||||
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("/gpu-acceleration"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="subpage-kicker">
|
||||
<Zap className="h-4 w-4" />
|
||||
Performance
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
GPU Accelerated Voice Dictation
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Leverage your GPU for faster speech recognition. Vocalinux supports AMD, Intel, and NVIDIA
|
||||
GPUs via Vulkan - with automatic fallback to CPU when needed.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-primary/20 bg-primary/5 p-6">
|
||||
<h2 className="mb-4 text-xl font-semibold">Quick Check: Do You Have GPU Support?</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Run this command to check if your system supports Vulkan GPU acceleration:
|
||||
</p>
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-4">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">vulkaninfo --summary | grep deviceName</code>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
If you see your GPU name, you're ready for accelerated dictation!
|
||||
On multi-GPU machines Vocalinux prefers a discrete Vulkan device when
|
||||
one is present; pick a specific device under Advanced settings if you
|
||||
need to override auto-select.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Supported GPU Backends</h2>
|
||||
<div className="space-y-6">
|
||||
{gpuBackends.map((backend) => {
|
||||
const Icon = backend.icon;
|
||||
return (
|
||||
<article
|
||||
key={backend.name}
|
||||
className={`rounded-[12px] border p-6 ${
|
||||
backend.recommended
|
||||
? "border-primary/50 bg-primary/5"
|
||||
: "border-border bg-background "
|
||||
}`}
|
||||
>
|
||||
<div className="mb-4 flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`rounded-lg p-2.5 ${backend.iconBg}`}>
|
||||
<Icon className={`h-5 w-5 ${backend.iconColor}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">{backend.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">{backend.supportedGPUs}</p>
|
||||
</div>
|
||||
</div>
|
||||
{backend.recommended && (
|
||||
<span className="rounded-full bg-primary px-3 py-1 text-xs font-semibold text-primary-foreground">
|
||||
Recommended
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mb-4 text-sm text-muted-foreground">{backend.description}</p>
|
||||
<ul className="space-y-1.5">
|
||||
{backend.advantages.map((advantage) => (
|
||||
<li key={advantage} className="flex items-start gap-2 text-sm">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
{advantage}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Model Performance Comparison</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Approximate transcription times for 5 seconds of audio. GPU times assume a mid-range
|
||||
Vulkan-compatible GPU.
|
||||
</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</th>
|
||||
<th className="pb-3 pr-4 font-semibold">Size</th>
|
||||
<th className="pb-3 pr-4 font-semibold">CPU Time</th>
|
||||
<th className="pb-3 pr-4 font-semibold">GPU Time</th>
|
||||
<th className="pb-3 pr-4 font-semibold">Accuracy</th>
|
||||
<th className="pb-3 font-semibold">Best For</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{modelPerformance.map((row) => (
|
||||
<tr key={row.model} className="border-b border-border">
|
||||
<td className="py-3 pr-4 font-medium">{row.model}</td>
|
||||
<td className="py-3 pr-4 text-muted-foreground">{row.size}</td>
|
||||
<td className="py-3 pr-4 text-muted-foreground">{row.cpuSpeed}</td>
|
||||
<td className="py-3 pr-4 text-muted-foreground">{row.gpuSpeed}</td>
|
||||
<td className="py-3 pr-4 text-muted-foreground">{row.accuracy}</td>
|
||||
<td className="py-3 text-muted-foreground">{row.recommended}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Hardware Requirements</h2>
|
||||
<div className="grid gap-6 sm:grid-cols-3">
|
||||
{hardwareRequirements.map((tier) => (
|
||||
<article
|
||||
key={tier.category}
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<h3 className="mb-3 font-semibold">{tier.category}</h3>
|
||||
<dl className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-muted-foreground">RAM</dt>
|
||||
<dd className="font-medium">{tier.ram}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-muted-foreground">Storage</dt>
|
||||
<dd className="font-medium">{tier.storage}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-muted-foreground">GPU</dt>
|
||||
<dd className="font-medium">{tier.gpu}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p className="mt-4 text-xs text-muted-foreground">{tier.performance}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-muted p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<Gauge className="mt-0.5 h-5 w-5 flex-shrink-0 text-muted-foreground" />
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-foreground">
|
||||
Intel GPU Note
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Some Intel integrated GPUs don't fully support the compute features needed for
|
||||
whisper.cpp GPU acceleration. Vocalinux v0.7.0+ automatically detects incompatible
|
||||
Intel GPUs and falls back to CPU processing for reliability.
|
||||
</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">Get Started with GPU Acceleration</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
The installer automatically detects your GPU and configures the best backend. Just run
|
||||
and go.
|
||||
</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 Now
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</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 hover:bg-muted"
|
||||
>
|
||||
Compare Engines
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,670 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Cpu,
|
||||
Download,
|
||||
Laptop,
|
||||
Monitor,
|
||||
RefreshCw,
|
||||
Server,
|
||||
Shield,
|
||||
Sparkles,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
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[];
|
||||
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. 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 after the one-time model download. Remote API is optional.",
|
||||
prerequisites: [
|
||||
"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)",
|
||||
],
|
||||
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: INSTALL_SCRIPT_COMMAND,
|
||||
otherInstallOptions: [DEB_OPTION, FLATPAK_OPTION],
|
||||
setupNotes: [
|
||||
"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: [
|
||||
...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 42+ with the install script or .rpm, on Wayland or X11.",
|
||||
intro:
|
||||
"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 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)",
|
||||
],
|
||||
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 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: [
|
||||
...COMMON_CHECKS,
|
||||
"Confirm startup behavior and language settings persist after reboot.",
|
||||
],
|
||||
},
|
||||
arch: {
|
||||
name: "Arch Linux",
|
||||
title: "Install Voice Dictation on Arch Linux",
|
||||
description:
|
||||
"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 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 or EndeavourOS",
|
||||
"Python 3.11+ with python-gobject",
|
||||
"Working microphone",
|
||||
"curl installed (sudo pacman -Sy --needed curl), or an AUR helper such as yay",
|
||||
],
|
||||
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`,
|
||||
},
|
||||
],
|
||||
setupNotes: [
|
||||
"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: [
|
||||
...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[]> = {
|
||||
ubuntu: [
|
||||
"ubuntu voice dictation",
|
||||
"speech to text ubuntu",
|
||||
"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",
|
||||
"fedora voice typing",
|
||||
"offline dictation fedora",
|
||||
],
|
||||
arch: [
|
||||
"arch linux voice dictation",
|
||||
"speech to text arch linux",
|
||||
"manjaro dictation",
|
||||
"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",
|
||||
],
|
||||
};
|
||||
|
||||
const isDistroSlug = (value: string): value is DistroSlug => {
|
||||
return value in distroConfig;
|
||||
};
|
||||
|
||||
const resolveParams = async (
|
||||
params: DistroParams,
|
||||
): Promise<{ distro: string }> => {
|
||||
return Promise.resolve(params);
|
||||
};
|
||||
|
||||
const getDistroStyles = (distro: DistroSlug) => {
|
||||
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",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return Object.keys(distroConfig).map((distro) => ({ distro }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: DistroParams;
|
||||
}): Promise<Metadata> {
|
||||
const { distro } = await resolveParams(params);
|
||||
|
||||
if (!isDistroSlug(distro)) {
|
||||
return buildPageMetadata({
|
||||
title: "Linux Voice Dictation Install Guide",
|
||||
description:
|
||||
"Install Vocalinux on Linux with distro-specific setup instructions for Ubuntu, Debian, Fedora, Arch, openSUSE, Linux Mint, and Pop!_OS.",
|
||||
path: "/install",
|
||||
});
|
||||
}
|
||||
|
||||
const config = distroConfig[distro];
|
||||
|
||||
return buildPageMetadata({
|
||||
// Search title leads with the query people type ("ubuntu speech to text");
|
||||
// the on-page h1 stays config.title.
|
||||
title: `${config.name} Speech to Text: Offline Voice Typing Setup`,
|
||||
description: config.description,
|
||||
path: `/install/${distro}`,
|
||||
keywords: distroKeywords[distro],
|
||||
});
|
||||
}
|
||||
|
||||
export default async function DistroInstallPage({
|
||||
params,
|
||||
}: {
|
||||
params: DistroParams;
|
||||
}) {
|
||||
const { distro } = await resolveParams(params);
|
||||
|
||||
if (!isDistroSlug(distro)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const config = distroConfig[distro];
|
||||
const styles = getDistroStyles(distro);
|
||||
const DistroIcon = styles.icon;
|
||||
|
||||
const howToJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "HowTo",
|
||||
name: `${config.title} in 3 Steps`,
|
||||
description: config.description,
|
||||
inLanguage: "en-US",
|
||||
totalTime: "PT10M",
|
||||
supply: [
|
||||
{
|
||||
"@type": "HowToSupply",
|
||||
name: "Linux computer",
|
||||
},
|
||||
{
|
||||
"@type": "HowToSupply",
|
||||
name: "Microphone",
|
||||
},
|
||||
],
|
||||
tool: [
|
||||
{
|
||||
"@type": "HowToTool",
|
||||
name: "Terminal",
|
||||
},
|
||||
],
|
||||
step: [
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
name: "Open terminal",
|
||||
text: "Launch your terminal application and ensure curl is available.",
|
||||
url: absoluteUrl(`/install/${distro}`),
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
name: "Run installer",
|
||||
text: `Run: ${config.installCommand}`,
|
||||
url: absoluteUrl(`/install/${distro}`),
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
name: "Start dictating",
|
||||
text: "Launch vocalinux and hold Right Alt (Option) to begin dictation, or choose toggle mode in Settings.",
|
||||
url: absoluteUrl(`/install/${distro}`),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(howToJsonLd) }}
|
||||
/>
|
||||
|
||||
<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}`}
|
||||
>
|
||||
<DistroIcon className={`h-4 w-4 ${styles.iconClass}`} />
|
||||
{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>
|
||||
|
||||
<div
|
||||
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" />
|
||||
Install command
|
||||
</h2>
|
||||
<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)] sm:text-base">
|
||||
{config.installCommand}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-10 grid gap-6 md:grid-cols-2">
|
||||
<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">
|
||||
<Shield className="h-5 w-5 text-primary" />
|
||||
Prerequisites
|
||||
</h2>
|
||||
<ul className="space-y-2 text-muted-foreground">
|
||||
{config.prerequisites.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" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<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" aria-hidden="true" />
|
||||
Supported on
|
||||
</h2>
|
||||
<ul className="space-y-2 text-muted-foreground">
|
||||
{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"
|
||||
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" />
|
||||
Setup notes for {config.name}
|
||||
</h2>
|
||||
<ol className="space-y-3 text-muted-foreground">
|
||||
{config.setupNotes.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" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<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
|
||||
</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
{config.postInstallChecks.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" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<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"
|
||||
>
|
||||
engine comparison
|
||||
</Link>
|
||||
. Stuck? See
|
||||
<Link
|
||||
href="/troubleshooting/"
|
||||
className="ml-1 font-semibold text-primary hover:underline"
|
||||
>
|
||||
troubleshooting
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { ChevronRight, Cpu, Laptop, Terminal } from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { TerminalBlock } from "@/components/terminal-block";
|
||||
import { buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const installCommand =
|
||||
"curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh -o /tmp/vl.sh && bash /tmp/vl.sh --interactive";
|
||||
const installDisplayCommand = `curl -fsSL \\
|
||||
https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh \\
|
||||
-o /tmp/vl.sh && \\
|
||||
bash /tmp/vl.sh --interactive`;
|
||||
|
||||
const distroGuides = [
|
||||
{
|
||||
name: "Ubuntu",
|
||||
href: "/install/ubuntu/",
|
||||
description:
|
||||
"Install offline voice dictation on Ubuntu 24.04+ with GNOME, KDE, X11, or Wayland.",
|
||||
icon: Laptop,
|
||||
},
|
||||
{
|
||||
name: "Fedora",
|
||||
href: "/install/fedora/",
|
||||
description:
|
||||
"Set up speech-to-text on Fedora Workstation and Silverblue with distro-specific notes.",
|
||||
icon: Terminal,
|
||||
},
|
||||
{
|
||||
name: "Arch Linux",
|
||||
href: "/install/arch/",
|
||||
description:
|
||||
"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 Speech to Text on Linux (Ubuntu, Fedora, Arch)",
|
||||
description:
|
||||
"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",
|
||||
"linux speech to text setup",
|
||||
"ubuntu voice typing install",
|
||||
"fedora speech recognition",
|
||||
"arch linux dictation",
|
||||
],
|
||||
});
|
||||
|
||||
export default function InstallGuidesPage() {
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<section>
|
||||
<p className="subpage-kicker">Linux installation</p>
|
||||
<h1>Install Voice Dictation on Linux</h1>
|
||||
<p className="subpage-lede">
|
||||
Pick your distro guide and follow a proven setup flow for offline
|
||||
speech-to-text on Linux. Every guide is written for real desktop usage.
|
||||
</p>
|
||||
|
||||
<div className="mt-8">
|
||||
<TerminalBlock
|
||||
command={installCommand}
|
||||
displayCommand={installDisplayCommand}
|
||||
label="install.sh --interactive"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
Then launch <code>vocalinux</code>.
|
||||
</p>
|
||||
<div className="install-alts">
|
||||
<a
|
||||
className="install-alt"
|
||||
href="https://github.com/VocaHQ/vocalinux/releases"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p className="install-alt-kicker">No installer</p>
|
||||
<h2>AppImage</h2>
|
||||
<p>
|
||||
x86_64 or aarch64 from GitHub Releases. No root. Host
|
||||
text-injection tools are still required.
|
||||
</p>
|
||||
</a>
|
||||
<a
|
||||
className="install-alt"
|
||||
href="https://github.com/VocaHQ/vocalinux/blob/main/docs/INSTALL.md#distro-packages-deb--rpm"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p className="install-alt-kicker">New</p>
|
||||
<h2>Native packages</h2>
|
||||
<p>
|
||||
.deb for Debian 12+ and Ubuntu 24.04+. .rpm for Fedora.
|
||||
x86_64 and aarch64 on every release.
|
||||
</p>
|
||||
</a>
|
||||
<a
|
||||
className="install-alt"
|
||||
href="/flatpak/"
|
||||
>
|
||||
<p className="install-alt-kicker">Auto-updates</p>
|
||||
<h2>Flatpak</h2>
|
||||
<p>
|
||||
Install once from the VocaHQ remote and{" "}
|
||||
<code>flatpak update</code> carries each stable release. Or
|
||||
sideload the .flatpak bundle.
|
||||
</p>
|
||||
</a>
|
||||
<a
|
||||
className="install-alt"
|
||||
href="https://snapcraft.io/vocalinux"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p className="install-alt-kicker">Snap Store</p>
|
||||
<h2>Snap</h2>
|
||||
<p>
|
||||
<code>snap install vocalinux --edge</code>. amd64 only for
|
||||
now; a .snap is attached to every release too.
|
||||
</p>
|
||||
</a>
|
||||
<a
|
||||
className="install-alt"
|
||||
href="https://aur.archlinux.org/packages/vocalinux"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p className="install-alt-kicker">Arch</p>
|
||||
<h2>AUR</h2>
|
||||
<p>
|
||||
<code>yay -S vocalinux</code>, or vocalinux-bin for the
|
||||
prebuilt package.
|
||||
</p>
|
||||
</a>
|
||||
<a
|
||||
className="install-alt"
|
||||
href="https://github.com/VocaHQ/vocalinux/blob/main/docs/INSTALL.md#from-source"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p className="install-alt-kicker">From source</p>
|
||||
<h2>Build it yourself</h2>
|
||||
<p>
|
||||
Clone the repo and run <code>./install.sh</code>. The install
|
||||
guide has the full source path.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-12 grid gap-4 md:grid-cols-3">
|
||||
{distroGuides.map((guide) => {
|
||||
const Icon = guide.icon;
|
||||
return (
|
||||
<Link
|
||||
key={guide.href}
|
||||
href={guide.href}
|
||||
className="subpage-card group p-5"
|
||||
>
|
||||
<div className="mb-3 inline-flex rounded-[10px] bg-primary/10 p-2.5">
|
||||
<Icon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-xl font-semibold">{guide.name}</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
{guide.description}
|
||||
</p>
|
||||
<span className="inline-flex items-center gap-1 text-sm font-semibold text-primary">
|
||||
Read {guide.name} guide
|
||||
<ChevronRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="subpage-panel mt-12 p-6 sm:p-8">
|
||||
<h2 className="mb-4">After installation</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li>
|
||||
Launch with{" "}
|
||||
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-sm text-foreground">
|
||||
vocalinux
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
Hold Right Alt (Option) to dictate, or switch to toggle mode in Settings.
|
||||
</li>
|
||||
<li>
|
||||
Choose whisper.cpp for speed, Whisper for maximum model
|
||||
compatibility, or VOSK for low-RAM systems.
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-6 text-sm text-muted-foreground">
|
||||
Need help choosing an engine? See the{" "}
|
||||
<Link
|
||||
href="/compare/"
|
||||
className="font-semibold text-primary hover:underline"
|
||||
>
|
||||
speech engine comparison
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { CheckCircle2, ChevronRight, Globe, Languages, Mic } from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const WHISPER_ENGINES = ["whisper.cpp", "Whisper", "Faster Whisper"] as const;
|
||||
const ALL_ENGINES = ["whisper.cpp", "Whisper", "Faster Whisper", "VOSK"] as const;
|
||||
|
||||
const supportedLanguages = [
|
||||
{
|
||||
code: "en",
|
||||
name: "English",
|
||||
nativeName: "English",
|
||||
flag: "🇺🇸",
|
||||
description: "Full support across all engines (whisper.cpp, Whisper, VOSK)",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "en-in",
|
||||
name: "English (India)",
|
||||
nativeName: "English (India)",
|
||||
flag: "🇮🇳",
|
||||
description: "Indian English dictation with a dedicated VOSK model",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "ar",
|
||||
name: "Arabic",
|
||||
nativeName: "العربية",
|
||||
flag: "🇸🇦",
|
||||
description: "Arabic speech recognition across Whisper and VOSK",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "bn",
|
||||
name: "Bengali",
|
||||
nativeName: "বাংলা",
|
||||
flag: "🇧🇩",
|
||||
description: "Bengali dictation with multilingual Whisper models",
|
||||
engines: [...WHISPER_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "ca",
|
||||
name: "Catalan",
|
||||
nativeName: "Català",
|
||||
flag: "🇦🇩",
|
||||
description: "Catalan voice typing with Whisper and VOSK",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "zh",
|
||||
name: "Chinese",
|
||||
nativeName: "中文",
|
||||
flag: "🇨🇳",
|
||||
description: "Mandarin Chinese voice dictation support",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "cs",
|
||||
name: "Czech",
|
||||
nativeName: "Čeština",
|
||||
flag: "🇨🇿",
|
||||
description: "Czech dictation with Whisper and VOSK",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "da",
|
||||
name: "Danish",
|
||||
nativeName: "Dansk",
|
||||
flag: "🇩🇰",
|
||||
description: "Danish speech-to-text with multilingual Whisper models",
|
||||
engines: [...WHISPER_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "nl",
|
||||
name: "Dutch",
|
||||
nativeName: "Nederlands",
|
||||
flag: "🇳🇱",
|
||||
description: "Dutch voice recognition across Whisper and VOSK",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "fi",
|
||||
name: "Finnish",
|
||||
nativeName: "Suomi",
|
||||
flag: "🇫🇮",
|
||||
description: "Finnish dictation with multilingual Whisper models",
|
||||
engines: [...WHISPER_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "fr",
|
||||
name: "French",
|
||||
nativeName: "Français",
|
||||
flag: "🇫🇷",
|
||||
description: "French language voice typing and transcription",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "de",
|
||||
name: "German",
|
||||
nativeName: "Deutsch",
|
||||
flag: "🇩🇪",
|
||||
description: "German dictation with high accuracy",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "el",
|
||||
name: "Greek",
|
||||
nativeName: "Ελληνικά",
|
||||
flag: "🇬🇷",
|
||||
description: "Greek speech recognition with multilingual Whisper models",
|
||||
engines: [...WHISPER_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "he",
|
||||
name: "Hebrew",
|
||||
nativeName: "עברית",
|
||||
flag: "🇮🇱",
|
||||
description: "Hebrew dictation with multilingual Whisper models",
|
||||
engines: [...WHISPER_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "hi",
|
||||
name: "Hindi",
|
||||
nativeName: "हिन्दी",
|
||||
flag: "🇮🇳",
|
||||
description: "Hindi language voice typing and transcription",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "hu",
|
||||
name: "Hungarian",
|
||||
nativeName: "Magyar",
|
||||
flag: "🇭🇺",
|
||||
description: "Hungarian dictation with multilingual Whisper models",
|
||||
engines: [...WHISPER_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "id",
|
||||
name: "Indonesian",
|
||||
nativeName: "Bahasa Indonesia",
|
||||
flag: "🇮🇩",
|
||||
description: "Indonesian voice typing with multilingual Whisper models",
|
||||
engines: [...WHISPER_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "it",
|
||||
name: "Italian",
|
||||
nativeName: "Italiano",
|
||||
flag: "🇮🇹",
|
||||
description: "Italian voice recognition and text dictation",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "ja",
|
||||
name: "Japanese",
|
||||
nativeName: "日本語",
|
||||
flag: "🇯🇵",
|
||||
description: "Japanese speech-to-text across Whisper and VOSK",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "ko",
|
||||
name: "Korean",
|
||||
nativeName: "한국어",
|
||||
flag: "🇰🇷",
|
||||
description: "Korean dictation with Whisper and VOSK",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "no",
|
||||
name: "Norwegian",
|
||||
nativeName: "Norsk",
|
||||
flag: "🇳🇴",
|
||||
description: "Norwegian speech recognition with multilingual Whisper models",
|
||||
engines: [...WHISPER_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "fa",
|
||||
name: "Persian",
|
||||
nativeName: "فارسی",
|
||||
flag: "🇮🇷",
|
||||
description: "Persian (Farsi) dictation with Whisper and VOSK",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "pl",
|
||||
name: "Polish",
|
||||
nativeName: "Polski",
|
||||
flag: "🇵🇱",
|
||||
description: "Polish voice typing with Whisper and VOSK",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "pt",
|
||||
name: "Portuguese",
|
||||
nativeName: "Português",
|
||||
flag: "🇧🇷",
|
||||
description: "Brazilian and European Portuguese support",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "ro",
|
||||
name: "Romanian",
|
||||
nativeName: "Română",
|
||||
flag: "🇷🇴",
|
||||
description: "Romanian dictation with multilingual Whisper models",
|
||||
engines: [...WHISPER_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "ru",
|
||||
name: "Russian",
|
||||
nativeName: "Русский",
|
||||
flag: "🇷🇺",
|
||||
description: "Russian language speech-to-text transcription",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "sl",
|
||||
name: "Slovenian",
|
||||
nativeName: "Slovenščina",
|
||||
flag: "🇸🇮",
|
||||
description: "Slovenian dictation with multilingual Whisper models",
|
||||
engines: [...WHISPER_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "es",
|
||||
name: "Spanish",
|
||||
nativeName: "Español",
|
||||
flag: "🇪🇸",
|
||||
description: "Complete Spanish language support for dictation",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "sv",
|
||||
name: "Swedish",
|
||||
nativeName: "Svenska",
|
||||
flag: "🇸🇪",
|
||||
description: "Swedish voice recognition with Whisper and VOSK",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "ta",
|
||||
name: "Tamil",
|
||||
nativeName: "தமிழ்",
|
||||
flag: "🇮🇳",
|
||||
description: "Tamil dictation with multilingual Whisper models",
|
||||
engines: [...WHISPER_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "th",
|
||||
name: "Thai",
|
||||
nativeName: "ไทย",
|
||||
flag: "🇹🇭",
|
||||
description: "Thai speech-to-text with multilingual Whisper models",
|
||||
engines: [...WHISPER_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "tr",
|
||||
name: "Turkish",
|
||||
nativeName: "Türkçe",
|
||||
flag: "🇹🇷",
|
||||
description: "Turkish dictation with Whisper and VOSK",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "uk",
|
||||
name: "Ukrainian",
|
||||
nativeName: "Українська",
|
||||
flag: "🇺🇦",
|
||||
description: "Ukrainian voice typing with Whisper and VOSK",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
{
|
||||
code: "vi",
|
||||
name: "Vietnamese",
|
||||
nativeName: "Tiếng Việt",
|
||||
flag: "🇻🇳",
|
||||
description: "Vietnamese speech recognition with Whisper and VOSK",
|
||||
engines: [...ALL_ENGINES],
|
||||
},
|
||||
];
|
||||
|
||||
const languageCount = supportedLanguages.length;
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: "Automatic Language Detection",
|
||||
description:
|
||||
"Vocalinux can automatically detect the language you're speaking, no manual switching required.",
|
||||
icon: Globe,
|
||||
},
|
||||
{
|
||||
title: "Engine-Aware Catalog",
|
||||
description:
|
||||
"Whisper engines (whisper.cpp, Whisper, Faster Whisper) cover the full language list. VOSK appears only where an official model exists. Parakeet uses its own English / European bundles and ignores the catalog picker.",
|
||||
icon: Mic,
|
||||
},
|
||||
{
|
||||
title: "Offline Processing",
|
||||
description:
|
||||
"Local engines process each language on your machine. Remote API is optional.",
|
||||
icon: CheckCircle2,
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Linux Speech to Text in 30+ Languages, Offline",
|
||||
description:
|
||||
"Offline voice typing on Linux in 30+ languages, including Spanish, French, German, Hungarian, Japanese, Korean and Arabic. Full language list and model picks.",
|
||||
path: "/languages",
|
||||
keywords: [
|
||||
"multilingual voice dictation",
|
||||
"Spanish voice typing",
|
||||
"French speech to text",
|
||||
"German dictation software",
|
||||
"Hungarian voice recognition",
|
||||
"Japanese speech to text",
|
||||
"Korean voice typing",
|
||||
"Arabic dictation",
|
||||
"Italian voice recognition",
|
||||
"Portuguese voice typing",
|
||||
"Russian speech recognition",
|
||||
"Slovenian voice typing",
|
||||
"Chinese dictation",
|
||||
"Hindi voice typing",
|
||||
"Linux multilingual dictation",
|
||||
],
|
||||
});
|
||||
|
||||
export default function LanguagesPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: `Multilingual Voice Dictation for Linux - ${languageCount}+ Languages Supported`,
|
||||
description:
|
||||
"Complete guide to Vocalinux multilingual voice dictation. Dictate in Spanish, French, German, Hungarian, Japanese, Korean, Arabic, Hindi, and more.",
|
||||
dateModified: "2026-07-29",
|
||||
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("/languages"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="subpage-kicker">
|
||||
<Languages className="h-4 w-4" />
|
||||
{languageCount}+ Languages
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Multilingual Voice Dictation for Linux
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Dictate in your native language. Vocalinux lists {languageCount} languages in Settings,
|
||||
and whisper.cpp / Whisper can auto-detect many more. Local engines stay on the machine.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 sm:grid-cols-3">
|
||||
{features.map((feature) => {
|
||||
const Icon = feature.icon;
|
||||
return (
|
||||
<div
|
||||
key={feature.title}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<Icon className="mb-3 h-8 w-8 text-primary" />
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">{feature.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">{feature.description}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Supported Languages</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{supportedLanguages.map((lang) => (
|
||||
<article
|
||||
key={lang.code}
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-3">
|
||||
<span className="text-3xl">{lang.flag}</span>
|
||||
<div>
|
||||
<h3 className="font-semibold">{lang.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">{lang.nativeName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-3 text-sm text-muted-foreground">{lang.description}</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{lang.engines.map((engine) => (
|
||||
<span
|
||||
key={engine}
|
||||
className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium bg-muted"
|
||||
>
|
||||
{engine}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</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">How Language Support Works</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-3 font-semibold">Language Selection</h3>
|
||||
<ul className="space-y-2 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" />
|
||||
Select your language in Settings → Speech Engine → Language
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Use "Auto" for automatic language detection (whisper.cpp/Whisper)
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Switch languages on-the-fly without restarting
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-3 font-semibold">Model Selection</h3>
|
||||
<ul className="space-y-2 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" />
|
||||
Larger models provide better accuracy for all languages
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Use a Standard multilingual whisper.cpp model for non-English dictation
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Models are downloaded automatically on first use
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Ready to Dictate in Your Language?</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Install Vocalinux and start dictating in your preferred language. All processing happens
|
||||
locally on your Linux machine.
|
||||
</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 Now
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</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 hover:bg-muted"
|
||||
>
|
||||
Compare Engines
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import "@/styles/globals.css";
|
||||
import React from "react";
|
||||
import { type Metadata } from "next";
|
||||
import Script from "next/script";
|
||||
import {
|
||||
absoluteUrl,
|
||||
DEFAULT_OG_IMAGE_ALT,
|
||||
DEFAULT_OG_IMAGE_PATH,
|
||||
GITHUB_REPO_URL,
|
||||
SITE_NAME,
|
||||
SITE_URL,
|
||||
TWITTER_HANDLE,
|
||||
VOCAHQ_URL,
|
||||
} from "@/lib/seo";
|
||||
|
||||
export const viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
maximumScale: 5,
|
||||
themeColor: "#f4f1e8",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(SITE_URL),
|
||||
title: {
|
||||
default: "Vocalinux: Offline Speech to Text & Voice Dictation for Linux",
|
||||
template: "%s | Vocalinux",
|
||||
},
|
||||
description:
|
||||
"Free, open-source speech to text for Linux. Offline voice dictation and voice typing in any app with whisper.cpp, VOSK or Parakeet, on X11 and Wayland.",
|
||||
applicationName: SITE_NAME,
|
||||
keywords: [
|
||||
"voice dictation linux",
|
||||
"speech to text linux",
|
||||
"offline voice dictation",
|
||||
"linux voice typing",
|
||||
"whisper.cpp linux",
|
||||
"vosk linux",
|
||||
"remote api speech recognition",
|
||||
"silero vad linux",
|
||||
"whisper.cpp advanced settings",
|
||||
"ibus voice dictation",
|
||||
"wayland voice dictation",
|
||||
"x11 speech recognition",
|
||||
"open source dictation software",
|
||||
"voice typing ubuntu",
|
||||
"voice typing fedora",
|
||||
"voice typing arch linux",
|
||||
],
|
||||
authors: [{ name: "Jatin K Malik", url: "https://github.com/jatinkrmalik" }],
|
||||
creator: "Jatin K Malik",
|
||||
publisher: SITE_NAME,
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/brand/vocalinux-mark-circle.svg", type: "image/svg+xml" },
|
||||
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
|
||||
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
|
||||
{ url: "/favicon-96x96.png", sizes: "96x96", type: "image/png" },
|
||||
{ url: "/vocalinux.png", type: "image/png", sizes: "512x512" },
|
||||
],
|
||||
apple: [
|
||||
{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" },
|
||||
],
|
||||
shortcut: "/favicon-32x32.png",
|
||||
},
|
||||
manifest: "/site.webmanifest",
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: "default",
|
||||
title: SITE_NAME,
|
||||
},
|
||||
formatDetection: {
|
||||
telephone: false,
|
||||
email: false,
|
||||
address: false,
|
||||
},
|
||||
referrer: "strict-origin-when-cross-origin",
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
url: absoluteUrl("/"),
|
||||
siteName: SITE_NAME,
|
||||
title: "Vocalinux: Offline Speech to Text & Voice Dictation for Linux",
|
||||
description:
|
||||
"Voice dictation for Linux with local engines or an optional Remote API. Install in minutes and dictate in any app on Ubuntu, Fedora, Arch, X11, or Wayland.",
|
||||
images: [
|
||||
{
|
||||
url: DEFAULT_OG_IMAGE_PATH,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: DEFAULT_OG_IMAGE_ALT,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Vocalinux: Offline Speech to Text & Voice Dictation for Linux",
|
||||
description:
|
||||
"Speech-to-text for Linux with whisper.cpp and VOSK. Local engines by default, optional Remote API, open source.",
|
||||
images: [DEFAULT_OG_IMAGE_PATH],
|
||||
creator: TWITTER_HANDLE,
|
||||
site: TWITTER_HANDLE,
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
alternates: {
|
||||
canonical: absoluteUrl("/"),
|
||||
languages: {
|
||||
"en-US": absoluteUrl("/"),
|
||||
},
|
||||
},
|
||||
category: "Technology",
|
||||
};
|
||||
|
||||
const organizationJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"@id": `${absoluteUrl("/")}#organization`,
|
||||
name: SITE_NAME,
|
||||
url: absoluteUrl("/"),
|
||||
logo: absoluteUrl("/vocalinux.png"),
|
||||
description: "Open-source, offline voice dictation software for Linux.",
|
||||
sameAs: [GITHUB_REPO_URL, VOCAHQ_URL],
|
||||
founder: {
|
||||
"@type": "Person",
|
||||
name: "Jatin K Malik",
|
||||
url: "https://github.com/jatinkrmalik",
|
||||
},
|
||||
parentOrganization: {
|
||||
"@type": "Organization",
|
||||
name: "VocaHQ",
|
||||
url: VOCAHQ_URL,
|
||||
},
|
||||
};
|
||||
|
||||
const webSiteJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"@id": `${absoluteUrl("/")}#website`,
|
||||
name: SITE_NAME,
|
||||
url: absoluteUrl("/"),
|
||||
inLanguage: "en-US",
|
||||
description:
|
||||
"Vocalinux provides offline speech-to-text and voice dictation for Linux desktops.",
|
||||
publisher: {
|
||||
"@id": `${absoluteUrl("/")}#organization`,
|
||||
},
|
||||
potentialAction: {
|
||||
"@type": "SearchAction",
|
||||
target: {
|
||||
"@type": "EntryPoint",
|
||||
urlTemplate: `${GITHUB_REPO_URL}/issues?q={search_term_string}`,
|
||||
},
|
||||
"query-input": {
|
||||
"@type": "PropertyValueSpecification",
|
||||
valueRequired: true,
|
||||
valueName: "search_term_string",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const siteWideJsonLd = [organizationJsonLd, webSiteJsonLd];
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className="scroll-smooth" suppressHydrationWarning>
|
||||
<head>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(siteWideJsonLd) }}
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<Script
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-7NBBNJNNQ7"
|
||||
strategy="lazyOnload"
|
||||
/>
|
||||
<Script id="google-analytics" strategy="lazyOnload">
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-7NBBNJNNQ7');
|
||||
`}
|
||||
</Script>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -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" />
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Auto-detect</h2>
|
||||
<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" />
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Language shortcuts</h2>
|
||||
<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" />
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Follow keyboard layout</h2>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { SiteChrome } from "@/components/site-chrome";
|
||||
import { DidYouMean } from "@/components/not-found-extras";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Page not found",
|
||||
description: "This page does not exist. Head back to the Vocalinux home page.",
|
||||
// Overrides the layout's "index, follow" so it agrees with Next's noindex.
|
||||
robots: { index: false, follow: true },
|
||||
};
|
||||
|
||||
const quickLinks = [
|
||||
{ href: "/install/", label: "Install guides" },
|
||||
{ href: "/faq/", label: "FAQ" },
|
||||
{ href: "/compare/", label: "Compare engines" },
|
||||
{ href: "/troubleshooting/", label: "Troubleshooting" },
|
||||
{ href: "/changelog/", label: "Changelog" },
|
||||
];
|
||||
|
||||
// 404 is a fine number. These are all true; check them in a shell.
|
||||
const stats = [
|
||||
{
|
||||
title: "chmod 404",
|
||||
body: "r-- --- r--. Owner and others can read, the group gets nothing, and nobody can write. Fitting for a page that isn't there.",
|
||||
},
|
||||
{
|
||||
title: "exit 404",
|
||||
body: "Exit statuses are 8 bits, so a shell reports $? = 148 (404 mod 256). Even the error code got truncated.",
|
||||
},
|
||||
{
|
||||
title: "0x194 · 0o624 · 0b110010100",
|
||||
body: "404 in hex, octal and binary. None of them is a valid URL on this site either.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<SiteChrome variant="page">
|
||||
<div className="page-main">
|
||||
<div className="shell nf">
|
||||
<section className="nf-hero">
|
||||
<p className="nf-kicker">HTTP 404 · No such file or directory</p>
|
||||
<h1>
|
||||
This page is <em>not in your $PATH</em>.
|
||||
</h1>
|
||||
<p className="nf-lede">
|
||||
The link is broken, the page moved, or the URL picked up a typo
|
||||
on the way here. Vocalinux can type what you say, but it can't
|
||||
type a page that doesn't exist.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="nf-stats" aria-labelledby="nf-stats-title">
|
||||
<h2 id="nf-stats-title">Fun facts about 404</h2>
|
||||
<div className="nf-stat-grid">
|
||||
{stats.map((stat) => (
|
||||
<article className="nf-stat" key={stat.title}>
|
||||
<h3>{stat.title}</h3>
|
||||
<p>{stat.body}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="nf-next">
|
||||
<DidYouMean />
|
||||
<div className="nf-actions">
|
||||
<Link className="btn btn-primary" href="/">
|
||||
Back to home
|
||||
</Link>
|
||||
<Link className="btn btn-secondary" href="/install/">
|
||||
Install Vocalinux
|
||||
</Link>
|
||||
</div>
|
||||
<nav className="nf-links" aria-label="Popular pages">
|
||||
{quickLinks.map((link) => (
|
||||
<Link key={link.href} href={link.href}>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SiteChrome>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
CloudOff,
|
||||
Database,
|
||||
Lock,
|
||||
Shield,
|
||||
WifiOff,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const benefits = [
|
||||
{
|
||||
title: "Works without a network",
|
||||
description:
|
||||
"After the model is downloaded, local engines work with no internet. Remote API needs the server you configured.",
|
||||
icon: WifiOff,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Zero Data Collection",
|
||||
description:
|
||||
"No telemetry, no analytics, no usage tracking. Local engines have nowhere to send audio unless you enable Remote API.",
|
||||
icon: Database,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "No Account Required",
|
||||
description:
|
||||
"No signup, no login, no password to forget. Download and run - that's it.",
|
||||
icon: Lock,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Your Voice Stays Yours",
|
||||
description:
|
||||
"Voice data is biometric. Your voice patterns, accent, and speech habits are yours alone.",
|
||||
icon: Shield,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
];
|
||||
|
||||
const vsCloud = [
|
||||
{ feature: "Requires internet for local dictation", offline: false, cloud: true },
|
||||
{ feature: "Voice data uploaded to vendor servers", offline: false, cloud: true },
|
||||
{ feature: "Account/signup required", offline: false, cloud: true },
|
||||
{ feature: "Subscription cost", offline: false, cloud: true },
|
||||
{ feature: "Works in airplane mode after model download", offline: true, cloud: false },
|
||||
{ feature: "Vendor can shut down service", offline: false, cloud: true },
|
||||
];
|
||||
|
||||
const sensitiveUseCases = [
|
||||
{
|
||||
title: "Local engines keep audio on the machine",
|
||||
description:
|
||||
"whisper.cpp, Whisper, and VOSK process speech on your hardware. That is a default for those engines, not a legal or compliance promise.",
|
||||
},
|
||||
{
|
||||
title: "Works after the model is downloaded",
|
||||
description:
|
||||
"Once the model is on disk, local engines keep working without a network. Remote API still needs the server you set.",
|
||||
},
|
||||
{
|
||||
title: "Remote API is opt-in",
|
||||
description:
|
||||
"If you point Vocalinux at a server you configure, audio goes there. Local engines do not.",
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Offline Speech to Text for Linux: No Cloud Needed",
|
||||
description:
|
||||
"Offline speech to text on Linux. whisper.cpp, VOSK and other local engines transcribe on your machine after a one-time model download. No cloud account.",
|
||||
path: "/offline",
|
||||
keywords: [
|
||||
"offline voice dictation",
|
||||
"no cloud speech recognition",
|
||||
"private voice typing",
|
||||
"local speech to text",
|
||||
"offline dictation software",
|
||||
"no internet voice recognition",
|
||||
"airplane mode dictation",
|
||||
"privacy-first speech recognition",
|
||||
],
|
||||
});
|
||||
|
||||
export default function OfflinePage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Offline Voice Dictation for Linux",
|
||||
description:
|
||||
"Local engines process speech on your machine. Remote API is optional and talks only to a server you configure.",
|
||||
dateModified: "2026-02-22",
|
||||
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("/offline"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="subpage-kicker">
|
||||
<CloudOff className="h-4 w-4" />
|
||||
Offline
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Offline Voice Dictation
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Local engines keep speech recognition on your computer using whisper.cpp, VOSK, or
|
||||
OpenAI Whisper models on your hardware. Remote API is optional and talks only to a
|
||||
server you configure.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{benefits.map((benefit) => {
|
||||
const Icon = benefit.icon;
|
||||
return (
|
||||
<article
|
||||
key={benefit.title}
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<div className={`mb-3 inline-flex rounded-lg p-2 ${benefit.iconBg}`}>
|
||||
<Icon className={`h-5 w-5 ${benefit.iconColor}`} />
|
||||
</div>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">{benefit.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">{benefit.description}</p>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Offline vs Cloud Dictation</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 text-center">Vocalinux (Offline)</th>
|
||||
<th className="pb-3 font-semibold text-center">Cloud Dictation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{vsCloud.map((row) => (
|
||||
<tr key={row.feature} className="border-b border-border">
|
||||
<td className="py-3 pr-4">{row.feature}</td>
|
||||
<td className="py-3 pr-4 text-center">
|
||||
{row.offline ? (
|
||||
<CheckCircle2 className="mx-auto h-5 w-5 text-primary" aria-label="Yes" />
|
||||
) : (
|
||||
<XCircle
|
||||
className="mx-auto h-5 w-5 text-muted-foreground/50"
|
||||
aria-label="No"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 text-center">
|
||||
{row.cloud ? (
|
||||
<CheckCircle2 className="mx-auto h-5 w-5 text-primary" aria-label="Yes" />
|
||||
) : (
|
||||
<XCircle
|
||||
className="mx-auto h-5 w-5 text-muted-foreground/50"
|
||||
aria-label="No"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Local engines</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{sensitiveUseCases.map((useCase) => (
|
||||
<div
|
||||
key={useCase.title}
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<h3 className="mb-2 font-semibold">{useCase.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{useCase.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</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">
|
||||
How Offline Processing Works
|
||||
</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Vocalinux uses speech recognition models that run entirely on your CPU or GPU:
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<strong>whisper.cpp</strong> - C++ port of OpenAI Whisper, runs locally with GPU
|
||||
acceleration
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<strong>VOSK</strong> - Lightweight offline engine, minimal resources
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<strong>OpenAI Whisper</strong> - PyTorch-based, runs on NVIDIA GPU or CPU
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
Models are downloaded once (74MB-3.0GB depending on size). After that, local engines
|
||||
process on the machine. Remote API is the exception: it sends audio to the server you
|
||||
configure.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-3 text-xl font-semibold text-foreground">
|
||||
Verify It Yourself
|
||||
</h2>
|
||||
<p className="mb-3 text-sm text-muted-foreground">
|
||||
Don't trust - verify. Vocalinux is open source. You can:
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
Read every line of code on{" "}
|
||||
<a
|
||||
href="https://github.com/VocaHQ/vocalinux"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold hover:underline"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
Run with <code className="rounded bg-muted px-1">--debug</code> and
|
||||
watch network activity. Local engines should stay quiet during dictation.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
Monitor with <code className="rounded bg-muted px-1">tcpdump</code> or{" "}
|
||||
<code className="rounded bg-muted px-1">wireshark</code>. Local engines
|
||||
should stay quiet during dictation.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
Disconnect from the internet and check that a local engine still dictates
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Truly Private Voice Dictation</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Install Vocalinux and use a local engine if you want speech recognition on the machine.
|
||||
Remote API stays optional.
|
||||
</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 Now
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/open-source/"
|
||||
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 hover:bg-muted"
|
||||
>
|
||||
View Source Code
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Code,
|
||||
Eye,
|
||||
GitBranch,
|
||||
Heart,
|
||||
Lock,
|
||||
Scale,
|
||||
Shield,
|
||||
Users,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const benefits = [
|
||||
{
|
||||
title: "No Vendor Lock-in",
|
||||
description:
|
||||
"Your voice data stays with you. No subscription to cancel, no service to shut down, no company to go out of business.",
|
||||
icon: Lock,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Transparent & Auditable",
|
||||
description:
|
||||
"Every line of code is visible. Security researchers and users can verify exactly what the software does.",
|
||||
icon: Eye,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Community Driven",
|
||||
description:
|
||||
"Built by Linux users, for Linux users. Features and fixes come from real users, not corporate roadmaps.",
|
||||
icon: Users,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Free and open source",
|
||||
description:
|
||||
"AGPL-3.0 licensed. Free to use, modify, and share. No freemium, no premium tier, no hidden costs.",
|
||||
icon: Heart,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
];
|
||||
|
||||
const vsProprietary = [
|
||||
{ feature: "Cost", vocalinux: "Free and open source", proprietary: "$10-500/month or $500+ license" },
|
||||
{ feature: "Source Code", vocalinux: "Fully open", proprietary: "Closed, proprietary" },
|
||||
{ feature: "Data Privacy", vocalinux: "Local engines by default", proprietary: "Often cloud-based" },
|
||||
{ feature: "Linux Support", vocalinux: "Native", proprietary: "Limited or none" },
|
||||
{ feature: "Customization", vocalinux: "Unlimited", proprietary: "Limited to vendor options" },
|
||||
{ feature: "Vendor Dependency", vocalinux: "None", proprietary: "High" },
|
||||
{ feature: "Community Support", vocalinux: "Active GitHub community", proprietary: "Paid support only" },
|
||||
];
|
||||
|
||||
const techStack = [
|
||||
{
|
||||
name: "whisper.cpp",
|
||||
description: "High-performance C++ port of OpenAI's Whisper model",
|
||||
license: "MIT",
|
||||
},
|
||||
{
|
||||
name: "VOSK",
|
||||
description: "Offline speech recognition toolkit",
|
||||
license: "Apache 2.0",
|
||||
},
|
||||
{
|
||||
name: "GTK 3",
|
||||
description: "Cross-platform GUI toolkit for Linux",
|
||||
license: "LGPL",
|
||||
},
|
||||
{
|
||||
name: "Python",
|
||||
description: "Application logic and orchestration",
|
||||
license: "PSF License",
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Open Source Dictation Software for Linux (AGPL-3.0)",
|
||||
description:
|
||||
"Free, open source dictation software for Linux under AGPL-3.0. Offline speech recognition with whisper.cpp or VOSK, no required account, all code on GitHub.",
|
||||
path: "/open-source",
|
||||
keywords: [
|
||||
"open source voice dictation",
|
||||
"free speech to text Linux",
|
||||
"AGPL voice recognition",
|
||||
"FOSS dictation software",
|
||||
"Linux voice typing free",
|
||||
"transparent voice recognition",
|
||||
"community voice dictation",
|
||||
"no subscription dictation",
|
||||
],
|
||||
});
|
||||
|
||||
export default function OpenSourcePage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Open Source Voice Dictation for Linux",
|
||||
description:
|
||||
"Why Vocalinux is 100% open source. AGPL-3.0 licensed, community-driven, no vendor lock-in voice dictation for Linux.",
|
||||
dateModified: "2026-02-22",
|
||||
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("/open-source"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-border bg-primary/10 px-4 py-1.5 text-sm font-medium text-muted-foreground">
|
||||
<Code className="h-4 w-4" />
|
||||
Open Source
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Free & Open Source Voice Dictation
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Vocalinux is 100% open source under the AGPL-3.0 license, and part of{" "}
|
||||
<a
|
||||
href="https://vocahq.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-primary hover:underline"
|
||||
>
|
||||
VocaHQ
|
||||
</a>
|
||||
. No subscriptions, no vendor lock-in, no hidden costs. Local engines keep
|
||||
audio on the machine. Remote API is optional.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-primary/5 p-6">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<Scale className="h-6 w-6 text-muted-foreground" />
|
||||
<h2 className="text-xl font-semibold text-foreground">
|
||||
AGPL-3.0 Licensed
|
||||
</h2>
|
||||
</div>
|
||||
<p className="mb-4 text-muted-foreground">
|
||||
Vocalinux is released under the GNU Affero General Public License v3.0, which means:
|
||||
</p>
|
||||
<ul className="grid gap-2 sm:grid-cols-2">
|
||||
{[
|
||||
"Free to use for any purpose",
|
||||
"Free to study and modify",
|
||||
"Free to share with others",
|
||||
"Modifications must remain open",
|
||||
"Network-deployed modifications must offer source",
|
||||
].map((item) => (
|
||||
<li key={item} className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Why Open Source Matters</h2>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
{benefits.map((benefit) => {
|
||||
const Icon = benefit.icon;
|
||||
return (
|
||||
<article
|
||||
key={benefit.title}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className={`rounded-lg p-2.5 ${benefit.iconBg}`}>
|
||||
<Icon className={`h-5 w-5 ${benefit.iconColor}`} />
|
||||
</div>
|
||||
<h3 className="font-semibold">{benefit.title}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{benefit.description}</p>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Open Source vs Proprietary</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">Vocalinux</th>
|
||||
<th className="pb-3 font-semibold">Proprietary Alternatives</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{vsProprietary.map((row) => (
|
||||
<tr key={row.feature} className="border-b border-border">
|
||||
<td className="py-3 pr-4 font-medium">{row.feature}</td>
|
||||
<td className="py-3 pr-4 text-primary">{row.vocalinux}</td>
|
||||
<td className="py-3 text-muted-foreground">{row.proprietary}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Built on Open Source</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Vocalinux stands on the shoulders of these excellent open source projects:
|
||||
</p>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{techStack.map((tech) => (
|
||||
<div
|
||||
key={tech.name}
|
||||
className="rounded-[12px] border border-border bg-background p-4"
|
||||
>
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<h3 className="font-semibold">{tech.name}</h3>
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium bg-muted">
|
||||
{tech.license}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{tech.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<Shield className="mt-1 h-8 w-8 flex-shrink-0 text-primary" />
|
||||
<div>
|
||||
<h2 className="mb-3 text-xl font-semibold">Privacy by Design</h2>
|
||||
<p className="mb-4 text-muted-foreground">
|
||||
Because Vocalinux is open source, you can verify what local engines do on your
|
||||
machine. The installed app has no usage telemetry. Remote API is optional and only
|
||||
talks to a server you configure.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{["No account required", "Local engines need no network after download", "No usage telemetry", "Full transparency"].map(
|
||||
(item) => (
|
||||
<span
|
||||
key={item}
|
||||
className="rounded-full bg-muted px-3 py-1 text-xs font-medium bg-muted"
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-primary/5 p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<GitBranch className="mt-1 h-6 w-6 flex-shrink-0 text-primary" />
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-foreground">
|
||||
Contribute to Vocalinux
|
||||
</h3>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
We welcome contributions! Whether it's bug fixes, features, documentation, or
|
||||
translations - join the community.
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/VocaHQ/vocalinux"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border bg-background px-4 py-2 text-sm font-semibold text-foreground hover:bg-muted"
|
||||
>
|
||||
<Zap className="h-4 w-4" />
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Join the Open Source Revolution</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Install Vocalinux and experience voice dictation that respects your freedom and privacy.
|
||||
</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 Free
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/alternatives/"
|
||||
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 hover:bg-muted"
|
||||
>
|
||||
Compare Alternatives
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,297 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { CheckCircle2, Lock, Server, Shield, Eye, Database, ExternalLink } from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Privacy Policy - Local Voice Dictation",
|
||||
description:
|
||||
"Local engines keep audio on your computer. Remote API is optional and only talks to a server you configure.",
|
||||
path: "/privacy",
|
||||
keywords: [
|
||||
"vocalinux privacy",
|
||||
"offline voice dictation privacy",
|
||||
"speech to text data privacy",
|
||||
"linux dictation security",
|
||||
],
|
||||
});
|
||||
|
||||
export default function PrivacyPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Vocalinux Privacy Policy",
|
||||
description:
|
||||
"How Vocalinux handles voice data: local engines on your machine, optional Remote API to a server you configure.",
|
||||
dateModified: "2026-09-23",
|
||||
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("/privacy"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-border bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<Shield className="h-4 w-4" />
|
||||
Privacy First
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
How Vocalinux handles your voice
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Vocalinux is built for privacy. Local engines process speech on your machine. Remote API
|
||||
is optional and only talks to a server you set.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Key Privacy Points */}
|
||||
<section className="mb-12 grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<div className="mb-4 inline-flex rounded-[12px] bg-primary/10 p-3">
|
||||
<Lock className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h2 className="subpage-card-title mb-2 text-xl font-semibold">Local engines</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Local engines do not need the internet after the model is downloaded. Processing stays
|
||||
on your device unless you turn on Remote API.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<div className="mb-4 inline-flex rounded-[12px] bg-primary/10 p-3">
|
||||
<Server className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h2 className="subpage-card-title mb-2 text-xl font-semibold">No Cloud Processing</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Local engines do not send your voice to our servers. Remote API sends audio only to the
|
||||
URL you configure.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<div className="mb-4 inline-flex rounded-[12px] bg-primary/10 p-3">
|
||||
<Eye className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h2 className="subpage-card-title mb-2 text-xl font-semibold">Open Source</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Fully open source under AGPL-3.0. Inspect the code yourself on GitHub. The installed
|
||||
app does not send usage telemetry.
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
{/* Detailed Privacy Sections */}
|
||||
<section className="space-y-8">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Database className="h-5 w-5 text-primary" />
|
||||
Data Collection
|
||||
</h2>
|
||||
<div className="space-y-4 text-muted-foreground">
|
||||
<p>
|
||||
<strong className="text-foreground">
|
||||
The installed Vocalinux app does not send usage telemetry.
|
||||
</strong>{" "}
|
||||
It does not phone home. The installed app does not do the following
|
||||
for us:
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Does not send voice recordings or audio data to us
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Does not send transcribed text to us
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Does not send usage analytics or telemetry
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Does not send device fingerprints to us
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Does not collect IP addresses or location data for us
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Local engines keep audio on your device. Remote API sends audio only
|
||||
to the server you configure.
|
||||
</p>
|
||||
<p>
|
||||
Those limits apply to the installed app. They do not describe this
|
||||
website.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Eye className="h-5 w-5 text-primary" />
|
||||
This website (vocalinux.com)
|
||||
</h2>
|
||||
<div className="space-y-4 text-muted-foreground">
|
||||
<p>
|
||||
vocalinux.com loads Google Analytics via the googletagmanager gtag
|
||||
script for measurement ID{" "}
|
||||
<code className="rounded bg-muted px-1.5 py-0.5">G-7NBBNJNNQ7</code>{" "}
|
||||
and runs gtag config with no IP anonymization and no cookie flags.
|
||||
That sends page views and a Google Analytics client identifier (a
|
||||
cookie) to Google. Google processes IP addresses and device and
|
||||
browser data. The installed Vocalinux app does not send usage
|
||||
telemetry.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Shield className="h-5 w-5 text-primary" />
|
||||
How We Process Your Voice
|
||||
</h2>
|
||||
<div className="space-y-4 text-muted-foreground">
|
||||
<p>When you dictate, here's exactly what happens:</p>
|
||||
<ol className="space-y-3 pl-4">
|
||||
<li className="border-l-2 border-primary pl-4">
|
||||
<strong className="text-foreground">1. Audio Capture</strong> - Your microphone
|
||||
records audio temporarily in RAM
|
||||
</li>
|
||||
<li className="border-l-2 border-primary pl-4">
|
||||
<strong className="text-foreground">2. Local Processing</strong> - Audio is
|
||||
processed by whisper.cpp, Whisper, or VOSK on your CPU/GPU
|
||||
</li>
|
||||
<li className="border-l-2 border-primary pl-4">
|
||||
<strong className="text-foreground">3. Text Output</strong> - Transcribed text is
|
||||
injected into your active application
|
||||
</li>
|
||||
<li className="border-l-2 border-primary pl-4">
|
||||
<strong className="text-foreground">4. Immediate Deletion</strong> - Audio data is
|
||||
discarded immediately after processing
|
||||
</li>
|
||||
</ol>
|
||||
<p className="mt-4 rounded-lg bg-muted p-4 text-sm">
|
||||
<strong>Note:</strong> Local engines do not write audio to disk or send it over the
|
||||
network. Remote API sends audio to the server you configure.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Lock className="h-5 w-5 text-primary" />
|
||||
Configuration Data
|
||||
</h2>
|
||||
<div className="space-y-4 text-muted-foreground">
|
||||
<p>
|
||||
Vocalinux stores configuration locally at <code className="rounded bg-muted px-1.5 py-0.5">~/.config/vocalinux/config.json</code>:
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Settings like preferred engine, model size, and language
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Custom keyboard shortcuts
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Window and tray preferences
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
This data is stored in JSON. You can view, edit, or delete it at
|
||||
any time.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
{/* Comparison with Cloud Services */}
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Vocalinux vs Cloud Dictation Services</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="pb-3 text-sm font-semibold">Feature</th>
|
||||
<th className="pb-3 text-sm font-semibold">Vocalinux</th>
|
||||
<th className="pb-3 text-sm font-semibold">Cloud Services</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-sm text-muted-foreground">
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-3 font-medium text-foreground">Data leaves device</td>
|
||||
<td className="py-3 text-primary">
|
||||
Only with Remote API
|
||||
</td>
|
||||
<td className="py-3 text-primary">Always</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-3 font-medium text-foreground">Internet required</td>
|
||||
<td className="py-3 text-primary">
|
||||
Only for Remote API or a model download
|
||||
</td>
|
||||
<td className="py-3 text-primary">Yes</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-3 font-medium text-foreground">Source code available</td>
|
||||
<td className="py-3 text-primary">Yes (AGPL-3.0)</td>
|
||||
<td className="py-3 text-primary">No</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-3 font-medium text-foreground">Voice recordings stored</td>
|
||||
<td className="py-3 text-primary">Not by us; Remote API only if you enable it</td>
|
||||
<td className="py-3">Varies by service</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-3 font-medium text-foreground">Cost</td>
|
||||
<td className="py-3 text-primary">Free and open source</td>
|
||||
<td className="py-3">Subscription</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact */}
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 text-xl font-semibold">Questions About Privacy?</h2>
|
||||
<p className="mb-4 text-muted-foreground">
|
||||
If you have any questions about our privacy practices or want to verify our claims,
|
||||
please reach out:
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/VocaHQ/vocalinux/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-primary hover:underline"
|
||||
>
|
||||
Open an issue on GitHub
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Globe,
|
||||
Lock,
|
||||
Network,
|
||||
Server,
|
||||
Settings,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const protocolOptions = [
|
||||
{
|
||||
name: "OpenAI-compatible",
|
||||
endpoint: "/v1/audio/transcriptions",
|
||||
description:
|
||||
"Use servers that expose the common Whisper transcription API shape, including Speaches, LocalAI, and other compatible backends.",
|
||||
},
|
||||
{
|
||||
name: "whisper.cpp server",
|
||||
endpoint: "/inference",
|
||||
description:
|
||||
"Point Vocalinux at the HTTP server bundled with whisper.cpp when you want a small LAN transcription appliance.",
|
||||
},
|
||||
];
|
||||
|
||||
const workflowSteps = [
|
||||
"Vocalinux records microphone audio locally as 16 kHz mono PCM.",
|
||||
"Voice activity detection decides when an utterance is ready.",
|
||||
"The utterance is packaged as an in-memory WAV upload.",
|
||||
"The configured server returns JSON with a text field.",
|
||||
"Vocalinux injects the transcription into the focused Linux app.",
|
||||
];
|
||||
|
||||
const setupSteps = [
|
||||
{
|
||||
title: "Run a trusted transcription server",
|
||||
description:
|
||||
"Start a whisper.cpp server or an OpenAI-compatible Whisper service on a machine your Linux desktop can reach.",
|
||||
},
|
||||
{
|
||||
title: "Open Settings -> Advanced",
|
||||
description:
|
||||
"Remote API is a power-user engine. Enable advanced settings, then configure the Remote Server section.",
|
||||
},
|
||||
{
|
||||
title: "Choose the endpoint format",
|
||||
description:
|
||||
"Select OpenAI-compatible or whisper.cpp so Vocalinux sends the multipart fields your server expects.",
|
||||
},
|
||||
{
|
||||
title: "Test, then dictate",
|
||||
description:
|
||||
"Use the connection test as a reachability check, then start dictating with the same shortcut flow as local engines.",
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Remote API Speech Recognition for Linux Dictation",
|
||||
description:
|
||||
"Configure Vocalinux Remote API speech recognition with OpenAI-compatible Whisper servers or whisper.cpp server endpoints for Linux voice dictation.",
|
||||
path: "/remote-api",
|
||||
keywords: [
|
||||
"remote api speech recognition linux",
|
||||
"openai compatible whisper linux dictation",
|
||||
"whisper.cpp server vocalinux",
|
||||
"self hosted transcription linux",
|
||||
],
|
||||
});
|
||||
|
||||
export default function RemoteApiPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "TechArticle",
|
||||
headline: "Remote API Speech Recognition for Linux Dictation",
|
||||
description:
|
||||
"How Vocalinux connects Linux voice dictation to OpenAI-compatible and whisper.cpp remote transcription servers.",
|
||||
dateModified: "2026-06-11",
|
||||
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("/remote-api"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="border-primary/30 bg-primary/10 mb-4 inline-flex items-center gap-2 rounded-full border px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<Server className="h-4 w-4" />
|
||||
v0.12.0 Remote Engine
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Remote API Speech Recognition for Linux
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Vocalinux can offload the heavy transcription step to a trusted HTTP
|
||||
server while keeping desktop capture, voice activity detection,
|
||||
shortcut handling, and text injection on your Linux machine.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 md:grid-cols-3">
|
||||
{[
|
||||
{
|
||||
icon: Network,
|
||||
title: "Use stronger hardware",
|
||||
description:
|
||||
"Run larger Whisper models on a workstation, mini PC, or server while a lightweight laptop stays responsive.",
|
||||
},
|
||||
{
|
||||
icon: Globe,
|
||||
title: "Open protocol choices",
|
||||
description:
|
||||
"Target OpenAI-compatible transcription services or the native whisper.cpp server endpoint.",
|
||||
},
|
||||
{
|
||||
icon: Settings,
|
||||
title: "Same desktop workflow",
|
||||
description:
|
||||
"Remote API behaves like a Vocalinux engine, so toggle mode, push-to-talk, VAD, and text injection still work normally.",
|
||||
},
|
||||
].map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<article
|
||||
key={item.title}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<span className="bg-primary/10 mb-4 inline-flex rounded-[12px] p-3">
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</span>
|
||||
<h2 className="mb-2 text-xl font-semibold">{item.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{item.description}
|
||||
</p>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-5 font-display text-2xl font-semibold">
|
||||
Supported Remote API Formats
|
||||
</h2>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{protocolOptions.map((option) => (
|
||||
<article
|
||||
key={option.endpoint}
|
||||
className="rounded-[12px] border border-border bg-muted p-5"
|
||||
>
|
||||
<h3 className="mb-2 text-lg font-semibold">{option.name}</h3>
|
||||
<p className="mb-3 text-sm text-muted-foreground">
|
||||
{option.description}
|
||||
</p>
|
||||
<code className="rounded-md bg-[color:var(--terminal)] px-2.5 py-1.5 text-sm text-[color:var(--terminal-fg)]">
|
||||
{option.endpoint}
|
||||
</code>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 lg:grid-cols-[1fr_1.1fr]">
|
||||
<div className="rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Terminal className="h-5 w-5 text-primary" />
|
||||
How the Request Flows
|
||||
</h2>
|
||||
<ol className="space-y-3 text-sm text-muted-foreground">
|
||||
{workflowSteps.map((step, index) => (
|
||||
<li key={step} className="flex gap-3">
|
||||
<span className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-primary text-xs font-semibold text-primary-foreground">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span>{step}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Lock className="h-5 w-5 text-primary" />
|
||||
Privacy and Security Notes
|
||||
</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" />
|
||||
Audio is not written to disk before upload.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Use HTTPS and an API key outside a trusted local network.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Remote API is not on-device. Audio goes to the server you
|
||||
configure.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-5 font-display text-2xl font-semibold">Remote API Setup Path</h2>
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
{setupSteps.map((step, index) => (
|
||||
<article
|
||||
key={step.title}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<span className="mb-4 inline-flex h-8 w-8 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
|
||||
{index + 1}
|
||||
</span>
|
||||
<h3 className="mb-2 text-xl font-semibold">{step.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{step.description}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-primary/20 bg-primary/5 rounded-[12px] border p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Next Steps</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Link href="/advanced-settings/" className="group">
|
||||
<h3 className="font-semibold">Open advanced settings</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
See where Remote Server options live in the settings dialog.
|
||||
</p>
|
||||
<span className="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
Advanced guide <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
<Link href="/compare/" className="group">
|
||||
<h3 className="font-semibold">Compare all engines</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Decide when remote transcription beats local whisper.cpp, Whisper,
|
||||
or VOSK.
|
||||
</p>
|
||||
<span className="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
Engine comparison <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
<Link href="/privacy/" className="group">
|
||||
<h3 className="font-semibold">Review privacy tradeoffs</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Understand how local and remote recognition differ for sensitive
|
||||
dictation.
|
||||
</p>
|
||||
<span className="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
Privacy policy <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { CheckCircle2, ChevronRight, Heart, Keyboard, Shield, User } from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const benefits = [
|
||||
{
|
||||
title: "Reduce Physical Strain",
|
||||
description:
|
||||
"Give your hands, wrists, and fingers a break. Voice dictation lets you type without the physical toll of keyboard use.",
|
||||
icon: Heart,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Prevent Repetitive Stress",
|
||||
description:
|
||||
"RSI develops from thousands of repeated motions. Dictating breaks the cycle and allows recovery.",
|
||||
icon: Keyboard,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Privacy for Health",
|
||||
description:
|
||||
"Local engines process speech on your machine. Remote API is optional and only talks to a server you configure.",
|
||||
icon: Shield,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Independence",
|
||||
description:
|
||||
"For those with mobility limitations, voice dictation provides computer independence without relying on others.",
|
||||
icon: User,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
];
|
||||
|
||||
const conditions = [
|
||||
{
|
||||
name: "Carpal Tunnel Syndrome",
|
||||
description:
|
||||
"Compression of the median nerve causing numbness, tingling, and weakness in the hand.",
|
||||
howDictationHelps:
|
||||
"Eliminate the wrist movements that aggravate symptoms while maintaining productivity.",
|
||||
},
|
||||
{
|
||||
name: "Repetitive Strain Injury (RSI)",
|
||||
description: "Pain in muscles, nerves, or tendons caused by repetitive movements and overuse.",
|
||||
howDictationHelps:
|
||||
"Break the cycle of repetitive keystrokes and allow your hands to recover.",
|
||||
},
|
||||
{
|
||||
name: "Tendinitis",
|
||||
description: "Inflammation or irritation of a tendon, often affecting the wrist and fingers.",
|
||||
howDictationHelps:
|
||||
"Reduce tendon stress by speaking instead of typing for extended periods.",
|
||||
},
|
||||
{
|
||||
name: "Tennis Elbow (Lateral Epicondylitis)",
|
||||
description: "Overuse injury causing pain on the outside of the elbow and forearm.",
|
||||
howDictationHelps:
|
||||
"Minimize the gripping and wrist motions that exacerbate elbow pain.",
|
||||
},
|
||||
{
|
||||
name: "Thoracic Outlet Syndrome",
|
||||
description:
|
||||
"Compression of nerves or blood vessels in the lower neck and upper chest.",
|
||||
howDictationHelps:
|
||||
"Reduce the forward-head posture and shoulder tension common with keyboard use.",
|
||||
},
|
||||
{
|
||||
name: "General Mobility Limitations",
|
||||
description:
|
||||
"Conditions affecting hand, arm, or finger movement from various causes.",
|
||||
howDictationHelps:
|
||||
"Full computer control without physical keyboard interaction required.",
|
||||
},
|
||||
];
|
||||
|
||||
const statistics = [
|
||||
{ value: "8+", label: "Hours per day average computer use for office workers" },
|
||||
{ value: "50%", label: "Of computer users experience RSI symptoms" },
|
||||
{ value: "100+", label: "Words per minute achievable with voice dictation" },
|
||||
{ value: "Local", label: "Engines run on your machine unless you enable Remote API" },
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Voice Dictation for RSI & Carpal Tunnel Prevention | Vocalinux",
|
||||
description:
|
||||
"Prevent and manage RSI, carpal tunnel, and repetitive strain injuries with voice dictation. Hands-free typing for Linux users with local engines or an optional Remote API.",
|
||||
path: "/rsi-prevention",
|
||||
keywords: [
|
||||
"RSI voice dictation",
|
||||
"carpal tunnel voice typing",
|
||||
"hands-free typing",
|
||||
"repetitive strain injury prevention",
|
||||
"voice typing for wrist pain",
|
||||
"dictation software for RSI",
|
||||
"ergonomic typing alternative",
|
||||
"Linux voice dictation accessibility",
|
||||
],
|
||||
});
|
||||
|
||||
export default function RsiPreventionPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Voice Dictation for RSI & Carpal Tunnel Prevention",
|
||||
description:
|
||||
"How voice dictation helps prevent and manage repetitive strain injuries. Hands-free typing solution for Linux users.",
|
||||
dateModified: "2026-02-22",
|
||||
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("/rsi-prevention"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-border bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<Heart className="h-4 w-4" />
|
||||
Health & Accessibility
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Voice Dictation for RSI Prevention
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Suffering from carpal tunnel, wrist pain, or repetitive strain? Voice dictation lets you
|
||||
keep working without the physical toll. Local engines run on your Linux machine. Remote
|
||||
API is optional.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{statistics.map((stat) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
className="rounded-[12px] border border-border bg-background p-5 text-center"
|
||||
>
|
||||
<p className="mb-1 font-display text-3xl font-semibold text-primary">{stat.value}</p>
|
||||
<p className="text-sm text-muted-foreground">{stat.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Why Voice Dictation Helps</h2>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
{benefits.map((benefit) => {
|
||||
const Icon = benefit.icon;
|
||||
return (
|
||||
<article
|
||||
key={benefit.title}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className={`rounded-lg p-2.5 ${benefit.iconBg}`}>
|
||||
<Icon className={`h-5 w-5 ${benefit.iconColor}`} />
|
||||
</div>
|
||||
<h3 className="font-semibold">{benefit.title}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{benefit.description}</p>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Conditions Helped by Voice Dictation</h2>
|
||||
<div className="space-y-4">
|
||||
{conditions.map((condition) => (
|
||||
<article
|
||||
key={condition.name}
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<h3 className="mb-2 font-semibold">{condition.name}</h3>
|
||||
<p className="mb-3 text-sm text-muted-foreground">{condition.description}</p>
|
||||
<div className="rounded-lg bg-primary/10 p-3">
|
||||
<p className="text-sm text-primary">
|
||||
<strong>How dictation helps:</strong> {condition.howDictationHelps}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</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">Getting Started with Hands-Free Typing</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-3 font-semibold">Tips for Transitioning</h3>
|
||||
<ul className="space-y-2 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" />
|
||||
Start by dictating emails and documents, then expand to other tasks
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Use voice commands for punctuation to maintain natural speech flow
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Take breaks - voice dictation is still work, and vocal rest matters too
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Combine with ergonomic equipment for maximum benefit
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-3 font-semibold">Vocalinux Advantages</h3>
|
||||
<ul className="space-y-2 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" />
|
||||
Toggle mode or push-to-talk control - no mouse required
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
System tray indicator shows recording status
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Pleasant audio feedback confirms start/stop
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Works in ALL Linux applications - universal compatibility
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[12px] border border-border bg-primary/10 p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold text-primary">
|
||||
Take Care of Your Hands
|
||||
</h2>
|
||||
<p className="mb-6 text-primary">
|
||||
Your health matters. If you're experiencing pain, numbness, or tingling, consult a
|
||||
healthcare professional. Voice dictation is a tool to help, not a substitute for proper
|
||||
medical care.
|
||||
</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" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/use-cases/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold text-primary hover:bg-primary/10"
|
||||
>
|
||||
See Other Use Cases
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { Camera, ChevronRight, Download } from "lucide-react";
|
||||
import {
|
||||
ScreenshotGallery,
|
||||
type Screenshot,
|
||||
} from "@/components/screenshot-gallery";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
/** Pixel size of the captured settings/log dialog screenshots. */
|
||||
const SETTINGS_SIZE = { width: 2128, height: 1698 } as const;
|
||||
|
||||
const productShots: Screenshot[] = [
|
||||
{
|
||||
src: "/screenshots/00-transcription.png",
|
||||
srcDark: "/screenshots/dark/00-transcription.png",
|
||||
alt: "Vocalinux dictating into a text editor with the tray menu open",
|
||||
title: "Dictation in action",
|
||||
description:
|
||||
"Voice typing into a focused app while the tray menu shows listening state.",
|
||||
width: 2518,
|
||||
height: 2057,
|
||||
},
|
||||
{
|
||||
src: "/screenshots/02-system-tray.png",
|
||||
srcDark: "/screenshots/dark/02-system-tray.png",
|
||||
alt: "Vocalinux system tray menu with voice typing controls",
|
||||
title: "System tray",
|
||||
description: "Tray menu for start/stop, settings, logs, and about.",
|
||||
width: 315,
|
||||
height: 466,
|
||||
},
|
||||
{
|
||||
src: "/screenshots/03-log-viewer.png",
|
||||
srcDark: "/screenshots/dark/03-log-viewer.png",
|
||||
alt: "Vocalinux log viewer dialog",
|
||||
title: "Log viewer",
|
||||
description: "Inspect runtime logs when diagnosing dictation issues.",
|
||||
...SETTINGS_SIZE,
|
||||
},
|
||||
];
|
||||
|
||||
const settingsShots: Screenshot[] = [
|
||||
{
|
||||
src: "/screenshots/settings-speech-engine.png",
|
||||
srcDark: "/screenshots/dark/settings-speech-engine.png",
|
||||
alt: "Vocalinux Speech Model settings with sidebar navigation",
|
||||
title: "Speech Model",
|
||||
description:
|
||||
"Choose whisper.cpp, Whisper, VOSK, or Remote API and pick a model.",
|
||||
...SETTINGS_SIZE,
|
||||
},
|
||||
{
|
||||
src: "/screenshots/settings-recognition.png",
|
||||
srcDark: "/screenshots/dark/settings-recognition.png",
|
||||
alt: "Vocalinux Dictation settings page",
|
||||
title: "Dictation",
|
||||
description: "Shortcuts, listening controls, and dictation output options.",
|
||||
...SETTINGS_SIZE,
|
||||
},
|
||||
{
|
||||
src: "/screenshots/settings-audio.png",
|
||||
srcDark: "/screenshots/dark/settings-audio.png",
|
||||
alt: "Vocalinux Audio settings page",
|
||||
title: "Audio",
|
||||
description: "Input device, feedback sounds, and capture options.",
|
||||
...SETTINGS_SIZE,
|
||||
},
|
||||
{
|
||||
src: "/screenshots/settings-performance.png",
|
||||
srcDark: "/screenshots/dark/settings-performance.png",
|
||||
alt: "Vocalinux Performance settings page",
|
||||
title: "Performance",
|
||||
description: "Auto-pause, model keep-alive, and GPU device selection.",
|
||||
...SETTINGS_SIZE,
|
||||
},
|
||||
{
|
||||
src: "/screenshots/settings-general.png",
|
||||
srcDark: "/screenshots/dark/settings-general.png",
|
||||
alt: "Vocalinux Application settings page",
|
||||
title: "Application",
|
||||
description: "Autostart, start minimized, and everyday defaults.",
|
||||
...SETTINGS_SIZE,
|
||||
},
|
||||
{
|
||||
src: "/screenshots/settings-advanced.png",
|
||||
srcDark: "/screenshots/dark/settings-advanced.png",
|
||||
alt: "Vocalinux Advanced settings with whisper.cpp decoding controls",
|
||||
title: "Advanced",
|
||||
description: "Power-user decoding controls and Remote Server options.",
|
||||
...SETTINGS_SIZE,
|
||||
},
|
||||
{
|
||||
src: "/screenshots/05-about-view.png",
|
||||
srcDark: "/screenshots/dark/05-about-view.png",
|
||||
alt: "Vocalinux About settings page with update checker",
|
||||
title: "About",
|
||||
description:
|
||||
"Version info plus Stable/Nightly update checks from GitHub Releases.",
|
||||
...SETTINGS_SIZE,
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Screenshots: Linux Voice Dictation App UI",
|
||||
description:
|
||||
"See Vocalinux in action: system tray, log viewer, and settings pages for speech model, dictation, audio, performance, application, advanced options, and about/updates. Flip between light and dark app shots.",
|
||||
path: "/screenshots",
|
||||
keywords: [
|
||||
"Vocalinux screenshots",
|
||||
"Linux voice dictation UI",
|
||||
"offline speech recognition settings",
|
||||
"whisper.cpp Linux screenshots",
|
||||
"voice typing system tray Linux",
|
||||
],
|
||||
});
|
||||
|
||||
export default function ScreenshotsPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ImageGallery",
|
||||
name: "Vocalinux Screenshots",
|
||||
description:
|
||||
"Product and settings screenshots of Vocalinux, offline voice dictation for Linux.",
|
||||
dateModified: "2026-08-03", // refreshed after About moved into Settings sidebar
|
||||
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("/screenshots"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="border-primary/30 bg-primary/10 mb-4 inline-flex items-center gap-2 rounded-full border px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<Camera className="h-4 w-4" />
|
||||
v0.16 UI
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Vocalinux Screenshots
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
A look at offline voice dictation on Linux: tray controls, debugging
|
||||
tools, and the searchable sidebar settings UI from v0.16. Flip light
|
||||
and dark app shots. Click any image to expand it.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<ScreenshotGallery
|
||||
productShots={productShots}
|
||||
settingsShots={settingsShots}
|
||||
/>
|
||||
|
||||
<section className="border-primary/20 bg-primary/5 rounded-[12px] border p-8">
|
||||
<h2 className="mb-3 font-display text-2xl font-semibold">Try it yourself</h2>
|
||||
<p className="mb-6 max-w-2xl text-muted-foreground">
|
||||
Install Vocalinux and open Settings from the system tray to explore
|
||||
the same screens on your desktop.
|
||||
</p>
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-[12px] bg-primary px-5 py-3 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Install guide
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/advanced-settings/"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-[12px] border border-border bg-background px-5 py-3 text-sm font-semibold transition-colors hover:bg-muted"
|
||||
>
|
||||
Advanced settings guide
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { ChevronRight, Keyboard, Mouse, Settings, Volume2, Zap } from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const shortcutCategories = [
|
||||
{
|
||||
category: "Dictation Controls",
|
||||
icon: Mic,
|
||||
iconColor: "text-primary",
|
||||
shortcuts: [
|
||||
{
|
||||
keys: ["Hold Right Alt"],
|
||||
action: "Push-to-talk (default): speak while held",
|
||||
note: "Release to stop · Option on Mac-layout keyboards",
|
||||
},
|
||||
{
|
||||
keys: ["Double-tap key"],
|
||||
action: "Toggle mode: start/stop dictation",
|
||||
note: "Optional — switch in Settings",
|
||||
},
|
||||
{
|
||||
keys: ["Settings"],
|
||||
action: "Choose Left/Right Ctrl, Alt, or Shift in Settings",
|
||||
note: "Modifier key side selection",
|
||||
},
|
||||
{
|
||||
keys: ["PTT"],
|
||||
action: "v0.10.1+ includes fixes for premature transcription during PTT silence",
|
||||
note: "Improved push-to-talk reliability",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Voice Commands",
|
||||
icon: Zap,
|
||||
iconColor: "text-primary",
|
||||
shortcuts: [
|
||||
{ keys: ["\"new line\""], action: "Insert a new line", note: "Say this phrase" },
|
||||
{ keys: ["\"new paragraph\""], action: "Insert a new paragraph", note: "Say this phrase" },
|
||||
{ keys: ["\"delete that\""], action: "Delete last phrase", note: "Also: \"scratch that\"" },
|
||||
{ keys: ["\"select all\""], action: "Select all text", note: "Voice command" },
|
||||
{ keys: ["\"copy this\""], action: "Copy selection", note: "Voice command" },
|
||||
{ keys: ["\"paste here\""], action: "Paste clipboard", note: "Voice command" },
|
||||
{ keys: ["\"undo\""], action: "Undo last action", note: "Voice command" },
|
||||
{ keys: ["\"redo\""], action: "Redo last action", note: "Voice command" },
|
||||
{ keys: ["Settings Toggle"], action: "Enable/disable voice commands", note: "Optional in v0.8.0-beta" },
|
||||
{
|
||||
keys: ["Voice Commands"],
|
||||
action: "Voice commands are optional since v0.8.0 - auto-enabled for VOSK, disabled for Whisper by default",
|
||||
note: "Engine-specific defaults",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Audio Feedback",
|
||||
icon: Volume2,
|
||||
iconColor: "text-primary",
|
||||
shortcuts: [
|
||||
{
|
||||
keys: ["Settings Toggle"],
|
||||
action: "Enable/disable sound effects from Settings dialog",
|
||||
note: "Audio feedback control",
|
||||
},
|
||||
{
|
||||
keys: ["v0.9.0+"],
|
||||
action: "Sound effects now toggleable - smooth gliding tones for start/stop",
|
||||
note: "Pleasant audio cues",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Tray Icon Actions",
|
||||
icon: Mouse,
|
||||
iconColor: "text-primary",
|
||||
shortcuts: [
|
||||
{ keys: ["Left Click"], action: "Toggle dictation", note: "On tray icon" },
|
||||
{ keys: ["Right Click"], action: "Open context menu", note: "Settings, quit, etc." },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Configuration",
|
||||
icon: Settings,
|
||||
iconColor: "text-primary",
|
||||
shortcuts: [
|
||||
{ keys: ["Config File"], action: "~/.config/vocalinux/config.json", note: "Manual editing" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function Mic(props: { className?: string }) {
|
||||
return (
|
||||
<svg className={props.className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
||||
<line x1="12" x2="12" y1="19" y2="22" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Keyboard Shortcuts and Voice Commands",
|
||||
description:
|
||||
"Learn Vocalinux keyboard shortcut modes and voice commands. Use toggle mode or push-to-talk, then customize activation and command behavior.",
|
||||
path: "/shortcuts",
|
||||
keywords: [
|
||||
"vocalinux shortcuts",
|
||||
"linux voice dictation commands",
|
||||
"speech to text keyboard shortcuts",
|
||||
"voice commands linux",
|
||||
],
|
||||
});
|
||||
|
||||
export default function ShortcutsPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Vocalinux Keyboard Shortcuts & Voice Commands",
|
||||
description:
|
||||
"Complete guide to Vocalinux keyboard shortcuts and voice commands for efficient Linux voice dictation.",
|
||||
dateModified: "2026-03-30",
|
||||
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("/shortcuts"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="subpage-kicker">
|
||||
<Keyboard className="h-4 w-4" />
|
||||
Quick Reference
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Keyboard Shortcuts & Voice Commands
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Master Vocalinux with keyboard shortcuts and voice commands. New installs default to
|
||||
push-to-talk (hold Right Alt), or switch to toggle mode (double-tap), then apply voice
|
||||
commands for editing and formatting.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Quick Start */}
|
||||
<section className="mb-12 rounded-[12px] border border-primary/20 bg-primary/5 p-6">
|
||||
<h2 className="mb-4 text-xl font-semibold">Quick Start</h2>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<kbd className="rounded-lg border border-border bg-muted px-3 py-1.5 font-mono text-sm font-semibold">
|
||||
Hold Right Alt
|
||||
</kbd>
|
||||
</div>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="font-medium">Push-to-Talk: Speak While Held</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
Right Alt is the Option key on Mac-layout keyboards. Toggle mode is also available:
|
||||
double-tap your configured key to start/stop. You can switch modes and keys in Settings.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Voice commands can be enabled or disabled from Settings depending on your workflow.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Shortcut Categories */}
|
||||
<section className="space-y-8">
|
||||
{shortcutCategories.map((category) => {
|
||||
const Icon = category.icon;
|
||||
return (
|
||||
<article
|
||||
key={category.category}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<h2 className="mb-6 flex items-center gap-3 font-display text-2xl font-semibold">
|
||||
<span className={`rounded-[12px] bg-muted p-2 bg-muted`}>
|
||||
<Icon className={`h-5 w-5 ${category.iconColor}`} />
|
||||
</span>
|
||||
{category.category}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{category.shortcuts.map((shortcut, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-wrap items-center justify-between gap-4 rounded-[12px] bg-muted p-4"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{shortcut.keys.map((key, keyIndex) => (
|
||||
<span key={keyIndex} className="flex items-center gap-2">
|
||||
<kbd className="rounded-lg border border-border bg-muted px-3 py-1.5 font-mono text-sm font-semibold">
|
||||
{key}
|
||||
</kbd>
|
||||
{keyIndex < shortcut.keys.length - 1 && (
|
||||
<span className="text-muted-foreground">+</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">{shortcut.action}</p>
|
||||
<p className="text-sm text-muted-foreground">{shortcut.note}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
{/* Customization */}
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Customizing Shortcuts</h2>
|
||||
<p className="mb-4 text-muted-foreground">
|
||||
You can customize the activation shortcut in the settings panel or by editing the
|
||||
configuration file directly:
|
||||
</p>
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-4">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">~/.config/vocalinux/config.json</code>
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
Open Vocalinux and access Settings from the tray icon to change keyboard shortcuts
|
||||
through the GUI.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Related Pages */}
|
||||
<section className="mt-12 grid gap-6 md:grid-cols-2">
|
||||
<Link
|
||||
href="/faq/"
|
||||
className="group rounded-[12px] border border-border bg-background p-6 transition-all"
|
||||
>
|
||||
<h3 className="mb-2 text-xl font-semibold">Frequently Asked Questions</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Get answers to common questions about Vocalinux features and usage.
|
||||
</p>
|
||||
<span className="mt-4 inline-flex items-center gap-1 text-sm font-semibold text-primary">
|
||||
View FAQ
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/troubleshooting/"
|
||||
className="group rounded-[12px] border border-border bg-background p-6 transition-all"
|
||||
>
|
||||
<h3 className="mb-2 text-xl font-semibold">Troubleshooting</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Having issues? Find solutions to common problems and fixes.
|
||||
</p>
|
||||
<span className="mt-4 inline-flex items-center gap-1 text-sm font-semibold text-primary">
|
||||
Get Help
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
</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" />
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">
|
||||
For recordings, not live dictation
|
||||
</h2>
|
||||
<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" />
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Speaker turns, English only</h2>
|
||||
<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" />
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Local processing</h2>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
ExternalLink,
|
||||
Lightbulb,
|
||||
Terminal,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const troubleshootingItems = [
|
||||
{
|
||||
title: "Vocalinux won't start",
|
||||
symptoms: [
|
||||
"Command not found",
|
||||
"Module import errors",
|
||||
"Application crashes on launch",
|
||||
],
|
||||
solutions: [
|
||||
"Ensure you ran the installer from the homepage or install guide",
|
||||
"Check if the virtual environment is activated: source ~/.local/share/vocalinux/venv/bin/activate",
|
||||
"Verify Python version (3.11+ required): python3 --version",
|
||||
"Try reinstalling: ./uninstall.sh && ./install.sh",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "No audio detected / microphone not working",
|
||||
symptoms: [
|
||||
"Dictation shows no text",
|
||||
"Audio level indicator stays at zero",
|
||||
],
|
||||
solutions: [
|
||||
"Check microphone permissions in your system settings",
|
||||
"Verify microphone is detected: arecord -l (Linux)",
|
||||
"Test microphone recording: arecord -d 3 test.wav && aplay test.wav",
|
||||
"Ensure no other application is using the microphone",
|
||||
"Check PulseAudio/PipeWire settings: pavucontrol",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Text not appearing in applications",
|
||||
symptoms: [
|
||||
"Dictation works but text doesn't appear",
|
||||
"Text injection fails",
|
||||
],
|
||||
solutions: [
|
||||
"For Wayland: Ensure IBus is running and Vocalinux IBus is configured",
|
||||
"For X11: Verify xdotool is installed: sudo apt install xdotool",
|
||||
"Check if the target application has focus",
|
||||
"Try a different application (terminal, text editor) to isolate the issue",
|
||||
"Restart Vocalinux after changing display servers",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "High CPU/GPU usage",
|
||||
symptoms: [
|
||||
"System slowdown during dictation",
|
||||
"Fan noise",
|
||||
"Laggy response",
|
||||
],
|
||||
solutions: [
|
||||
"Use a smaller model (tiny or base instead of medium/large)",
|
||||
"Enable GPU acceleration if available (Vulkan for AMD/Intel, CUDA for NVIDIA)",
|
||||
"Switch to VOSK engine for lower resource usage",
|
||||
"Reduce audio sample rate in settings",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Poor transcription accuracy",
|
||||
symptoms: ["Many transcription errors", "Wrong words", "Garbled text"],
|
||||
solutions: [
|
||||
"Use a larger model (small → medium → large) for better accuracy",
|
||||
"Ensure microphone is positioned correctly and not too far",
|
||||
"Reduce background noise",
|
||||
"Speak clearly and at a moderate pace",
|
||||
"Try a different speech engine (whisper.cpp vs VOSK)",
|
||||
"Set the correct language in settings",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Remote API transcription fails",
|
||||
symptoms: [
|
||||
"Connection test fails",
|
||||
"HTTP 401/403 errors",
|
||||
"HTTP 404 on transcription",
|
||||
],
|
||||
solutions: [
|
||||
"Confirm the server URL is reachable from the Vocalinux machine",
|
||||
"Check whether the server expects /inference or /v1/audio/transcriptions",
|
||||
"Set the API key if your server requires bearer token authentication",
|
||||
"Use HTTPS and a firewall when the server is outside a trusted LAN",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Silero VAD is not active",
|
||||
symptoms: [
|
||||
"Recognition tab shows amplitude VAD",
|
||||
"Silence-only buffers still appear",
|
||||
],
|
||||
solutions: [
|
||||
'Install neural VAD support: pip install "vocalinux[vad]"',
|
||||
"Restart Vocalinux after installing ONNX Runtime support",
|
||||
"Use the VAD sensitivity setting in Recognition to tune quiet speech detection",
|
||||
"If Silero cannot load, Vocalinux falls back safely to amplitude-based VAD",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Installation fails",
|
||||
symptoms: ["Dependency errors", "Package not found", "Permission denied"],
|
||||
solutions: [
|
||||
"Ensure you have internet connectivity for downloading dependencies",
|
||||
"Run with sufficient permissions (don't use sudo unless specifically needed)",
|
||||
"Check your distribution is supported (Ubuntu 24.04+, Debian 12+, Fedora 39+, Arch)",
|
||||
"Install system dependencies manually: sudo apt install python3-pip python3-gi python3-venv",
|
||||
"Check disk space: df -h",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Keyboard shortcut not working",
|
||||
symptoms: [
|
||||
"Shortcut mode doesn't start dictation",
|
||||
"Custom shortcuts ignored",
|
||||
],
|
||||
solutions: [
|
||||
"Check if another application is capturing the shortcut",
|
||||
"Verify Vocalinux is running (check system tray)",
|
||||
"Try changing the shortcut in Settings",
|
||||
"On Wayland, ensure keyboard shortcuts are not blocked by compositor settings",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Vocalinux stops working after system suspend/resume",
|
||||
symptoms: [
|
||||
"Dictation doesn't start after laptop wakes",
|
||||
"Keyboard shortcuts stop working",
|
||||
"App appears running but not responding",
|
||||
],
|
||||
solutions: [
|
||||
"v0.10.1+ automatically recovers after suspend - ensure you're on latest version",
|
||||
"Check system tray icon status after resume",
|
||||
"If issues persist, restart Vocalinux from tray menu",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Tray icon not appearing",
|
||||
symptoms: ["No system tray icon", "Cannot access settings"],
|
||||
solutions: [
|
||||
"v0.10.1+ bundles resources to prevent missing tray icons - ensure you're on latest version",
|
||||
"Ensure your desktop environment supports system trays (GNOME requires an AppIndicator extension)",
|
||||
"Install the GNOME AppIndicator extension (pick your distro): Debian/Ubuntu: sudo apt install gnome-shell-extension-appindicator — Fedora: sudo dnf install gnome-shell-extension-appindicator — Arch: sudo pacman -S gnome-shell-extension-appindicator",
|
||||
"Then enable the extension (Extensions app, or: gnome-extensions enable appindicatorsupport@rgcjonas.gmail.com) and log out/in if needed",
|
||||
"Debian/Ubuntu: sudo apt install libayatana-appindicator3-1 — Fedora: sudo dnf install libayatana-appindicator-gtk3 — Arch: sudo pacman -S libayatana-appindicator (legacy libappindicator-gtk3 can leave the icon missing on KDE)",
|
||||
"Try launching from terminal to see any errors: vocalinux --debug",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Settings dialog missing close button",
|
||||
symptoms: ["Cannot close settings window", "Close button not visible"],
|
||||
solutions: [
|
||||
"v0.10.1+ includes dedicated close button - update to latest version",
|
||||
"Settings dialog now forces window decorations",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Linux Voice Dictation Troubleshooting Guide",
|
||||
description:
|
||||
"Fix common Vocalinux issues: microphone problems, text injection, installation errors, and performance. Step-by-step solutions for Linux voice dictation.",
|
||||
path: "/troubleshooting",
|
||||
keywords: [
|
||||
"vocalinux troubleshooting",
|
||||
"linux voice dictation problems",
|
||||
"speech to text not working",
|
||||
"microphone issues linux",
|
||||
],
|
||||
});
|
||||
|
||||
export default function TroubleshootingPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Vocalinux Troubleshooting Guide",
|
||||
description:
|
||||
"Complete troubleshooting guide for Vocalinux Linux voice dictation software.",
|
||||
dateModified: "2026-06-11",
|
||||
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("/troubleshooting"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-border bg-muted px-4 py-1.5 text-sm font-medium text-muted-foreground">
|
||||
<Wrench className="h-4 w-4" />
|
||||
Support Guide
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Troubleshooting Guide
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Having issues with Vocalinux? Find solutions to common problems below.
|
||||
Can't find your issue? Open a GitHub issue for help.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-xl font-semibold">
|
||||
<Terminal className="h-5 w-5 text-primary" />
|
||||
Quick Debug Command
|
||||
</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Run Vocalinux with debug output to see detailed logs:
|
||||
</p>
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-4">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">vocalinux --debug</code>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-6">
|
||||
{troubleshootingItems.map((item) => (
|
||||
<article
|
||||
key={item.title}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<h2 className="mb-4 text-xl font-semibold">{item.title}</h2>
|
||||
|
||||
<div className="mb-4 rounded-lg bg-muted p-4">
|
||||
<p className="mb-2 flex items-center gap-2 text-sm font-semibold text-muted-foreground">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
Symptoms
|
||||
</p>
|
||||
<ul className="ml-6 list-disc text-sm text-muted-foreground">
|
||||
{item.symptoms.map((symptom) => (
|
||||
<li key={symptom} className="break-words">
|
||||
{symptom}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-primary/10 p-4">
|
||||
<p className="mb-2 flex items-center gap-2 text-sm font-semibold text-primary">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
Solutions
|
||||
</p>
|
||||
<ul className="ml-6 list-disc text-sm text-primary">
|
||||
{item.solutions.map((solution) => (
|
||||
<li key={solution} className="break-words">
|
||||
{solution}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Lightbulb className="h-5 w-5 text-primary" />
|
||||
Still having issues?
|
||||
</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
Check the{" "}
|
||||
<Link
|
||||
href="/faq/"
|
||||
className="font-semibold text-primary hover:underline"
|
||||
>
|
||||
FAQ page
|
||||
</Link>{" "}
|
||||
for common questions
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
Review{" "}
|
||||
<Link
|
||||
href="/remote-api/"
|
||||
className="font-semibold text-primary hover:underline"
|
||||
>
|
||||
Remote API setup
|
||||
</Link>{" "}
|
||||
or{" "}
|
||||
<Link
|
||||
href="/voice-activity-detection/"
|
||||
className="font-semibold text-primary hover:underline"
|
||||
>
|
||||
Silero VAD behavior
|
||||
</Link>{" "}
|
||||
for newer recognition features
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<a
|
||||
href="https://github.com/VocaHQ/vocalinux/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 font-semibold text-primary hover:underline"
|
||||
>
|
||||
Search existing issues on GitHub
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<a
|
||||
href="https://github.com/VocaHQ/vocalinux/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 font-semibold text-primary hover:underline"
|
||||
>
|
||||
Open a new issue (include debug logs)
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { Accessibility, BookOpen, Briefcase, ChevronRight, Code, Feather, GraduationCap, Heart, Users } from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const useCases = [
|
||||
{
|
||||
title: "Developers",
|
||||
icon: Code,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
description: "Code faster with voice. Dictate comments, documentation, and even code snippets hands-free.",
|
||||
benefits: [
|
||||
"Dictate code comments while keeping hands on the keyboard",
|
||||
"Write documentation at the speed of thought",
|
||||
"Reduce RSI from constant typing",
|
||||
"Multitask: review code while dictating notes",
|
||||
],
|
||||
commands: ["new line", "open parenthesis", "close parenthesis", "tab"],
|
||||
},
|
||||
{
|
||||
title: "Writers & Authors",
|
||||
icon: Feather,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
description: "Capture ideas as fast as you can think them. Perfect for drafts, articles, and creative writing.",
|
||||
benefits: [
|
||||
"Write at natural speaking speed (100+ WPM)",
|
||||
"Overcome writer's block by speaking freely",
|
||||
"Maintain creative flow without keyboard interruptions",
|
||||
"Dictate while walking or pacing",
|
||||
],
|
||||
commands: ["new paragraph", "quote", "delete that", "undo"],
|
||||
},
|
||||
{
|
||||
title: "Students",
|
||||
icon: GraduationCap,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
description: "Take notes during lectures, transcribe recordings, and write essays faster.",
|
||||
benefits: [
|
||||
"Take lecture notes at speaking speed",
|
||||
"Transcribe audio recordings for study",
|
||||
"Write essays and papers hands-free",
|
||||
"Multi-language support for language learners",
|
||||
],
|
||||
commands: ["select all", "copy", "paste", "new line"],
|
||||
},
|
||||
{
|
||||
title: "Accessibility Users",
|
||||
icon: Accessibility,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
description: "Full voice control for users with mobility impairments, RSI, or carpal tunnel.",
|
||||
benefits: [
|
||||
"Type without using hands",
|
||||
"Reduce strain from repetitive typing",
|
||||
"Independence for users with physical disabilities",
|
||||
"Local engines, no required cloud account",
|
||||
],
|
||||
commands: ["select all", "delete that", "undo", "redo"],
|
||||
},
|
||||
{
|
||||
title: "Professionals",
|
||||
icon: Briefcase,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
description: "Dictate emails, reports, and meeting notes. Boost productivity in any profession.",
|
||||
benefits: [
|
||||
"Draft emails quickly with natural speech",
|
||||
"Create meeting minutes in real-time",
|
||||
"Write reports hands-free",
|
||||
"Local engines keep audio on the machine",
|
||||
],
|
||||
commands: ["new paragraph", "comma", "period", "send"],
|
||||
},
|
||||
{
|
||||
title: "Content Creators",
|
||||
icon: Heart,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
description: "Create YouTube scripts, podcast notes, and social media content at speaking speed.",
|
||||
benefits: [
|
||||
"Write scripts as fast as you can speak",
|
||||
"Transcribe video/audio content",
|
||||
"Draft social media posts hands-free",
|
||||
"Create captions and subtitles",
|
||||
],
|
||||
commands: ["new line", "quote", "colon", "hashtag"],
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Use Cases for Linux Voice Dictation",
|
||||
description:
|
||||
"Discover how developers, writers, students, and professionals use Vocalinux for Linux voice dictation. Find your use case and boost productivity.",
|
||||
path: "/use-cases",
|
||||
keywords: [
|
||||
"voice dictation for developers",
|
||||
"speech to text for writers",
|
||||
"linux accessibility voice typing",
|
||||
"dictation software for students",
|
||||
],
|
||||
});
|
||||
|
||||
export default function UseCasesPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Vocalinux Use Cases - Voice Dictation for Every Workflow",
|
||||
description:
|
||||
"Discover how different professionals use Vocalinux for Linux voice dictation.",
|
||||
dateModified: "2026-02-19",
|
||||
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("/use-cases"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="subpage-kicker">
|
||||
<Users className="h-4 w-4" />
|
||||
For Everyone
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Voice Dictation for Every Workflow
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Whether you're coding, writing, studying, or need accessibility support, Vocalinux adapts
|
||||
to your needs. See how different professionals use voice dictation to boost productivity.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-8">
|
||||
{useCases.map((useCase) => {
|
||||
const Icon = useCase.icon;
|
||||
return (
|
||||
<article
|
||||
key={useCase.title}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<div className="mb-6 flex flex-wrap items-start gap-4">
|
||||
<div className={`rounded-[12px] p-3 ${useCase.iconBg}`}>
|
||||
<Icon className={`h-6 w-6 ${useCase.iconColor}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="mb-2 font-display text-2xl font-semibold">{useCase.title}</h2>
|
||||
<p className="text-muted-foreground">{useCase.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-3 font-semibold">Key Benefits</h3>
|
||||
<ul className="space-y-2">
|
||||
{useCase.benefits.map((benefit) => (
|
||||
<li key={benefit} className="flex items-start gap-2 text-sm text-muted-foreground">
|
||||
<ChevronRight className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
{benefit}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 font-semibold">Helpful Voice Commands</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{useCase.commands.map((cmd) => (
|
||||
<kbd
|
||||
key={cmd}
|
||||
className="rounded-lg border border-border bg-muted px-2 py-1 text-xs font-mono"
|
||||
>
|
||||
"{cmd}"
|
||||
</kbd>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Ready to Get Started?</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Install Vocalinux in minutes and start dictating in any application on Linux.
|
||||
</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 Now
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/shortcuts/"
|
||||
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 hover:bg-muted"
|
||||
>
|
||||
View Voice Commands
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
Activity,
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Cpu,
|
||||
Mic,
|
||||
Settings,
|
||||
Shield,
|
||||
Volume2,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const vadBenefits = [
|
||||
{
|
||||
title: "Drops silence-only buffers",
|
||||
description:
|
||||
"Silero VAD helps Vocalinux ignore chunks that do not contain speech before they reach the recognition engine.",
|
||||
},
|
||||
{
|
||||
title: "Cleaner dictation sessions",
|
||||
description:
|
||||
"Better speech boundaries reduce empty transcriptions and lower the chance that silence becomes stray text.",
|
||||
},
|
||||
{
|
||||
title: "Same sensitivity control",
|
||||
description:
|
||||
"The existing 1-5 VAD sensitivity setting works for both Silero and the amplitude fallback.",
|
||||
},
|
||||
];
|
||||
|
||||
const signalFlow = [
|
||||
"Microphone audio is captured locally.",
|
||||
"Vocalinux checks each short chunk for speech activity.",
|
||||
"Speech chunks are kept for transcription.",
|
||||
"Silence-only chunks are dropped before recognition.",
|
||||
"If Silero is unavailable, amplitude-based VAD takes over.",
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Silero VAD for Cleaner Linux Voice Dictation",
|
||||
description:
|
||||
"Learn how Vocalinux uses Silero neural voice activity detection to drop silence-only buffers and improve Linux speech-to-text dictation.",
|
||||
path: "/voice-activity-detection",
|
||||
keywords: [
|
||||
"silero vad linux dictation",
|
||||
"voice activity detection speech to text",
|
||||
"vocalinux vad sensitivity",
|
||||
"cleaner whisper dictation linux",
|
||||
],
|
||||
});
|
||||
|
||||
export default function VoiceActivityDetectionPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "TechArticle",
|
||||
headline: "Silero VAD for Cleaner Linux Voice Dictation",
|
||||
description:
|
||||
"How Vocalinux uses neural voice activity detection to separate speech from silence before transcription.",
|
||||
dateModified: "2026-06-11",
|
||||
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("/voice-activity-detection"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="border-primary/30 bg-primary/10 mb-4 inline-flex items-center gap-2 rounded-full border px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<Activity className="h-4 w-4" />
|
||||
v0.12.0 Silero VAD
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Silero VAD for Cleaner Linux Voice Dictation
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Vocalinux now uses Silero neural voice activity detection when ONNX
|
||||
Runtime support is available. It identifies speech before
|
||||
transcription, drops silence-only buffers, and falls back safely when
|
||||
the neural backend is not installed.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 md:grid-cols-3">
|
||||
{vadBenefits.map((benefit) => (
|
||||
<article
|
||||
key={benefit.title}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<span className="mb-4 inline-flex rounded-[12px] bg-primary/10 p-3">
|
||||
<CheckCircle2 className="h-6 w-6 text-primary" />
|
||||
</span>
|
||||
<h2 className="mb-2 text-xl font-semibold">{benefit.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{benefit.description}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 lg:grid-cols-[1.05fr_1fr]">
|
||||
<div className="rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Volume2 className="h-5 w-5 text-primary" />
|
||||
How Speech Detection Fits In
|
||||
</h2>
|
||||
<ol className="space-y-3 text-sm text-muted-foreground">
|
||||
{signalFlow.map((step, index) => (
|
||||
<li key={step} className="flex gap-3">
|
||||
<span className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-primary text-xs font-semibold text-primary-foreground">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span>{step}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Settings className="h-5 w-5 text-primary" />
|
||||
Settings and Installation
|
||||
</h2>
|
||||
<div className="space-y-4 text-sm text-muted-foreground">
|
||||
<p>
|
||||
The official installer attempts to install neural VAD support. If
|
||||
ONNX Runtime is not available, Vocalinux logs the fallback and
|
||||
continues with amplitude-based VAD.
|
||||
</p>
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-4">
|
||||
<code className="text-[color:var(--terminal-fg)]">
|
||||
pip install "vocalinux[vad]"
|
||||
</code>
|
||||
</div>
|
||||
<p>
|
||||
The Recognition tab shows which backend is active. Higher VAD
|
||||
sensitivity values are more responsive to quiet speech for both
|
||||
backends.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 md:grid-cols-3">
|
||||
{[
|
||||
{
|
||||
icon: Shield,
|
||||
title: "Local by design",
|
||||
description:
|
||||
"Silero runs on your machine. Voice activity detection does not require a cloud service.",
|
||||
},
|
||||
{
|
||||
icon: Cpu,
|
||||
title: "Small CPU-side helper",
|
||||
description:
|
||||
"The VAD step is a lightweight pre-filter before whisper.cpp, Whisper, VOSK, or Remote API recognition.",
|
||||
},
|
||||
{
|
||||
icon: Mic,
|
||||
title: "Useful for push-to-talk",
|
||||
description:
|
||||
"Cleaner speech boundaries also help short recordings and stop-on-release flows avoid silence-only output.",
|
||||
},
|
||||
].map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<article
|
||||
key={item.title}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<span className="bg-primary/10 mb-4 inline-flex rounded-[12px] p-3">
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</span>
|
||||
<h2 className="mb-2 text-xl font-semibold">{item.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{item.description}
|
||||
</p>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="border-primary/20 bg-primary/5 rounded-[12px] border p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Related Guides</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Link href="/shortcuts/" className="group">
|
||||
<h3 className="font-semibold">Shortcut modes</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Pair VAD with toggle or push-to-talk activation.
|
||||
</p>
|
||||
<span className="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
View shortcuts <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
<Link href="/advanced-settings/" className="group">
|
||||
<h3 className="font-semibold">Advanced recognition tuning</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Adjust whisper.cpp thresholds and anti-hallucination controls.
|
||||
</p>
|
||||
<span className="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
Tune settings <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
<Link href="/compare/" className="group">
|
||||
<h3 className="font-semibold">Engine comparison</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
See how VAD works before local and remote recognition engines.
|
||||
</p>
|
||||
<span className="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
Compare engines <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
Bug,
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Code2,
|
||||
Keyboard,
|
||||
Lightbulb,
|
||||
MessageSquare,
|
||||
Mic,
|
||||
ToggleLeft,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const setupSteps = [
|
||||
{
|
||||
title: "Install Vocalinux",
|
||||
description:
|
||||
"Follow the install guide, launch Vocalinux, and confirm your microphone and language are configured.",
|
||||
href: "/install/",
|
||||
linkLabel: "Open installation guide",
|
||||
icon: CheckCircle2,
|
||||
},
|
||||
{
|
||||
title: "Configure your activation shortcut",
|
||||
description:
|
||||
"Pick a shortcut that does not conflict with VS Code keybindings. Most developers keep Ctrl-based activation for muscle-memory.",
|
||||
href: "/shortcuts/",
|
||||
linkLabel: "View shortcuts and commands",
|
||||
icon: Keyboard,
|
||||
},
|
||||
];
|
||||
|
||||
const workflows = [
|
||||
{
|
||||
title: "Toggle mode for quick dictation",
|
||||
description:
|
||||
"Use toggle mode when you need short bursts: comments, TODO notes, commit message drafts, or quick variable descriptions.",
|
||||
tips: [
|
||||
"Double-tap your shortcut to start and stop.",
|
||||
"Best for short phrases between normal typing.",
|
||||
"Great when context-switching between keyboard and voice.",
|
||||
],
|
||||
icon: ToggleLeft,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Push-to-talk for continuous coding (recommended)",
|
||||
description:
|
||||
"Use push-to-talk when writing longer explanations in docs, code reviews, and comments. Hold key to speak, release to stop.",
|
||||
tips: [
|
||||
"Keeps accidental transcription low in noisy environments.",
|
||||
"Predictable start/stop behavior for focused coding sessions.",
|
||||
"Preferred by many developers for precise control.",
|
||||
],
|
||||
icon: Mic,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Voice commands for code editing",
|
||||
description:
|
||||
"Combine dictation with editing commands like punctuation and structure helpers to move faster through repetitive formatting.",
|
||||
tips: [
|
||||
"Use commands like \"new line\", \"tab\", and \"semicolon\".",
|
||||
"Treat voice as an editor assistant, not a full code generator.",
|
||||
"Use keyboard for dense syntax-heavy sections.",
|
||||
],
|
||||
icon: MessageSquare,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
];
|
||||
|
||||
const vscodeTips = [
|
||||
"Dictate comments first: inline comments and docstrings are ideal voice targets.",
|
||||
"For strings, speak full sentence chunks and then fix quotes/escaping with keyboard.",
|
||||
"Use \"new line\" and \"tab\" commands for indentation rhythm in blocks.",
|
||||
"When writing markdown in VS Code, voice dictation is often faster than typing.",
|
||||
];
|
||||
|
||||
const troubleshootingTips = [
|
||||
"No text appears in VS Code: verify Vocalinux is active and the editor window is focused.",
|
||||
"Shortcut conflicts: remap Vocalinux activation to avoid overlapping VS Code keybinds.",
|
||||
"Unexpected punctuation: review your voice command phrases and disable commands if needed.",
|
||||
"Low accuracy: use a quieter mic setup and test another model size/engine in settings.",
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "How to Dictate in VS Code on Linux (Voice Coding Workflow)",
|
||||
description:
|
||||
"Learn how to use voice dictation in VS Code on Linux. Setup Vocalinux for programming, recommended workflows for coding, PTT vs toggle mode, and troubleshooting tips.",
|
||||
path: "/voice-typing-vscode",
|
||||
keywords: [
|
||||
"voice typing VS Code Linux",
|
||||
"voice coding linux",
|
||||
"dictate code linux",
|
||||
"speech to text programming linux",
|
||||
"vocalinux VS Code",
|
||||
],
|
||||
});
|
||||
|
||||
export default function VoiceTypingVsCodePage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "How to Dictate in VS Code on Linux (Voice Coding Workflow)",
|
||||
description:
|
||||
"Learn how to use voice dictation in VS Code on Linux. Setup Vocalinux for programming, recommended workflows for coding, PTT vs toggle mode, and troubleshooting tips.",
|
||||
dateModified: "2026-03-30",
|
||||
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("/voice-typing-vscode"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="subpage-kicker">
|
||||
<Code2 className="h-4 w-4" />
|
||||
VS Code Workflow
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
How to Dictate in VS Code on Linux (Voice Coding Workflow)
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Learn how to use Vocalinux for practical, low-friction voice coding in VS Code on Linux.
|
||||
This guide covers setup, toggle vs push-to-talk, editing workflows, and common fixes.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Lightbulb className="h-5 w-5 text-primary" />
|
||||
Why use voice coding in VS Code?
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Voice dictation helps developers reduce repetitive typing, keep momentum during documentation,
|
||||
and lower hand strain. For most workflows, the sweet spot is hybrid: keyboard for syntax,
|
||||
voice for comments, explanations, notes, and refactors written in plain language.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Setup in 2 steps</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{setupSteps.map((step) => {
|
||||
const Icon = step.icon;
|
||||
return (
|
||||
<article
|
||||
key={step.title}
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<div className="mb-3 inline-flex rounded-lg bg-primary/10 p-2">
|
||||
<Icon className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<h3 className="mb-2 font-semibold">{step.title}</h3>
|
||||
<p className="mb-3 text-sm text-muted-foreground">{step.description}</p>
|
||||
<Link href={step.href} className="text-sm font-semibold text-primary hover:underline">
|
||||
{step.linkLabel}
|
||||
</Link>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Recommended workflows</h2>
|
||||
<div className="space-y-4">
|
||||
{workflows.map((workflow) => {
|
||||
const Icon = workflow.icon;
|
||||
|
||||
return (
|
||||
<article
|
||||
key={workflow.title}
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<h3 className="mb-2 flex items-center gap-3 text-lg font-semibold">
|
||||
<span className={`rounded-lg p-2 ${workflow.iconBg}`}>
|
||||
<Icon className={`h-4 w-4 ${workflow.iconColor}`} />
|
||||
</span>
|
||||
{workflow.title}
|
||||
</h3>
|
||||
<p className="mb-3 text-sm text-muted-foreground">{workflow.description}</p>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
{workflow.tips.map((tip) => (
|
||||
<li key={tip} className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
|
||||
<span>{tip}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">VS Code-specific tips</h2>
|
||||
<ul className="space-y-3 text-sm text-muted-foreground">
|
||||
{vscodeTips.map((tip) => (
|
||||
<li key={tip} className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
|
||||
<span>{tip}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold text-muted-foreground">
|
||||
<Bug className="h-5 w-5" />
|
||||
Troubleshooting
|
||||
</h2>
|
||||
<ul className="space-y-3 text-sm text-muted-foreground">
|
||||
{troubleshootingTips.map((tip) => (
|
||||
<li key={tip} className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>{tip}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-5 flex flex-wrap gap-4 text-sm">
|
||||
<Link href="/for-developers/" className="font-semibold text-primary hover:underline">
|
||||
Developer guide
|
||||
</Link>
|
||||
<Link href="/shortcuts/" className="font-semibold text-primary hover:underline">
|
||||
Shortcuts and commands
|
||||
</Link>
|
||||
<Link href="/install/" className="font-semibold text-primary hover:underline">
|
||||
Installation docs
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[12px] border border-primary/20 bg-primary/5 p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Ready to voice-type in VS Code?</h2>
|
||||
<p className="mb-6 max-w-3xl text-muted-foreground">
|
||||
Install Vocalinux and use a hybrid keyboard + voice workflow that fits real-world Linux
|
||||
development.
|
||||
</p>
|
||||
<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" />
|
||||
</Link>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Command,
|
||||
Cpu,
|
||||
Gauge,
|
||||
Headphones,
|
||||
PanelTop,
|
||||
Trophy,
|
||||
Waypoints,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const comparisonRows = [
|
||||
{
|
||||
feature: "Interface and daily workflow",
|
||||
vocalinux: "Full GTK GUI + tray controls + keyboard shortcuts",
|
||||
nerdDictation: "CLI-first terminal workflow",
|
||||
winner: "Vocalinux",
|
||||
icon: PanelTop,
|
||||
},
|
||||
{
|
||||
feature: "Offline voice dictation",
|
||||
vocalinux: "Local engines, plus an optional Remote API",
|
||||
nerdDictation: "Offline dictation, single CLI-centered flow",
|
||||
winner: "Vocalinux",
|
||||
icon: Command,
|
||||
},
|
||||
{
|
||||
feature: "Wayland text injection",
|
||||
vocalinux: "Native IBus integration for dictation in any app",
|
||||
nerdDictation: "IBus-based text injection on Wayland",
|
||||
winner: "Vocalinux",
|
||||
icon: Waypoints,
|
||||
},
|
||||
{
|
||||
feature: "GPU acceleration",
|
||||
vocalinux: "Vulkan acceleration across AMD, Intel, and NVIDIA",
|
||||
nerdDictation: "CPU-focused workflow",
|
||||
winner: "Vocalinux",
|
||||
icon: Cpu,
|
||||
},
|
||||
{
|
||||
feature: "Push-to-talk",
|
||||
vocalinux: "Built-in push-to-talk and toggle mode",
|
||||
nerdDictation: "Terminal-first trigger workflow",
|
||||
winner: "Vocalinux",
|
||||
icon: Gauge,
|
||||
},
|
||||
{
|
||||
feature: "Audio feedback",
|
||||
vocalinux: "Built-in pleasant sound effects for start/stop/error",
|
||||
nerdDictation: "No native desktop sound-feedback layer",
|
||||
winner: "Vocalinux",
|
||||
icon: Headphones,
|
||||
},
|
||||
{
|
||||
feature: "System tray integration",
|
||||
vocalinux: "Tray icon with status, controls, and settings access",
|
||||
nerdDictation: "No native system tray app",
|
||||
winner: "Vocalinux",
|
||||
icon: Trophy,
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Nerd Dictation Alternative for Linux | Vocalinux",
|
||||
description:
|
||||
"Comparing Vocalinux and Nerd Dictation for offline Linux voice dictation. CLI vs GUI, GPU vs CPU-only, Wayland support, and more.",
|
||||
path: "/vs-nerd-dictation",
|
||||
keywords: [
|
||||
"vocalinux vs nerd dictation",
|
||||
"linux voice dictation comparison",
|
||||
"offline speech to text linux",
|
||||
],
|
||||
});
|
||||
|
||||
export default function VsNerdDictationPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Vocalinux vs Nerd Dictation: Best Offline Voice Dictation for Linux",
|
||||
description:
|
||||
"Comparing Vocalinux and Nerd Dictation for offline Linux voice dictation. CLI vs GUI, GPU vs CPU-only, Wayland support, and more.",
|
||||
dateModified: "2026-03-30",
|
||||
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("/vs-nerd-dictation"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="subpage-kicker">
|
||||
<Trophy className="h-4 w-4" />
|
||||
Vocalinux Comparison
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Vocalinux vs Nerd Dictation
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Nerd Dictation is a popular offline CLI tool for Linux. But if you want a complete desktop
|
||||
dictation experience with GUI controls, tray integration, push-to-talk, and broad GPU support,
|
||||
Vocalinux is the better choice.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="overflow-x-auto rounded-[12px] border border-border bg-background">
|
||||
<h2 className="px-4 pb-2 pt-5 font-display text-2xl font-semibold">Feature-by-feature comparison</h2>
|
||||
<table className="w-full border-collapse text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="px-4 py-3 text-sm font-semibold">Feature</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Vocalinux</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Nerd Dictation</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Winner</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{comparisonRows.map((row) => {
|
||||
const Icon = row.icon;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={row.feature}
|
||||
className="border-b border-border align-top"
|
||||
>
|
||||
<td className="px-4 py-4 font-semibold">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="rounded-md bg-primary/10 p-1.5">
|
||||
<Icon className="h-4 w-4 text-primary" />
|
||||
</span>
|
||||
{row.feature}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.vocalinux}</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.nerdDictation}</td>
|
||||
<td className="px-4 py-4 text-sm font-semibold text-primary">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
{row.winner}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section className="mt-10 rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Go deeper</h2>
|
||||
<div className="grid gap-3 text-sm text-muted-foreground sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Link href="/offline/" className="font-semibold text-primary hover:underline">
|
||||
Why offline dictation matters
|
||||
</Link>
|
||||
<Link href="/gpu-acceleration/" className="font-semibold text-primary hover:underline">
|
||||
Vulkan GPU acceleration details
|
||||
</Link>
|
||||
<Link href="/wayland/" className="font-semibold text-primary hover:underline">
|
||||
Wayland + IBus integration guide
|
||||
</Link>
|
||||
<Link href="/compare/" className="font-semibold text-primary hover:underline">
|
||||
Engine comparison (whisper.cpp vs Whisper vs VOSK)
|
||||
</Link>
|
||||
<Link href="/for-developers/" className="font-semibold text-primary hover:underline">
|
||||
Developer docs and architecture
|
||||
</Link>
|
||||
<Link href="/rsi-prevention/" className="font-semibold text-primary hover:underline">
|
||||
RSI prevention with voice typing
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-10 rounded-[12px] border border-primary/20 bg-primary/5 p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Ready to switch from terminal-only dictation?</h2>
|
||||
<p className="mb-6 max-w-3xl text-muted-foreground">
|
||||
Install Vocalinux for a native Linux voice dictation workflow with offline privacy, better
|
||||
usability, and stronger performance options.
|
||||
</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" />
|
||||
</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 hover:bg-muted"
|
||||
>
|
||||
Engine comparison
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Keyboard,
|
||||
Monitor,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const waylandFeatures = [
|
||||
{
|
||||
title: "Native IBus Integration",
|
||||
description:
|
||||
"Text injection via IBus includes readiness checks, scoped activation, and runtime recovery for modern Wayland sessions.",
|
||||
icon: Terminal,
|
||||
},
|
||||
{
|
||||
title: "Keyboard Layout Preservation",
|
||||
description:
|
||||
"v0.10.1+ preserves your XKB keyboard layout when activating IBus. No more switching to US layout unexpectedly.",
|
||||
icon: Keyboard,
|
||||
},
|
||||
{
|
||||
title: "X11 Fallback Support",
|
||||
description:
|
||||
"Automatic fallback to xdotool when running under XWayland or traditional X11 sessions.",
|
||||
icon: Monitor,
|
||||
},
|
||||
{
|
||||
title: "Desktop Agnostic",
|
||||
description:
|
||||
"Full support on GNOME and KDE Plasma, good support on sway, and experimental support on Hyprland.",
|
||||
icon: Zap,
|
||||
},
|
||||
{
|
||||
title: "Reliability Hardening",
|
||||
description:
|
||||
"Recent betas improve IBus startup, keyboard layout preservation, non-ASCII fallback, and suspend/resume behavior.",
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
];
|
||||
|
||||
const compositorSupport = [
|
||||
{
|
||||
name: "GNOME (mutter)",
|
||||
method: "IBus",
|
||||
status: "Full support",
|
||||
notes: "Default on Ubuntu, Fedora, Debian GNOME editions",
|
||||
},
|
||||
{
|
||||
name: "KDE Plasma (KWin)",
|
||||
method: "IBus",
|
||||
status: "Full support",
|
||||
notes: "Works with IBus input method framework",
|
||||
},
|
||||
{
|
||||
name: "Sway",
|
||||
method: "IBus / wtype",
|
||||
status: "Good support",
|
||||
notes: "i3-compatible Wayland compositor",
|
||||
},
|
||||
{
|
||||
name: "Hyprland",
|
||||
method: "IBus / wtype",
|
||||
status: "Experimental",
|
||||
notes: "May need wtype configuration; see the Hyprland and sway guide",
|
||||
},
|
||||
{
|
||||
name: "river",
|
||||
method: "IBus / wtype",
|
||||
status: "Untested",
|
||||
notes: "Dynamic tiling Wayland compositor",
|
||||
},
|
||||
{
|
||||
name: "XWayland apps",
|
||||
method: "xdotool",
|
||||
status: "Full support",
|
||||
notes: "X11 apps running under Wayland",
|
||||
},
|
||||
];
|
||||
|
||||
const setupSteps = [
|
||||
{
|
||||
step: 1,
|
||||
title: "Install IBus",
|
||||
description: "Most distributions include IBus by default. If not:",
|
||||
command: "sudo apt install ibus ibus-gtk ibus-gtk3",
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: "Start IBus Daemon",
|
||||
description: "Ensure the IBus daemon is running in your session:",
|
||||
command: "ibus-daemon -drx",
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Run Vocalinux",
|
||||
description: "Vocalinux will auto-detect Wayland and configure IBus:",
|
||||
command: "vocalinux",
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Wayland Voice Dictation Support - GNOME, KDE, Sway | Vocalinux",
|
||||
description:
|
||||
"Full Wayland support for voice dictation on Linux. Works with GNOME, KDE Plasma, Sway, Hyprland, and other Wayland compositors via IBus integration.",
|
||||
path: "/wayland",
|
||||
keywords: [
|
||||
"wayland voice dictation",
|
||||
"GNOME voice typing",
|
||||
"KDE speech to text",
|
||||
"Sway voice recognition",
|
||||
"Hyprland dictation",
|
||||
"wayland text injection",
|
||||
"linux wayland speech recognition",
|
||||
"ibus voice input",
|
||||
],
|
||||
});
|
||||
|
||||
export default function WaylandPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Wayland Voice Dictation Support for Linux",
|
||||
description:
|
||||
"Complete guide to Vocalinux Wayland support. Voice dictation for GNOME, KDE, Sway, Hyprland, and other Wayland compositors.",
|
||||
dateModified: "2026-06-11",
|
||||
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("/wayland"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="border-primary/30 bg-primary/10 mb-4 inline-flex items-center gap-2 rounded-full border px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<Monitor className="h-4 w-4" />
|
||||
Wayland Support
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Voice Dictation on Wayland
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Full voice dictation support for modern Wayland compositors. Works
|
||||
with GNOME, KDE Plasma, Sway, Hyprland, and more. No X11 required.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
{waylandFeatures.map((feature) => {
|
||||
const Icon = feature.icon;
|
||||
return (
|
||||
<div
|
||||
key={feature.title}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<Icon className="mb-3 h-8 w-8 text-primary" />
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">{feature.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">
|
||||
Supported Wayland Compositors
|
||||
</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="pb-3 pr-4 text-sm font-semibold">Compositor</th>
|
||||
<th className="pb-3 pr-4 text-sm font-semibold">Method</th>
|
||||
<th className="pb-3 pr-4 text-sm font-semibold">Status</th>
|
||||
<th className="pb-3 text-sm font-semibold">Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-sm">
|
||||
{compositorSupport.map((item) => (
|
||||
<tr
|
||||
key={item.name}
|
||||
className="border-b border-border"
|
||||
>
|
||||
<td className="py-3 pr-4 font-medium">{item.name}</td>
|
||||
<td className="py-3 pr-4 text-muted-foreground">
|
||||
{item.method}
|
||||
</td>
|
||||
<td className="py-3 pr-4">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
{item.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 text-muted-foreground">{item.notes}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Quick Setup Guide</h2>
|
||||
<div className="space-y-4">
|
||||
{setupSteps.map((step) => (
|
||||
<div
|
||||
key={step.step}
|
||||
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">
|
||||
{step.step}
|
||||
</span>
|
||||
<h3 className="font-semibold">{step.title}</h3>
|
||||
</div>
|
||||
<p className="mb-3 ml-10 text-sm text-muted-foreground">
|
||||
{step.description}
|
||||
</p>
|
||||
<div className="ml-10 rounded-lg bg-[color:var(--terminal)] p-3">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">{step.command}</code>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
If text doesn't appear, ensure IBus is your active input method
|
||||
(System Settings → Keyboard → Input Methods).
|
||||
</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">
|
||||
How Wayland Text Injection Works
|
||||
</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Unlike X11 where applications can simulate keyboard input globally,
|
||||
Wayland's security model requires a different approach. Vocalinux
|
||||
uses:
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<strong>IBus Engine</strong> - A custom IBus input method that
|
||||
receives text from Vocalinux and types it into the focused
|
||||
application
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<strong>wtype</strong> - Native Wayland typing tool for compositors
|
||||
that support the virtual keyboard protocol
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<strong>X11 Fallback</strong> - xdotool for XWayland applications
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
v0.9.0+ includes clipboard fallback for Wayland compositors without
|
||||
virtual keyboard support. Copy-to-clipboard is now disabled by default
|
||||
for privacy - enable in Settings if needed.
|
||||
</p>
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
v0.10.2+ also improves IBus detection without legacy environment
|
||||
variables and handles non-ASCII ydotool paths with clipboard paste
|
||||
fallback.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Start Dictating on Wayland</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Vocalinux automatically detects your display server and configures the
|
||||
appropriate text injection method. Just install and run.
|
||||
</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 Now
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</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 hover:bg-muted"
|
||||
>
|
||||
Troubleshooting Guide
|
||||
</Link>
|
||||
<Link
|
||||
href="/desktop-reliability/"
|
||||
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 hover:bg-muted"
|
||||
>
|
||||
Reliability Improvements
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
Cpu,
|
||||
Gauge,
|
||||
Rocket,
|
||||
Sparkles,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const modelComparison = [
|
||||
{
|
||||
model: "Tiny",
|
||||
size: "~74MB",
|
||||
ramCpu: "~0.8-1.2GB",
|
||||
ramGpu: "~0.5-0.8GB VRAM",
|
||||
accuracy: "Good",
|
||||
speed: "Fastest",
|
||||
bestFor: "Older laptops, lowest latency, quick notes",
|
||||
},
|
||||
{
|
||||
model: "Base",
|
||||
size: "~141MB",
|
||||
ramCpu: "~1.2-1.8GB",
|
||||
ramGpu: "~0.8-1.2GB VRAM",
|
||||
accuracy: "Good+",
|
||||
speed: "Very fast",
|
||||
bestFor: "Balanced real-time dictation on most systems",
|
||||
},
|
||||
{
|
||||
model: "Small",
|
||||
size: "~465MB",
|
||||
ramCpu: "~2-3GB",
|
||||
ramGpu: "~1.5-2.5GB VRAM",
|
||||
accuracy: "High",
|
||||
speed: "Fast",
|
||||
bestFor: "Most users wanting stronger punctuation and names",
|
||||
},
|
||||
{
|
||||
model: "Medium",
|
||||
size: "~1.5GB",
|
||||
ramCpu: "~4-6GB",
|
||||
ramGpu: "~3-5GB VRAM",
|
||||
accuracy: "Very high",
|
||||
speed: "Moderate",
|
||||
bestFor: "Accuracy-focused dictation, technical writing",
|
||||
},
|
||||
{
|
||||
model: "Large",
|
||||
size: "~3.0GB",
|
||||
ramCpu: "~8GB+",
|
||||
ramGpu: "~6-10GB VRAM",
|
||||
accuracy: "Best",
|
||||
speed: "Slowest",
|
||||
bestFor: "Maximum quality with strong GPU hardware",
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Whisper Model Sizes: Tiny to Large for Linux Dictation",
|
||||
description:
|
||||
"Complete guide to Whisper model sizes for Linux voice dictation. Compare tiny vs base vs small vs medium vs large models - download size, RAM usage, accuracy, GPU requirements, and speed.",
|
||||
path: "/whisper-model-guide",
|
||||
keywords: [
|
||||
"whisper model sizes",
|
||||
"whisper tiny vs base",
|
||||
"which whisper model",
|
||||
"whisper.cpp models",
|
||||
"linux dictation model selection",
|
||||
],
|
||||
});
|
||||
|
||||
export default function WhisperModelGuidePage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Whisper Model Sizes Explained: Tiny, Base, Small, Medium, Large for Linux",
|
||||
description:
|
||||
"Complete guide to Whisper model sizes for Linux voice dictation. Compare tiny vs base vs small vs medium vs large models for size, memory, speed, and accuracy.",
|
||||
dateModified: "2026-03-30",
|
||||
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("/whisper-model-guide"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="subpage-kicker">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Whisper Model Guide
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Whisper Model Sizes Explained: Tiny, Base, Small, Medium, Large for Linux
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Picking the right model is the biggest performance decision for Linux voice dictation.
|
||||
Use this guide to choose between tiny, base, small, medium, and large based on RAM,
|
||||
speed, and real-world transcription accuracy in Vocalinux.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="overflow-x-auto rounded-[12px] border border-border bg-background">
|
||||
<table className="w-full border-collapse text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="px-4 py-3 text-sm font-semibold">Model</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Size</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">RAM (CPU)</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">RAM (GPU)</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Accuracy</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Speed</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Best For</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{modelComparison.map((row) => (
|
||||
<tr
|
||||
key={row.model}
|
||||
className="border-b border-border align-top"
|
||||
>
|
||||
<td className="px-4 py-4 font-semibold">{row.model}</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.size}</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.ramCpu}</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.ramGpu}</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.accuracy}</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.speed}</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.bestFor}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section className="mt-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Which model should you choose?</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h3 className="mb-3 inline-flex items-center gap-2 text-xl font-semibold">
|
||||
<Zap className="h-5 w-5 text-primary" />
|
||||
Tiny or Base
|
||||
</h3>
|
||||
<ul className="space-y-2 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" />
|
||||
Best if you care about low latency over absolute transcription quality.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Great for older CPUs, battery-conscious laptops, and quick chat replies.
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h3 className="mb-3 inline-flex items-center gap-2 text-xl font-semibold">
|
||||
<Gauge className="h-5 w-5 text-primary" />
|
||||
Small (recommended default)
|
||||
</h3>
|
||||
<ul className="space-y-2 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" />
|
||||
Best balance for most Linux dictation workflows.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Better punctuation and proper nouns while staying near real-time.
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h3 className="mb-3 inline-flex items-center gap-2 text-xl font-semibold">
|
||||
<Cpu className="h-5 w-5 text-primary" />
|
||||
Medium
|
||||
</h3>
|
||||
<ul className="space-y-2 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" />
|
||||
Use when accuracy is more important than response time.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Ideal for technical dictation, multilingual speech, and noisy environments.
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h3 className="mb-3 inline-flex items-center gap-2 text-xl font-semibold">
|
||||
<Rocket className="h-5 w-5 text-primary" />
|
||||
Large
|
||||
</h3>
|
||||
<ul className="space-y-2 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" />
|
||||
Choose only if you have strong GPU hardware and want maximum quality.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Best for polished long-form writing where errors are costly.
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-12 grid gap-6 md:grid-cols-3">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-xl font-semibold">Need more speed?</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Enable GPU acceleration to run larger models faster, especially medium and large.
|
||||
</p>
|
||||
<Link href="/gpu-acceleration/" className="inline-flex items-center gap-1 text-primary hover:underline">
|
||||
GPU acceleration guide <ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-xl font-semibold">Comparing engines?</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
See whisper.cpp vs Whisper vs VOSK before deciding your full dictation stack.
|
||||
</p>
|
||||
<Link href="/compare/" className="inline-flex items-center gap-1 text-primary hover:underline">
|
||||
Engine comparison <ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-xl font-semibold">Ready to install?</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Use the install guide to get Vocalinux running with the model size that fits your hardware.
|
||||
</p>
|
||||
<Link href="/install/" className="inline-flex items-center gap-1 text-primary hover:underline">
|
||||
Installation guide <ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-[12px] border border-primary/30 bg-primary/10 p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Install Vocalinux and start dictating today</h2>
|
||||
<p className="mb-6 max-w-3xl text-muted-foreground">
|
||||
Start with <strong>small</strong> if unsure, then move up or down after a day of use.
|
||||
Vocalinux makes model changes simple, so you can tune for your exact speed/accuracy
|
||||
preference.
|
||||
</p>
|
||||
<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
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import {
|
||||
BookOpen,
|
||||
ChevronRight,
|
||||
FileText,
|
||||
Globe,
|
||||
Heart,
|
||||
Lock,
|
||||
Mic,
|
||||
PenLine,
|
||||
ToggleLeft,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const writingModes = [
|
||||
{
|
||||
title: "Toggle mode for natural writing flow",
|
||||
description:
|
||||
"Use toggle mode for long-form drafting. Start dictation, speak continuously, and stay in creative flow while writing chapters, essays, and long notes.",
|
||||
icon: ToggleLeft,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Push-to-talk for structured writing",
|
||||
description:
|
||||
"Use push-to-talk when precision matters: outlines, headlines, bullet lists, or source notes. Hold to speak, release to review and edit.",
|
||||
icon: Mic,
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
];
|
||||
|
||||
const writingCommands = [
|
||||
"new line",
|
||||
"new paragraph",
|
||||
"period",
|
||||
"comma",
|
||||
"question mark",
|
||||
"colon",
|
||||
"quote",
|
||||
"delete that",
|
||||
"undo",
|
||||
"capitalize",
|
||||
];
|
||||
|
||||
const integrations = [
|
||||
{
|
||||
app: "Obsidian",
|
||||
description:
|
||||
"Capture ideas directly into your vault, journal entries, or daily notes without breaking thought flow.",
|
||||
},
|
||||
{
|
||||
app: "Notion",
|
||||
description:
|
||||
"Draft blog outlines, content calendars, and research notes with hands-free voice input.",
|
||||
},
|
||||
{
|
||||
app: "LibreOffice",
|
||||
description:
|
||||
"Dictate manuscripts, essays, and article drafts in Writer with standard punctuation commands.",
|
||||
},
|
||||
{
|
||||
app: "Terminal Editors",
|
||||
description:
|
||||
"Use Vocalinux with terminal apps and editors to write notes, commit messages, and drafts quickly.",
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Voice Dictation for Writers on Linux",
|
||||
description:
|
||||
"Complete guide to voice dictation for writers on Linux. How to dictate novels, blog posts, and notes using Vocalinux. Hands-free writing workflow tips.",
|
||||
path: "/writers",
|
||||
keywords: [
|
||||
"voice dictation for writers linux",
|
||||
"dictate novel linux",
|
||||
"voice typing obsidian linux",
|
||||
"voice to text writing linux",
|
||||
"linux dictation for authors",
|
||||
],
|
||||
});
|
||||
|
||||
export default function WritersPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Voice Dictation for Writers on Linux: Dictate Novels, Articles & Notes",
|
||||
description:
|
||||
"Complete guide to voice dictation for writers on Linux. How to dictate novels, blog posts, and notes using Vocalinux. Hands-free writing workflow tips.",
|
||||
dateModified: "2026-03-30",
|
||||
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("/writers"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="subpage-kicker">
|
||||
<PenLine className="h-4 w-4" />
|
||||
For Writers
|
||||
</p>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Voice Dictation for Writers on Linux: Dictate Novels, Articles & Notes
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Write at the speed of thought. Vocalinux helps novelists, bloggers, students, and
|
||||
journalists dictate drafts hands-free in their favorite Linux apps.
|
||||
</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">Why Writers Should Use Voice Dictation</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-3 inline-flex items-center gap-2 font-semibold">
|
||||
<Heart className="h-5 w-5 text-primary" /> RSI prevention and comfort
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Long writing sessions can strain wrists and fingers. Dictating reduces repetitive
|
||||
keyboard stress and helps you keep creating without pain.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-3 inline-flex items-center gap-2 font-semibold">
|
||||
<FileText className="h-5 w-5 text-primary" /> More words, less friction
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Most people speak faster than they type. Voice dictation helps produce first drafts,
|
||||
lecture notes, and article outlines faster while preserving momentum.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Recommended Workflow for Writers</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{writingModes.map((mode) => {
|
||||
const Icon = mode.icon;
|
||||
return (
|
||||
<article
|
||||
key={mode.title}
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<div className={`mb-3 inline-flex rounded-lg p-2.5 ${mode.iconBg}`}>
|
||||
<Icon className={`h-5 w-5 ${mode.iconColor}`} />
|
||||
</div>
|
||||
<h3 className="mb-2 font-semibold">{mode.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{mode.description}</p>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</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">Voice Commands Useful for Writing</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
These commands help maintain writing rhythm while dictating punctuation and formatting.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{writingCommands.map((cmd) => (
|
||||
<kbd
|
||||
key={cmd}
|
||||
className="rounded-lg border border-border bg-muted px-2 py-1 text-xs font-mono"
|
||||
>
|
||||
"{cmd}"
|
||||
</kbd>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">App Integrations for Writing Workflows</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{integrations.map((integration) => (
|
||||
<article
|
||||
key={integration.app}
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<h3 className="mb-2 font-semibold">{integration.app}</h3>
|
||||
<p className="text-sm text-muted-foreground">{integration.description}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Link
|
||||
href="/languages/"
|
||||
className="rounded-[12px] border border-border bg-background p-5 transition-colors hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
<Globe className="mb-3 h-6 w-6 text-primary" />
|
||||
<h3 className="mb-1 font-semibold">Multilingual Writing</h3>
|
||||
<p className="text-sm text-muted-foreground">See supported languages for dictation.</p>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/rsi-prevention/"
|
||||
className="rounded-[12px] border border-border bg-background p-5 transition-colors hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
<Heart className="mb-3 h-6 w-6 text-primary" />
|
||||
<h3 className="mb-1 font-semibold">Health Benefits</h3>
|
||||
<p className="text-sm text-muted-foreground">Learn how dictation helps prevent RSI.</p>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/offline/"
|
||||
className="rounded-[12px] border border-border bg-background p-5 transition-colors hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
<Lock className="mb-3 h-6 w-6 text-primary" />
|
||||
<h3 className="mb-1 font-semibold">Privacy Benefits</h3>
|
||||
<p className="text-sm text-muted-foreground">Keep drafts private with offline dictation.</p>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/use-cases/"
|
||||
className="rounded-[12px] border border-border bg-background p-5 transition-colors hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
<BookOpen className="mb-3 h-6 w-6 text-primary" />
|
||||
<h3 className="mb-1 font-semibold">More Use Cases</h3>
|
||||
<p className="text-sm text-muted-foreground">Explore all workflows powered by Vocalinux.</p>
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[12px] border border-primary/30 bg-primary/10 p-8">
|
||||
<h2 className="mb-3 font-display text-2xl font-semibold">Start Writing with Your Voice</h2>
|
||||
<p className="mb-6 max-w-3xl text-muted-foreground">
|
||||
Install Vocalinux and start dictating novels, blog posts, academic notes, and newsroom
|
||||
drafts directly on Linux - without cloud lock-in.
|
||||
</p>
|
||||
<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" />
|
||||
</Link>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { DidYouMean, suggestPage } from '../not-found-extras';
|
||||
|
||||
const pages = ['/install/', '/install/ubuntu/', '/install/fedora/', '/faq/', '/compare/'];
|
||||
|
||||
describe('suggestPage', () => {
|
||||
it('suggests the closest page for a typo', () => {
|
||||
expect(suggestPage('/install/ubunt', pages)).toBe('/install/ubuntu/');
|
||||
expect(suggestPage('/INSTALL/FEDORA', pages)).toBe('/install/fedora/');
|
||||
expect(suggestPage('/compar', pages)).toBe('/compare/');
|
||||
});
|
||||
|
||||
it('suggests nothing for an unrelated path', () => {
|
||||
expect(suggestPage('/wp-admin/setup-config.php', pages)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DidYouMean', () => {
|
||||
afterEach(() => {
|
||||
window.history.pushState({}, '', '/');
|
||||
});
|
||||
|
||||
it('links to the closest page for the current URL', async () => {
|
||||
window.history.pushState({}, '', '/install/ubunt');
|
||||
render(<DidYouMean />);
|
||||
// Jest's next/link ignores trailingSlash, so check the visible path.
|
||||
expect(await screen.findByRole('link')).toHaveTextContent('/install/ubuntu/');
|
||||
});
|
||||
|
||||
it('does not crash on a malformed percent-escape', () => {
|
||||
window.history.pushState({}, '', '/%FF/');
|
||||
expect(() => render(<DidYouMean />)).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { OptimizedImage, VocalinuxLogo } from '../optimized-image';
|
||||
|
||||
// Mock Next.js Image component
|
||||
jest.mock('next/image', () => ({
|
||||
__esModule: true,
|
||||
default: (props: any) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { priority, ...imgProps } = props;
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
return <img {...imgProps} alt={props.alt} />;
|
||||
},
|
||||
}));
|
||||
|
||||
describe('OptimizedImage', () => {
|
||||
const defaultProps = {
|
||||
src: '/test-image.png',
|
||||
alt: 'Test Image',
|
||||
width: 100,
|
||||
height: 100,
|
||||
};
|
||||
|
||||
it('renders without crashing', () => {
|
||||
render(<OptimizedImage {...defaultProps} />);
|
||||
const img = screen.getByAltText('Test Image');
|
||||
expect(img).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a picture element with WebP source', () => {
|
||||
const { container } = render(<OptimizedImage {...defaultProps} />);
|
||||
const picture = container.querySelector('picture');
|
||||
expect(picture).toBeInTheDocument();
|
||||
|
||||
const source = container.querySelector('source');
|
||||
expect(source).toBeInTheDocument();
|
||||
expect(source).toHaveAttribute('srcset', '/test-image.webp');
|
||||
expect(source).toHaveAttribute('type', 'image/webp');
|
||||
});
|
||||
|
||||
it('provides PNG fallback for browsers without WebP support', () => {
|
||||
render(<OptimizedImage {...defaultProps} />);
|
||||
const img = screen.getByAltText('Test Image');
|
||||
expect(img).toHaveAttribute('src', '/test-image.png');
|
||||
});
|
||||
|
||||
it('converts jpg to webp in source srcset', () => {
|
||||
const { container } = render(
|
||||
<OptimizedImage {...defaultProps} src="/image.jpg" />
|
||||
);
|
||||
const source = container.querySelector('source');
|
||||
expect(source).toHaveAttribute('srcset', '/image.webp');
|
||||
});
|
||||
|
||||
it('converts jpeg to webp in source srcset', () => {
|
||||
const { container } = render(
|
||||
<OptimizedImage {...defaultProps} src="/image.jpeg" />
|
||||
);
|
||||
const source = container.querySelector('source');
|
||||
expect(source).toHaveAttribute('srcset', '/image.webp');
|
||||
});
|
||||
|
||||
it('handles case-insensitive extensions', () => {
|
||||
const { container } = render(
|
||||
<OptimizedImage {...defaultProps} src="/image.PNG" />
|
||||
);
|
||||
const source = container.querySelector('source');
|
||||
expect(source).toHaveAttribute('srcset', '/image.webp');
|
||||
});
|
||||
|
||||
it('applies custom className to picture element', () => {
|
||||
const { container } = render(
|
||||
<OptimizedImage {...defaultProps} className="custom-class" />
|
||||
);
|
||||
const picture = container.querySelector('picture');
|
||||
expect(picture).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('passes priority prop to Image component', () => {
|
||||
render(<OptimizedImage {...defaultProps} priority />);
|
||||
const img = screen.getByAltText('Test Image');
|
||||
// Priority images should not have loading="lazy"
|
||||
expect(img).not.toHaveAttribute('loading', 'lazy');
|
||||
});
|
||||
|
||||
it('sets correct dimensions on the image', () => {
|
||||
render(<OptimizedImage {...defaultProps} width={200} height={150} />);
|
||||
const img = screen.getByAltText('Test Image');
|
||||
expect(img).toHaveAttribute('width', '200');
|
||||
expect(img).toHaveAttribute('height', '150');
|
||||
});
|
||||
});
|
||||
|
||||
describe('VocalinuxLogo', () => {
|
||||
it('renders without crashing', () => {
|
||||
render(<VocalinuxLogo />);
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses the circular mark', () => {
|
||||
render(<VocalinuxLogo />);
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toHaveAttribute('src', '/brand/vocalinux-mark-circle.svg');
|
||||
});
|
||||
|
||||
it('uses default dimensions when not specified', () => {
|
||||
render(<VocalinuxLogo />);
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toHaveAttribute('width', '32');
|
||||
expect(img).toHaveAttribute('height', '32');
|
||||
});
|
||||
|
||||
it('accepts custom dimensions', () => {
|
||||
render(<VocalinuxLogo width={64} height={64} />);
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toHaveAttribute('width', '64');
|
||||
expect(img).toHaveAttribute('height', '64');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
render(<VocalinuxLogo className="logo-class" />);
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toHaveClass('logo-class');
|
||||
});
|
||||
|
||||
it('passes priority prop for eager loading', () => {
|
||||
render(<VocalinuxLogo priority />);
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toHaveAttribute('loading', 'eager');
|
||||
});
|
||||
|
||||
it('uses empty alt text when decorative', () => {
|
||||
const { container } = render(<VocalinuxLogo decorative />);
|
||||
expect(container.querySelector('img')).toHaveAttribute('alt', '');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,280 @@
|
||||
import React from "react";
|
||||
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import {
|
||||
ScreenshotGallery,
|
||||
type Screenshot,
|
||||
} from "../screenshot-gallery";
|
||||
|
||||
const productShots: Screenshot[] = [
|
||||
{
|
||||
src: "/screenshots/00-transcription.png",
|
||||
srcDark: "/screenshots/dark/00-transcription.png",
|
||||
alt: "Transcription alt",
|
||||
title: "Transcription in action",
|
||||
description: "Dictate into apps",
|
||||
width: 100,
|
||||
height: 80,
|
||||
},
|
||||
{
|
||||
src: "/screenshots/02-system-tray.png",
|
||||
srcDark: "/screenshots/dark/02-system-tray.png",
|
||||
alt: "Tray alt",
|
||||
title: "System tray",
|
||||
description: "Tray states",
|
||||
width: 100,
|
||||
height: 80,
|
||||
},
|
||||
];
|
||||
|
||||
const settingsShots: Screenshot[] = [
|
||||
{
|
||||
src: "/screenshots/settings-speech-engine.png",
|
||||
srcDark: "/screenshots/dark/settings-speech-engine.png",
|
||||
alt: "Speech engine alt",
|
||||
title: "Speech Engine",
|
||||
description: "Pick a model",
|
||||
width: 100,
|
||||
height: 160,
|
||||
},
|
||||
];
|
||||
|
||||
function mockColorScheme(dark: boolean) {
|
||||
const listeners = new Set<(event: MediaQueryListEvent) => void>();
|
||||
const media = {
|
||||
matches: dark,
|
||||
media: "(prefers-color-scheme: dark)",
|
||||
onchange: null,
|
||||
addEventListener: (
|
||||
_type: string,
|
||||
listener: (event: MediaQueryListEvent) => void,
|
||||
) => {
|
||||
listeners.add(listener);
|
||||
},
|
||||
removeEventListener: (
|
||||
_type: string,
|
||||
listener: (event: MediaQueryListEvent) => void,
|
||||
) => {
|
||||
listeners.delete(listener);
|
||||
},
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
emit(next: boolean) {
|
||||
media.matches = next;
|
||||
const event = { matches: next } as MediaQueryListEvent;
|
||||
listeners.forEach((listener) => listener(event));
|
||||
},
|
||||
};
|
||||
window.matchMedia = jest.fn().mockImplementation((query: string) => {
|
||||
if (query.includes("prefers-color-scheme: dark")) return media;
|
||||
return { ...media, matches: false, addEventListener: jest.fn() };
|
||||
});
|
||||
return media;
|
||||
}
|
||||
|
||||
describe("ScreenshotGallery lightbox", () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
mockColorScheme(false);
|
||||
});
|
||||
|
||||
it("defaults to the browser color scheme before any override", async () => {
|
||||
mockColorScheme(true);
|
||||
render(
|
||||
<ScreenshotGallery
|
||||
productShots={productShots}
|
||||
settingsShots={settingsShots}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Dark" })).toHaveAttribute(
|
||||
"aria-pressed",
|
||||
"true",
|
||||
);
|
||||
});
|
||||
expect(
|
||||
document.querySelector(
|
||||
'img.is-on[src="/screenshots/dark/00-transcription.png"]',
|
||||
),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it("follows the browser scheme until the visitor flips the switch", async () => {
|
||||
const media = mockColorScheme(true);
|
||||
render(
|
||||
<ScreenshotGallery
|
||||
productShots={productShots}
|
||||
settingsShots={settingsShots}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Dark" })).toHaveAttribute(
|
||||
"aria-pressed",
|
||||
"true",
|
||||
);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
media.emit(false);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Light" })).toHaveAttribute(
|
||||
"aria-pressed",
|
||||
"true",
|
||||
);
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Dark" }));
|
||||
act(() => {
|
||||
media.emit(false);
|
||||
});
|
||||
expect(screen.getByRole("button", { name: "Dark" })).toHaveAttribute(
|
||||
"aria-pressed",
|
||||
"true",
|
||||
);
|
||||
expect(sessionStorage.getItem("vocalinux-shot-appearance")).toBe("dark");
|
||||
});
|
||||
|
||||
it("restores a tab override from sessionStorage", async () => {
|
||||
sessionStorage.setItem("vocalinux-shot-appearance", "dark");
|
||||
render(
|
||||
<ScreenshotGallery
|
||||
productShots={productShots}
|
||||
settingsShots={settingsShots}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Dark" })).toHaveAttribute(
|
||||
"aria-pressed",
|
||||
"true",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("flips between light and dark app shots without changing the page theme", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ScreenshotGallery
|
||||
productShots={productShots}
|
||||
settingsShots={settingsShots}
|
||||
/>,
|
||||
);
|
||||
|
||||
const lightBtn = screen.getByRole("button", { name: "Light" });
|
||||
const darkBtn = screen.getByRole("button", { name: "Dark" });
|
||||
expect(lightBtn).toHaveAttribute("aria-pressed", "true");
|
||||
expect(darkBtn).toHaveAttribute("aria-pressed", "false");
|
||||
|
||||
await user.click(darkBtn);
|
||||
expect(darkBtn).toHaveAttribute("aria-pressed", "true");
|
||||
expect(lightBtn).toHaveAttribute("aria-pressed", "false");
|
||||
expect(
|
||||
document.querySelector(
|
||||
'img.is-on[src="/screenshots/dark/settings-speech-engine.png"]',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(document.documentElement).not.toHaveClass("dark");
|
||||
});
|
||||
|
||||
it("opens an expanded gallery view when a screenshot is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ScreenshotGallery
|
||||
productShots={productShots}
|
||||
settingsShots={settingsShots}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "View larger: Speech Engine" }),
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole("dialog");
|
||||
expect(dialog).toBeInTheDocument();
|
||||
expect(dialog).toHaveAttribute("aria-modal", "true");
|
||||
// Settings shot is third overall (index 2 -> 3/3)
|
||||
expect(within(dialog).getByText(/3 \/ 3/)).toBeInTheDocument();
|
||||
const speechImgs = within(dialog).getAllByAltText("Speech engine alt");
|
||||
expect(speechImgs.length).toBe(1);
|
||||
expect(speechImgs[0]).toHaveAttribute(
|
||||
"src",
|
||||
"/screenshots/settings-speech-engine.png",
|
||||
);
|
||||
expect(within(dialog).getByText("Speech Engine")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("navigates with next/prev controls and closes with Escape", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ScreenshotGallery
|
||||
productShots={productShots}
|
||||
settingsShots={settingsShots}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: "View larger: Transcription in action",
|
||||
}),
|
||||
);
|
||||
|
||||
let dialog = screen.getByRole("dialog");
|
||||
expect(within(dialog).getByText(/1 \/ 3/)).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getAllByAltText("Transcription alt").length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Next screenshot" }));
|
||||
dialog = screen.getByRole("dialog");
|
||||
expect(within(dialog).getByText(/2 \/ 3/)).toBeInTheDocument();
|
||||
expect(within(dialog).getAllByAltText("Tray alt").length).toBeGreaterThan(0);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "Previous screenshot" }),
|
||||
);
|
||||
dialog = screen.getByRole("dialog");
|
||||
expect(within(dialog).getByText(/1 \/ 3/)).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getAllByAltText("Transcription alt").length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
fireEvent.keyDown(window, { key: "Escape" });
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("supports keyboard arrow navigation while open", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ScreenshotGallery
|
||||
productShots={productShots}
|
||||
settingsShots={settingsShots}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: "View larger: Transcription in action",
|
||||
}),
|
||||
);
|
||||
|
||||
fireEvent.keyDown(window, { key: "ArrowRight" });
|
||||
expect(
|
||||
within(screen.getByRole("dialog")).getAllByAltText("Tray alt").length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
fireEvent.keyDown(window, { key: "ArrowRight" });
|
||||
expect(
|
||||
within(screen.getByRole("dialog")).getAllByAltText("Speech engine alt")
|
||||
.length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
fireEvent.keyDown(window, { key: "ArrowLeft" });
|
||||
expect(
|
||||
within(screen.getByRole("dialog")).getAllByAltText("Tray alt").length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
type SyntaxHighlighterStyle = Record<string, React.CSSProperties>;
|
||||
|
||||
type SyntaxHighlighterProps = {
|
||||
children: string;
|
||||
language: string;
|
||||
style: SyntaxHighlighterStyle;
|
||||
className: string;
|
||||
customStyle?: React.CSSProperties;
|
||||
wrapLongLines: boolean;
|
||||
};
|
||||
|
||||
// Lazy load syntax highlighter to reduce initial bundle size (~84KB savings)
|
||||
const SyntaxHighlighter = dynamic<SyntaxHighlighterProps>(
|
||||
() =>
|
||||
import("react-syntax-highlighter").then(
|
||||
(mod) => mod.default as React.ComponentType<SyntaxHighlighterProps>,
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="subpage-terminal p-4 text-sm">
|
||||
<pre className="whitespace-pre-wrap">Loading...</pre>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
interface CodeBlockProps {
|
||||
language: string;
|
||||
children: string;
|
||||
className?: string;
|
||||
customStyle?: React.CSSProperties;
|
||||
wrapLongLines?: boolean;
|
||||
}
|
||||
|
||||
export function CodeBlock({
|
||||
language,
|
||||
children,
|
||||
className = "",
|
||||
customStyle,
|
||||
wrapLongLines = false,
|
||||
}: CodeBlockProps) {
|
||||
const [style, setStyle] = useState<SyntaxHighlighterStyle | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Lazy load the style
|
||||
import("react-syntax-highlighter/dist/esm/styles/hljs").then((mod) => {
|
||||
setStyle(mod.atomOneDark as SyntaxHighlighterStyle);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!style) {
|
||||
return (
|
||||
<div
|
||||
className={`subpage-terminal p-4 text-sm ${className}`}
|
||||
style={customStyle}
|
||||
>
|
||||
<pre className="whitespace-pre-wrap text-green-400">{children}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={style}
|
||||
className={className}
|
||||
customStyle={customStyle}
|
||||
wrapLongLines={wrapLongLines}
|
||||
>
|
||||
{children}
|
||||
</SyntaxHighlighter>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
// Re-export from optimized-image component for backward compatibility
|
||||
export { VocalinuxLogo } from "./optimized-image";
|
||||
|
||||
// Legacy export - use VocalinuxLogo from optimized-image instead
|
||||
export const VocalinuxLogoLegacy: React.FC<{ className?: string }> = ({ className = "" }) => {
|
||||
return (
|
||||
<Image
|
||||
src="/vocalinux.png"
|
||||
alt="Vocalinux Logo"
|
||||
width={36}
|
||||
height={36}
|
||||
className={className}
|
||||
priority
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Alternative: Inline SVG version for when color customization is needed
|
||||
export const VocalinuxLogoInline: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
return (
|
||||
<svg width="36" height="36" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M243.33 120.92c-58.29 1.8-107.88 51.39-107.88 110.12v100.24c0 20.87 5.16 39.95 14.02 57.07l-44.12 44.12c-3.27 3.27-3.27 8.58 0 11.86 1.64 1.64 3.79 2.46 5.93 2.46 2.14 0 4.29-.82 5.93-2.46l43.39-43.39a107.388 107.388 0 0074.73 30.13h.01c20.94 0 40.83-6.05 57.99-16.52 23.86-14.53 42.22-37.58 49.7-64.48.8-2.89-1.18-5.8-4.17-6.14-2.99-.33-5.57 1.79-5.75 4.8-2.91 48.11-43.05 85.82-91.5 85.82h-.76c-50.83-1.57-93.56-42.09-93.56-93.67V231.04c0-51.11 41.57-93.67 92.68-93.67 46.36 0 86.43 35.37 93.54 80.62.41 2.64 2.69 4.56 5.38 4.5 2.67-.06 4.84-2.1 5.13-4.75 7.65-71.11-46.43-98.62-99.69-96.82z" fill="currentColor" />
|
||||
<path d="M299.33 256.96c0-2.36-1.91-4.27-4.27-4.27s-4.27 1.91-4.27 4.27v62.09c0 2.36 1.91 4.27 4.27 4.27s4.27-1.91 4.27-4.27v-62.09z" fill="currentColor" />
|
||||
<path d="M361.42 164.01v-17.06c0-2.36-1.91-4.27-4.27-4.27s-4.27 1.91-4.27 4.27v17.06c0 2.36 1.91 4.27 4.27 4.27s4.27-1.91 4.27-4.27zM336.62 164.01v-17.06c0-2.36-1.91-4.27-4.27-4.27s-4.27 1.91-4.27 4.27v17.06c0 2.36 1.91 4.27 4.27 4.27s4.27-1.91 4.27-4.27zM311.82 164.01v-17.06c0-2.36-1.91-4.27-4.27-4.27s-4.27 1.91-4.27 4.27v17.06c0 2.36 1.91 4.27 4.27 4.27s4.27-1.91 4.27-4.27z" fill="currentColor" />
|
||||
<path d="M404.61 164.01c0-28.19-11.08-38.24-11.08-38.24-5.54-5.54-12.75-8.31-20-8.31s-14.46 2.77-20 8.31l-7.65 7.65-7.65-7.65c-11.04-11.04-28.96-11.04-40 0l-7.65 7.65-7.65-7.65c-11.04-11.04-28.96-11.04-40 0-11.08 11.08-11.08 28.92 0 40l47.65 47.57v44.62c0 14.15 11.51 25.65 25.65 25.65 14.15 0 25.65-11.51 25.65-25.65v-44.62l47.65-47.57c5.54-5.54 8.31-12.75 8.31-20s-2.77-14.46-8.31-20c-1.67-1.67-1.67-4.38 0-6.04 10.88 4.95 20.73 13.84 20.73 24.27zm-80.82 84.97v19.07c0 9.44-7.68 17.11-17.11 17.11s-17.11-7.67-17.11-17.11v-19.07l17.11 17.11 17.11-17.11zm24.27-48.27l-41.38 41.38L265.3 200.7c-7.37-7.37-7.37-19.32 0-26.69 7.37-7.37 19.32-7.37 26.69 0l14.69 14.69 14.69-14.69c3.56-3.56 8.3-5.52 13.34-5.52s9.78 1.96 13.35 5.52a4.233 4.233 0 006.04 0c3.56-3.56 8.3-5.52 13.34-5.52s9.78 1.96 13.34 5.52c7.37 7.37 7.37 19.32 0 26.69z" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { VocalinuxLogo } from "./icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
interface LogoProps {
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
textClassName?: string;
|
||||
}
|
||||
export function Logo({
|
||||
className,
|
||||
iconClassName,
|
||||
textClassName
|
||||
}: LogoProps) {
|
||||
return <div className={cn("flex items-center gap-2", className)}>
|
||||
<VocalinuxLogo className={cn("h-8 w-8", iconClassName)} />
|
||||
<span className={cn("font-bold text-xl", textClassName)}>Vocalinux</span>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { footerGroups, mobileExtraNav, pageNav } from "@/lib/site-nav";
|
||||
|
||||
// Static export serves one 404.html for every missing path, so the requested
|
||||
// URL is only known after mount. The first render shows nothing extra to keep
|
||||
// hydration clean.
|
||||
|
||||
function useMissingPath(): string | null {
|
||||
const [path, setPath] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
const { pathname } = window.location;
|
||||
if (pathname && pathname !== "/404/" && pathname !== "/404.html") {
|
||||
// Malformed escapes such as /%FF/ make decodeURIComponent throw.
|
||||
let decoded = pathname;
|
||||
try {
|
||||
decoded = decodeURIComponent(pathname);
|
||||
} catch {
|
||||
// Keep the raw path; it still gets a suggestion attempt.
|
||||
}
|
||||
setPath(decoded.slice(0, 80));
|
||||
}
|
||||
}, []);
|
||||
return path;
|
||||
}
|
||||
|
||||
// Every internal page the site links from its header and footer.
|
||||
const knownPages = Array.from(
|
||||
new Set(
|
||||
[...pageNav, ...mobileExtraNav, ...footerGroups.flatMap((g) => g.links)]
|
||||
.map((link) => link.href)
|
||||
.filter((href) => href.startsWith("/") && !href.startsWith("/#") && href !== "/"),
|
||||
),
|
||||
);
|
||||
|
||||
function editDistance(a: string, b: string): number {
|
||||
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
let prev = row[0];
|
||||
row[0] = i;
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
const current = row[j];
|
||||
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
||||
prev = current;
|
||||
}
|
||||
}
|
||||
return row[b.length];
|
||||
}
|
||||
|
||||
const normalize = (path: string) => `/${path.toLowerCase().replace(/^\/+|\/+$/g, "")}/`;
|
||||
|
||||
export function suggestPage(path: string, pages: string[] = knownPages): string | null {
|
||||
const target = normalize(path);
|
||||
let best: { href: string; distance: number } | null = null;
|
||||
for (const href of pages) {
|
||||
const distance = editDistance(target, href);
|
||||
if (!best || distance < best.distance) best = { href, distance };
|
||||
}
|
||||
if (!best) return null;
|
||||
// Distance 0 is a case or trailing-slash slip (Pages is case-sensitive).
|
||||
// Otherwise it must be close enough to be a typo, not a different page.
|
||||
return best.distance <= Math.max(2, Math.floor(best.href.length / 3)) ? best.href : null;
|
||||
}
|
||||
|
||||
export function DidYouMean() {
|
||||
const path = useMissingPath();
|
||||
const suggestion = path ? suggestPage(path) : null;
|
||||
if (!suggestion) return null;
|
||||
return (
|
||||
<p className="nf-suggest">
|
||||
Did you mean <Link href={suggestion}>{suggestion}</Link>?
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
interface OptimizedImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
width: number;
|
||||
height: number;
|
||||
className?: string;
|
||||
priority?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimized image component that uses WebP format with PNG fallback.
|
||||
* Browsers that support WebP will load the smaller WebP file, while older
|
||||
* browsers will fallback to PNG format.
|
||||
*/
|
||||
export const OptimizedImage: React.FC<OptimizedImageProps> = ({
|
||||
src,
|
||||
alt,
|
||||
width,
|
||||
height,
|
||||
className = "",
|
||||
priority = false,
|
||||
}) => {
|
||||
// Get WebP version of the image
|
||||
const webpSrc = src.replace(/\.(png|jpg|jpeg)$/i, '.webp');
|
||||
|
||||
// For Next.js Image component with priority and proper optimization
|
||||
// we use the Next.js Image with WebP source
|
||||
return (
|
||||
<picture className={className}>
|
||||
<source srcSet={webpSrc} type="image/webp" />
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
priority={priority}
|
||||
/>
|
||||
</picture>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Specialized Vocalinux logo component with WebP support
|
||||
*/
|
||||
export const VocalinuxLogo: React.FC<{
|
||||
className?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
priority?: boolean;
|
||||
/** Set when the visible "Vocalinux" text sits next to the mark. */
|
||||
decorative?: boolean;
|
||||
}> = ({
|
||||
className = "",
|
||||
width = 32,
|
||||
height = 32,
|
||||
priority = false,
|
||||
decorative = false,
|
||||
}) => {
|
||||
return (
|
||||
// SVG lockup; next/image is for raster.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src="/brand/vocalinux-mark-circle.svg"
|
||||
alt={decorative ? "" : "Vocalinux"}
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
decoding="async"
|
||||
loading={priority ? "eager" : "lazy"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,410 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useEffect, useId, useState } from "react";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Expand,
|
||||
ImageIcon,
|
||||
Settings,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
export type Screenshot = {
|
||||
src: string;
|
||||
srcDark?: string;
|
||||
alt: string;
|
||||
title: string;
|
||||
description: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type ShotTheme = "light" | "dark";
|
||||
|
||||
const THEME_STORAGE_KEY = "vocalinux-shot-appearance";
|
||||
|
||||
export function darkScreenshotPath(src: string): string {
|
||||
if (src.includes("/dark/")) return src;
|
||||
return src.replace("/screenshots/", "/screenshots/dark/");
|
||||
}
|
||||
|
||||
function shotDarkSrc(shot: Screenshot): string {
|
||||
return shot.srcDark ?? darkScreenshotPath(shot.src);
|
||||
}
|
||||
|
||||
function systemShotTheme(): ShotTheme {
|
||||
if (typeof window === "undefined") return "light";
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
function readStoredTheme(): ShotTheme | null {
|
||||
try {
|
||||
const stored = sessionStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (stored === "light" || stored === "dark") return stored;
|
||||
} catch {
|
||||
/* private mode */
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
type ScreenshotGalleryProps = {
|
||||
productShots: Screenshot[];
|
||||
settingsShots: Screenshot[];
|
||||
};
|
||||
|
||||
function ShotPair({
|
||||
shot,
|
||||
theme,
|
||||
className,
|
||||
loading = "lazy",
|
||||
}: {
|
||||
shot: Screenshot;
|
||||
theme: ShotTheme;
|
||||
className?: string;
|
||||
loading?: "lazy" | "eager";
|
||||
}) {
|
||||
const darkSrc = shotDarkSrc(shot);
|
||||
return (
|
||||
<div className="shot-pair">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={shot.src}
|
||||
alt={theme === "light" ? shot.alt : ""}
|
||||
width={shot.width}
|
||||
height={shot.height}
|
||||
className={`${className ?? ""} ${theme === "light" ? "is-on" : ""}`.trim()}
|
||||
loading={loading}
|
||||
decoding="async"
|
||||
aria-hidden={theme !== "light"}
|
||||
/>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={darkSrc}
|
||||
alt={theme === "dark" ? shot.alt : ""}
|
||||
width={shot.width}
|
||||
height={shot.height}
|
||||
className={`${className ?? ""} ${theme === "dark" ? "is-on" : ""}`.trim()}
|
||||
loading={loading}
|
||||
decoding="async"
|
||||
aria-hidden={theme !== "dark"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScreenshotCard({
|
||||
shot,
|
||||
theme,
|
||||
onOpen,
|
||||
}: {
|
||||
shot: Screenshot;
|
||||
theme: ShotTheme;
|
||||
onOpen: () => void;
|
||||
}) {
|
||||
return (
|
||||
<figure className="gallery-card">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
className="group relative flex min-h-0 w-full flex-1 cursor-zoom-in items-center justify-center bg-muted p-3 text-left"
|
||||
aria-label={`View larger: ${shot.title}`}
|
||||
>
|
||||
<ShotPair
|
||||
shot={shot}
|
||||
theme={theme}
|
||||
className="mx-auto h-auto max-h-64 w-full rounded-lg object-contain"
|
||||
/>
|
||||
<span className="pointer-events-none absolute bottom-4 right-4 inline-flex items-center gap-1.5 rounded-full bg-[color:var(--dark-ink)] px-2.5 py-1 text-xs font-medium text-[color:var(--paper-bright)] opacity-0 shadow transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
|
||||
<Expand className="h-3.5 w-3.5" />
|
||||
Expand
|
||||
</span>
|
||||
</button>
|
||||
<figcaption className="space-y-1">
|
||||
<h3 className="text-lg font-semibold">{shot.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{shot.description}</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
function Lightbox({
|
||||
shots,
|
||||
index,
|
||||
theme,
|
||||
onClose,
|
||||
onPrev,
|
||||
onNext,
|
||||
titleId,
|
||||
}: {
|
||||
shots: Screenshot[];
|
||||
index: number;
|
||||
theme: ShotTheme;
|
||||
onClose: () => void;
|
||||
onPrev: () => void;
|
||||
onNext: () => void;
|
||||
titleId: string;
|
||||
}) {
|
||||
const shot = shots[index];
|
||||
const hasPrev = index > 0;
|
||||
const hasNext = index < shots.length - 1;
|
||||
|
||||
useEffect(() => {
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
onClose();
|
||||
} else if (event.key === "ArrowLeft") {
|
||||
onPrev();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
onNext();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [onClose, onNext, onPrev]);
|
||||
|
||||
if (!shot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/90 p-3 sm:p-6"
|
||||
onClick={onClose}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="absolute right-3 top-3 z-10 inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
|
||||
aria-label="Close gallery"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onPrev();
|
||||
}}
|
||||
disabled={!hasPrev}
|
||||
className="absolute left-2 top-1/2 z-10 inline-flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-white disabled:pointer-events-none disabled:opacity-30 sm:left-4"
|
||||
aria-label="Previous screenshot"
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onNext();
|
||||
}}
|
||||
disabled={!hasNext}
|
||||
className="absolute right-2 top-1/2 z-10 inline-flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-white disabled:pointer-events-none disabled:opacity-30 sm:right-4"
|
||||
aria-label="Next screenshot"
|
||||
>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
className="flex max-h-full w-full max-w-6xl flex-col items-center gap-3"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="relative flex max-h-[min(80vh,900px)] w-full items-center justify-center">
|
||||
<ShotPair
|
||||
shot={shot}
|
||||
theme={theme}
|
||||
loading="eager"
|
||||
className="max-h-[min(80vh,900px)] w-auto max-w-full rounded-lg object-contain shadow-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-2xl px-2 text-center text-white">
|
||||
<p id={titleId} className="text-lg font-semibold">
|
||||
{shot.title}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-white/80">{shot.description}</p>
|
||||
<p className="mt-2 text-xs text-white/60">
|
||||
{index + 1} / {shots.length}
|
||||
<span className="mx-2">·</span>
|
||||
Esc to close · arrow keys to browse
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AppearanceSwitch({
|
||||
theme,
|
||||
onChange,
|
||||
}: {
|
||||
theme: ShotTheme;
|
||||
onChange: (theme: ShotTheme) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="shot-theme-bar">
|
||||
<div>
|
||||
<p className="shot-theme-kicker">App appearance</p>
|
||||
<p className="shot-theme-copy">
|
||||
Flip the captured GTK theme. Starts from your system setting.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="shot-switch"
|
||||
data-mode={theme}
|
||||
role="group"
|
||||
aria-label="App appearance"
|
||||
>
|
||||
<span className="shot-switch-knob" aria-hidden="true" />
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={theme === "light"}
|
||||
onClick={() => onChange("light")}
|
||||
>
|
||||
Light
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={theme === "dark"}
|
||||
onClick={() => onChange("dark")}
|
||||
>
|
||||
Dark
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ScreenshotGallery({
|
||||
productShots,
|
||||
settingsShots,
|
||||
}: ScreenshotGalleryProps) {
|
||||
const allShots = [...productShots, ...settingsShots];
|
||||
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
||||
const [theme, setTheme] = useState<ShotTheme>("light");
|
||||
const [userOverride, setUserOverride] = useState(false);
|
||||
const titleId = useId();
|
||||
|
||||
useEffect(() => {
|
||||
const stored = readStoredTheme();
|
||||
if (stored) {
|
||||
setTheme(stored);
|
||||
setUserOverride(true);
|
||||
return;
|
||||
}
|
||||
setTheme(systemShotTheme());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (userOverride) return undefined;
|
||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const onChange = () => setTheme(media.matches ? "dark" : "light");
|
||||
media.addEventListener("change", onChange);
|
||||
return () => media.removeEventListener("change", onChange);
|
||||
}, [userOverride]);
|
||||
|
||||
const chooseTheme = useCallback((next: ShotTheme) => {
|
||||
setTheme(next);
|
||||
setUserOverride(true);
|
||||
try {
|
||||
sessionStorage.setItem(THEME_STORAGE_KEY, next);
|
||||
} catch {
|
||||
/* private mode */
|
||||
}
|
||||
}, []);
|
||||
|
||||
const openAt = useCallback((index: number) => {
|
||||
setActiveIndex(index);
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setActiveIndex(null);
|
||||
}, []);
|
||||
|
||||
const showPrev = useCallback(() => {
|
||||
setActiveIndex((current) => {
|
||||
if (current === null || current <= 0) {
|
||||
return current;
|
||||
}
|
||||
return current - 1;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const showNext = useCallback(() => {
|
||||
setActiveIndex((current) => {
|
||||
if (current === null || current >= allShots.length - 1) {
|
||||
return current;
|
||||
}
|
||||
return current + 1;
|
||||
});
|
||||
}, [allShots.length]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppearanceSwitch theme={theme} onChange={chooseTheme} />
|
||||
|
||||
<section className="mb-14">
|
||||
<div className="mb-6 flex items-center gap-2">
|
||||
<ImageIcon className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-2xl font-bold">Product</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 items-stretch gap-6 sm:grid-cols-3">
|
||||
{productShots.map((shot, index) => (
|
||||
<ScreenshotCard
|
||||
key={shot.src}
|
||||
shot={shot}
|
||||
theme={theme}
|
||||
onOpen={() => openAt(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-14">
|
||||
<div className="mb-6 flex items-center gap-2">
|
||||
<Settings className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-2xl font-bold">Settings</h2>
|
||||
</div>
|
||||
<p className="mb-6 max-w-3xl text-muted-foreground">
|
||||
Every sidebar settings page, from speech model selection through
|
||||
advanced whisper.cpp tuning. Click any shot to expand it.
|
||||
</p>
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{settingsShots.map((shot, index) => (
|
||||
<ScreenshotCard
|
||||
key={shot.src}
|
||||
shot={shot}
|
||||
theme={theme}
|
||||
onOpen={() => openAt(productShots.length + index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{activeIndex !== null && (
|
||||
<Lightbox
|
||||
shots={allShots}
|
||||
index={activeIndex}
|
||||
theme={theme}
|
||||
onClose={close}
|
||||
onPrev={showPrev}
|
||||
onNext={showNext}
|
||||
titleId={titleId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { SiteChrome } from "@/components/site-chrome";
|
||||
import { absoluteUrl } from "@/lib/seo";
|
||||
|
||||
const pageTitles: Record<string, string> = {
|
||||
"/install": "Install Guides",
|
||||
"/install/ubuntu": "Ubuntu",
|
||||
"/install/fedora": "Fedora",
|
||||
"/install/arch": "Arch",
|
||||
"/screenshots": "Screenshots",
|
||||
"/changelog": "Changelog",
|
||||
"/compare": "Engine Comparison",
|
||||
"/remote-api": "Remote API",
|
||||
"/languages": "Languages",
|
||||
"/wayland": "Wayland",
|
||||
"/gpu-acceleration": "GPU Acceleration",
|
||||
"/voice-activity-detection": "Silero VAD",
|
||||
"/advanced-settings": "Advanced Settings",
|
||||
"/desktop-reliability": "Desktop Reliability",
|
||||
"/autostart": "Autostart",
|
||||
"/for-developers": "For Developers",
|
||||
"/rsi-prevention": "RSI Prevention",
|
||||
"/writers": "For Writers",
|
||||
"/gnome-kde": "GNOME vs KDE",
|
||||
"/use-cases": "Use Cases",
|
||||
"/vs-nerd-dictation": "vs Nerd Dictation",
|
||||
"/whisper-model-guide": "Whisper Models",
|
||||
"/voice-typing-vscode": "VS Code",
|
||||
"/offline": "Offline",
|
||||
"/open-source": "Open Source",
|
||||
"/privacy": "Privacy Policy",
|
||||
"/faq": "FAQ",
|
||||
"/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 {
|
||||
return segment.replace(/-/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
function Breadcrumbs() {
|
||||
const pathname = usePathname();
|
||||
if (!pathname || pathname === "/") return null;
|
||||
|
||||
const segments = pathname.split("/").filter(Boolean);
|
||||
const crumbs = segments.map((segment, index) => {
|
||||
const href = "/" + segments.slice(0, index + 1).join("/");
|
||||
const key = href.replace(/\/$/, "");
|
||||
return {
|
||||
href: href.endsWith("/") ? href : `${href}/`,
|
||||
label: pageTitles[key] ?? titleFromSegment(segment),
|
||||
};
|
||||
});
|
||||
|
||||
// Same trail as the visible breadcrumbs, so the markup matches the page.
|
||||
const breadcrumbJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: [{ href: "/", label: "Home" }, ...crumbs].map(
|
||||
(crumb, index) => ({
|
||||
"@type": "ListItem",
|
||||
position: index + 1,
|
||||
name: crumb.label,
|
||||
item: absoluteUrl(crumb.href),
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
|
||||
/>
|
||||
<nav className="breadcrumbs" aria-label="Breadcrumb">
|
||||
<Link href="/">Home</Link>
|
||||
{crumbs.map((crumb, index) => (
|
||||
<React.Fragment key={crumb.href}>
|
||||
<span aria-hidden="true">/</span>
|
||||
{index === crumbs.length - 1 ? (
|
||||
<span>{crumb.label}</span>
|
||||
) : (
|
||||
<Link href={crumb.href}>{crumb.label}</Link>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SeoSubpageShell({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<SiteChrome variant="page">
|
||||
<div className="page-main">
|
||||
<div className="shell">
|
||||
<Breadcrumbs />
|
||||
<div className="subpage-content min-w-0">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</SiteChrome>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useId, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Star } from "lucide-react";
|
||||
import { VocalinuxLogo } from "@/components/optimized-image";
|
||||
import { GITHUB_REPO_URL, VOCAHQ_URL } from "@/lib/seo";
|
||||
import {
|
||||
footerGroups,
|
||||
homeNav,
|
||||
mobileExtraNav,
|
||||
pageNav,
|
||||
socialLinks,
|
||||
type NavLink,
|
||||
} from "@/lib/site-nav";
|
||||
|
||||
// Star count is fetched once at build time (next.config.js) so visitors never
|
||||
// call api.github.com. Empty or missing falls back to the "GitHub" label.
|
||||
const GITHUB_STARS = Number(process.env.NEXT_PUBLIC_GITHUB_STARS) || null;
|
||||
|
||||
function SmartLink({
|
||||
href,
|
||||
className,
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
href: string;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
const external = href.startsWith("http");
|
||||
if (external) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
if (href.startsWith("#")) {
|
||||
return (
|
||||
<a href={href} className={className} onClick={onClick}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Link href={href} className={className} onClick={onClick}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function SiteChrome({
|
||||
children,
|
||||
variant = "page",
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
variant?: "home" | "page";
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const stars = GITHUB_STARS;
|
||||
const navId = useId();
|
||||
const nav = variant === "home" ? homeNav : pageNav;
|
||||
const ctaHref = variant === "home" ? "#install" : "/#install";
|
||||
const extras: NavLink[] =
|
||||
variant === "home"
|
||||
? [
|
||||
{ href: "/screenshots/", label: "Screenshots" },
|
||||
{ href: "/install/", label: "Distro guides" },
|
||||
...mobileExtraNav,
|
||||
]
|
||||
: mobileExtraNav;
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") setOpen(false);
|
||||
};
|
||||
const onResize = () => {
|
||||
if (window.innerWidth >= 921) setOpen(false);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKey);
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-[100dvh] max-w-[100vw] overflow-x-clip">
|
||||
<a className="skip-link" href="#main">
|
||||
Skip to content
|
||||
</a>
|
||||
|
||||
<header className="site-header">
|
||||
<div className="shell header-inner">
|
||||
<Link href="/" className="brand" aria-label="Vocalinux home">
|
||||
<VocalinuxLogo
|
||||
width={28}
|
||||
height={28}
|
||||
className="h-7 w-7"
|
||||
priority
|
||||
decorative
|
||||
/>
|
||||
<span>Vocalinux</span>
|
||||
</Link>
|
||||
|
||||
<nav className="nav-links" aria-label="Primary">
|
||||
{nav.map((item) => (
|
||||
<SmartLink key={item.href + item.label} href={item.href}>
|
||||
{item.label}
|
||||
</SmartLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="header-actions">
|
||||
<a
|
||||
className="header-stars"
|
||||
href={GITHUB_REPO_URL}
|
||||
rel="noopener noreferrer"
|
||||
aria-label={
|
||||
stars !== null
|
||||
? `Vocalinux on GitHub, ${stars.toLocaleString("en-US")} stars`
|
||||
: "Vocalinux on GitHub"
|
||||
}
|
||||
>
|
||||
<Star
|
||||
className="h-4 w-4"
|
||||
strokeWidth={1.75}
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{stars !== null ? stars.toLocaleString("en-US") : "GitHub"}</span>
|
||||
</a>
|
||||
<button
|
||||
className="nav-toggle"
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
aria-controls={navId}
|
||||
aria-label="Toggle menu"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
>
|
||||
<span className="sr-only">
|
||||
{open ? "Close navigation" : "Open navigation"}
|
||||
</span>
|
||||
<span className="nav-toggle-bars" aria-hidden="true">
|
||||
<i></i>
|
||||
<i></i>
|
||||
<i></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
id={navId}
|
||||
className="mobile-nav shell"
|
||||
aria-label="Site"
|
||||
hidden={!open}
|
||||
>
|
||||
{[...nav, ...extras].map((item) => (
|
||||
<SmartLink
|
||||
key={`m-${item.href}-${item.label}`}
|
||||
href={item.href}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{item.label}
|
||||
</SmartLink>
|
||||
))}
|
||||
<a
|
||||
className="btn btn-primary"
|
||||
href={ctaHref}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Install Vocalinux
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main id="main">{children}</main>
|
||||
|
||||
<footer className="site-footer">
|
||||
<div className="shell">
|
||||
<div className="footer-inner">
|
||||
<div className="footer-brand">
|
||||
<Link href="/" className="brand">
|
||||
<VocalinuxLogo
|
||||
width={28}
|
||||
height={28}
|
||||
className="h-7 w-7"
|
||||
decorative
|
||||
/>
|
||||
<span>Vocalinux</span>
|
||||
</Link>
|
||||
<p>
|
||||
Offline voice typing for Linux. Local engines by default, no
|
||||
required account.
|
||||
</p>
|
||||
<ul className="footer-social">
|
||||
{socialLinks.map((item) => (
|
||||
<li key={item.href}>
|
||||
<a
|
||||
href={item.href}
|
||||
{...(item.href.startsWith("http")
|
||||
? { target: "_blank", rel: "noopener noreferrer" }
|
||||
: {})}
|
||||
aria-label={item.label}
|
||||
title={item.label}
|
||||
>
|
||||
<span
|
||||
className="footer-social-icon"
|
||||
style={{
|
||||
maskImage: `url(${item.icon})`,
|
||||
WebkitMaskImage: `url(${item.icon})`,
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{footerGroups.map((group) => (
|
||||
<div className="footer-col" key={group.title}>
|
||||
<h2>{group.title}</h2>
|
||||
<ul>
|
||||
{group.links.map((link) => (
|
||||
<li key={link.href + link.label}>
|
||||
<SmartLink href={link.href}>{link.label}</SmartLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="footer-base">
|
||||
<span>
|
||||
AGPL-3.0. Built in public by{" "}
|
||||
<a
|
||||
href="https://github.com/jatinkrmalik"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Jatin K Malik
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
<span>
|
||||
Part of{" "}
|
||||
<a href={VOCAHQ_URL} rel="noopener noreferrer">
|
||||
VocaHQ
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
|
||||
export function TerminalPrompt() {
|
||||
return (
|
||||
<span className="terminal-prompt">
|
||||
<span className="terminal-user">user</span>
|
||||
<span className="terminal-at">@</span>
|
||||
<span className="terminal-host">linux</span>
|
||||
<span className="terminal-path">:~</span>
|
||||
<span className="terminal-hash">$ </span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function TerminalBlock({
|
||||
command,
|
||||
displayCommand,
|
||||
label = "terminal",
|
||||
}: {
|
||||
command: string;
|
||||
displayCommand: string;
|
||||
label?: string;
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
void navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 2000);
|
||||
}, [command]);
|
||||
|
||||
return (
|
||||
<div className="terminal-panel min-w-0">
|
||||
<div className="terminal-chrome">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
title={copied ? "Copied" : "Copy"}
|
||||
className={`terminal-copy${copied ? " is-copied" : ""}`}
|
||||
aria-label={copied ? "Copied to clipboard" : "Copy to clipboard"}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4" strokeWidth={2} />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" strokeWidth={1.75} />
|
||||
)}
|
||||
</button>
|
||||
<span className="window-title">{label}</span>
|
||||
<span className="linux-controls" aria-hidden="true">
|
||||
<i></i>
|
||||
<i></i>
|
||||
<i className="close"></i>
|
||||
</span>
|
||||
</div>
|
||||
<pre className="max-w-full min-w-0 overflow-x-auto break-all">
|
||||
<TerminalPrompt />
|
||||
{displayCommand}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'default' | 'outline' | 'ghost' | 'link';
|
||||
size?: 'default' | 'sm' | 'lg';
|
||||
className?: string;
|
||||
asChild?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}, ref) => {
|
||||
const Comp = asChild ? "span" : "button";
|
||||
return <Comp className={cn("inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", {
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90": variant === "default",
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground": variant === "outline",
|
||||
"hover:bg-accent hover:text-accent-foreground": variant === "ghost",
|
||||
"text-primary underline-offset-4 hover:underline": variant === "link",
|
||||
"h-10 px-4 py-2": size === "default",
|
||||
"h-8 rounded-md px-3 text-xs": size === "sm",
|
||||
"h-12 rounded-md px-6": size === "lg"
|
||||
}, className)} ref={ref} {...props} />;
|
||||
});
|
||||
Button.displayName = "Button";
|
||||
export { Button };
|
||||
@@ -0,0 +1,23 @@
|
||||
import { absoluteUrl } from "../seo";
|
||||
|
||||
describe("absoluteUrl", () => {
|
||||
it("returns the site root for /", () => {
|
||||
expect(absoluteUrl("/")).toBe("https://vocalinux.com/");
|
||||
});
|
||||
|
||||
it("adds the trailing slash pages are served with", () => {
|
||||
expect(absoluteUrl("/install/debian")).toBe(
|
||||
"https://vocalinux.com/install/debian/",
|
||||
);
|
||||
expect(absoluteUrl("/faq/")).toBe("https://vocalinux.com/faq/");
|
||||
});
|
||||
|
||||
it("leaves file URLs without a trailing slash", () => {
|
||||
expect(absoluteUrl("/vocalinux.png")).toBe(
|
||||
"https://vocalinux.com/vocalinux.png",
|
||||
);
|
||||
expect(absoluteUrl("/og-image.png")).toBe(
|
||||
"https://vocalinux.com/og-image.png",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,594 @@
|
||||
export class AIServiceError extends Error {
|
||||
statusCode: number;
|
||||
provider: string;
|
||||
|
||||
constructor(message: string, statusCode: number, provider: string) {
|
||||
super(message);
|
||||
this.name = 'AIServiceError';
|
||||
this.statusCode = statusCode;
|
||||
this.provider = provider;
|
||||
}
|
||||
}
|
||||
|
||||
export type TextGenerationResult = {
|
||||
text: string;
|
||||
usage: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
};
|
||||
finishReason?: 'stop' | 'length' | 'content_filter';
|
||||
};
|
||||
|
||||
// Vision-enabled Text Types
|
||||
export type ContentItem =
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'image_url'; image_url: { url: string } };
|
||||
|
||||
export type Message = {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: ContentItem[];
|
||||
};
|
||||
|
||||
// We reuse TextGenerationResult since the output format is the same
|
||||
|
||||
|
||||
const CONFIG: {
|
||||
webId: string;
|
||||
availableProviders: {
|
||||
text: string[];
|
||||
visionEnabled: string[];
|
||||
};
|
||||
} = {
|
||||
webId: "6818365e6da47c0013cf7afd",
|
||||
availableProviders: {
|
||||
"text": [
|
||||
"azure-gpt-4o"
|
||||
],
|
||||
"visionEnabled": [
|
||||
"azure-gpt-4o"
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const API_KEYS = {};
|
||||
|
||||
// ===== TEXT GENERATION MODULE =====
|
||||
export interface TextProviderImplementation {
|
||||
generate(prompt: string): Promise<TextGenerationResult>;
|
||||
generateStream(prompt: string, onChunk: (text: string) => void): Promise<TextGenerationResult>;
|
||||
}
|
||||
|
||||
export interface VisionTextProviderImplementation extends TextProviderImplementation {
|
||||
generateWithImages(prompt: string, imageUrls: string[]): Promise<TextGenerationResult>;
|
||||
generateWithImagesStream(prompt: string, imageUrls: string[], onChunk: (text: string) => void): Promise<TextGenerationResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate text using a specific provider
|
||||
* @param prompt - The prompt to generate text from
|
||||
* @param provider - The provider to use
|
||||
* @returns A promise that resolves to the generated text
|
||||
*/
|
||||
export async function generateText(
|
||||
prompt: string,
|
||||
provider: string
|
||||
): Promise<TextGenerationResult> {
|
||||
if (!prompt) throw new Error('Prompt is required');
|
||||
if (!provider) throw new Error('Provider is required');
|
||||
|
||||
if (!CONFIG.availableProviders.text.includes(provider)) {
|
||||
throw new Error(`Provider ${provider} is not available`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await textProviderImplementations[provider].generate(prompt);
|
||||
} catch (error) {
|
||||
if (error instanceof AIServiceError) {
|
||||
throw error;
|
||||
} else {
|
||||
throw new AIServiceError(
|
||||
`Error generating text: ${error instanceof Error ? error.message : String(error)}`,
|
||||
500,
|
||||
provider
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate text using a specific provider and stream the results
|
||||
* @param prompt - The prompt to generate text from
|
||||
* @param onChunk - A callback function that receives a chunk of text, can be used to update a UI or display the text in real-time
|
||||
* @param provider - The provider to use
|
||||
* @returns A promise that resolves to the generated text
|
||||
*/
|
||||
export async function generateTextStream(
|
||||
prompt: string,
|
||||
onChunk: (text: string) => void,
|
||||
provider: string
|
||||
): Promise<TextGenerationResult> {
|
||||
if (!prompt) throw new Error('Prompt is required');
|
||||
if (!onChunk) throw new Error('onChunk callback is required');
|
||||
if (!provider) throw new Error('Provider is required');
|
||||
|
||||
if (!CONFIG.availableProviders.text.includes(provider)) {
|
||||
throw new Error(`Provider ${provider} is not available`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await textProviderImplementations[provider].generateStream(prompt, onChunk);
|
||||
} catch (error) {
|
||||
if (error instanceof AIServiceError) {
|
||||
throw error;
|
||||
} else {
|
||||
throw new AIServiceError(
|
||||
`Error streaming text: ${error instanceof Error ? error.message : String(error)}`,
|
||||
500,
|
||||
provider
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available text providers
|
||||
* @returns An array of all available text providers
|
||||
*/
|
||||
export function getTextProviders(): string[] {
|
||||
return CONFIG.availableProviders.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available vision-enabled providers
|
||||
* @returns An array of all available vision-enabled providers
|
||||
*/
|
||||
export function getVisionEnabledProviders(): string[] {
|
||||
return CONFIG.availableProviders.visionEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate text using a specific provider and provide images
|
||||
* @param prompt - The prompt to generate text from
|
||||
* @param imageUrls - An array of image URLs
|
||||
* @param provider - The provider to use
|
||||
* @returns A promise that resolves to the generated text
|
||||
*/
|
||||
export async function generateTextWithImages(
|
||||
prompt: string,
|
||||
imageUrls: string[],
|
||||
provider: string
|
||||
): Promise<TextGenerationResult> {
|
||||
if (!prompt) throw new Error('Prompt is required');
|
||||
if (!imageUrls || !Array.isArray(imageUrls)) throw new Error('Image URLs must be an array');
|
||||
if (!provider) throw new Error('Provider is required');
|
||||
|
||||
if (!CONFIG.availableProviders.visionEnabled.includes(provider)) {
|
||||
throw new Error(`Provider ${provider} does not support image inputs`);
|
||||
}
|
||||
|
||||
try {
|
||||
const implementation = textProviderImplementations[provider] as VisionTextProviderImplementation;
|
||||
return await implementation.generateWithImages(prompt, imageUrls);
|
||||
} catch (error) {
|
||||
if (error instanceof AIServiceError) {
|
||||
throw error;
|
||||
} else {
|
||||
throw new AIServiceError(
|
||||
`Error generating text with images: ${error instanceof Error ? error.message : String(error)}`,
|
||||
500,
|
||||
provider
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate text using a specific provider and provide images and stream the results
|
||||
* @param prompt - The prompt to generate text from
|
||||
* @param imageUrls - An array of image URLs
|
||||
* @param onChunk - A callback function that receives a chunk of text, can be used to update a UI or display the text in real-time
|
||||
* @param provider - The provider to use
|
||||
* @returns A promise that resolves to the generated text
|
||||
*/
|
||||
export async function generateTextWithImagesStream(
|
||||
prompt: string,
|
||||
imageUrls: string[],
|
||||
onChunk: (text: string) => void,
|
||||
provider: string
|
||||
): Promise<TextGenerationResult> {
|
||||
if (!prompt) throw new Error('Prompt is required');
|
||||
if (!imageUrls || !Array.isArray(imageUrls)) throw new Error('Image URLs must be an array');
|
||||
if (!onChunk) throw new Error('onChunk callback is required');
|
||||
if (!provider) throw new Error('Provider is required');
|
||||
|
||||
if (!CONFIG.availableProviders.visionEnabled.includes(provider)) {
|
||||
throw new Error(`Provider ${provider} does not support image inputs`);
|
||||
}
|
||||
|
||||
try {
|
||||
const implementation = textProviderImplementations[provider] as VisionTextProviderImplementation;
|
||||
return await implementation.generateWithImagesStream(prompt, imageUrls, onChunk);
|
||||
} catch (error) {
|
||||
if (error instanceof AIServiceError) {
|
||||
throw error;
|
||||
} else {
|
||||
throw new AIServiceError(
|
||||
`Error streaming text with images: ${error instanceof Error ? error.message : String(error)}`,
|
||||
500,
|
||||
provider
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shared implementation for all LiteLLM text providers
|
||||
/**
|
||||
* Generate text using a specific provider
|
||||
* @param prompt - The prompt to generate text from
|
||||
* @param integrationId - The integration ID to use
|
||||
* @param model - The model to use
|
||||
* @returns A promise that resolves to the generated text
|
||||
*/
|
||||
async function proxyGenerate(prompt: string, integrationId: string, model: string): Promise<TextGenerationResult> {
|
||||
const requestBody = {
|
||||
webId: CONFIG.webId,
|
||||
integrationId,
|
||||
prompt,
|
||||
stream: false,
|
||||
model
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('https://proxy.getcreatr.com/v1/proxy/llm', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new AIServiceError(
|
||||
`Proxy error: ${response.status} ${response.statusText}`,
|
||||
response.status,
|
||||
model
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
|
||||
return {
|
||||
text: data.data.choices[0].message.content,
|
||||
usage: {
|
||||
promptTokens: data.data.usage.prompt_tokens,
|
||||
completionTokens: data.data.usage.completion_tokens,
|
||||
totalTokens: data.data.usage.total_tokens
|
||||
},
|
||||
finishReason: data.data.choices[0].finish_reason
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AIServiceError) throw error;
|
||||
throw new Error(`Failed to call proxy: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate text using a specific provider and stream the results
|
||||
* @param prompt - The prompt to generate text from
|
||||
* @param onChunk - A callback function that receives a chunk of text, can be used to update a UI or display the text in real-time
|
||||
* @param integrationId - The integration ID to use
|
||||
* @param model - The model to use
|
||||
* @returns A promise that resolves to the generated text
|
||||
*/
|
||||
async function proxyGenerateStream(prompt: string, onChunk: (text: string) => void, integrationId: string, model: string): Promise<TextGenerationResult> {
|
||||
const requestBody = {
|
||||
webId: CONFIG.webId,
|
||||
integrationId,
|
||||
prompt,
|
||||
stream: true,
|
||||
model
|
||||
};
|
||||
|
||||
let fullText = '';
|
||||
let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
||||
let finishReason;
|
||||
|
||||
try {
|
||||
const response = await fetch('https://proxy.getcreatr.com/v1/proxy/llm/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new AIServiceError(
|
||||
`Proxy streaming error: ${response.status} ${response.statusText}`,
|
||||
response.status,
|
||||
model
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.body) throw new Error('Response body is null or undefined');
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buffer = '';
|
||||
let isStreamActive = true;
|
||||
|
||||
while (isStreamActive) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
isStreamActive = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
const events = buffer.split('\n\n');
|
||||
buffer = events.pop() || '';
|
||||
|
||||
for (const event of events) {
|
||||
if (event.startsWith('data: ')) {
|
||||
const data = event.slice(6).trim();
|
||||
|
||||
if (data === '[DONE]') {
|
||||
isStreamActive = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
const chunkContent = parsed.choices[0]?.delta?.content || '';
|
||||
if (chunkContent) {
|
||||
fullText += chunkContent;
|
||||
onChunk(chunkContent);
|
||||
}
|
||||
|
||||
if (parsed.choices[0]?.finish_reason) {
|
||||
finishReason = parsed.choices[0].finish_reason;
|
||||
}
|
||||
|
||||
// Update usage information from the final chunk
|
||||
if (parsed.usage) {
|
||||
usage = {
|
||||
promptTokens: parsed.usage.prompt_tokens,
|
||||
completionTokens: parsed.usage.completion_tokens,
|
||||
totalTokens: parsed.usage.total_tokens
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing stream chunk:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { text: fullText, usage, finishReason };
|
||||
} catch (error) {
|
||||
if (error instanceof AIServiceError) throw error;
|
||||
throw new Error(`Failed to stream from proxy: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Shared implementation for vision-enabled text providers
|
||||
/**
|
||||
* Generate text using a specific provider and provide images
|
||||
* @param prompt - The prompt to generate text from
|
||||
* @param imageUrls - An array of image URLs
|
||||
* @param integrationId - The integration ID to use
|
||||
* @param model - The model to use
|
||||
* @returns A promise that resolves to the generated text
|
||||
*/
|
||||
async function proxyGenerateWithImages(prompt: string, imageUrls: string[], integrationId: string, model: string): Promise<TextGenerationResult> {
|
||||
// Build content array with text and images for each message
|
||||
const content: ContentItem[] = [
|
||||
{ type: 'text', text: prompt }
|
||||
];
|
||||
|
||||
// Add each image URL to the content array
|
||||
for (const url of imageUrls) {
|
||||
content.push({
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: url
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create the request payload exactly matching the expected format
|
||||
const requestBody = {
|
||||
model,
|
||||
webId: CONFIG.webId,
|
||||
integrationId,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content
|
||||
}
|
||||
],
|
||||
stream: false
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('https://proxy.getcreatr.com/v1/proxy/llm', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new AIServiceError(
|
||||
`Vision proxy error: ${response.status} ${response.statusText}`,
|
||||
response.status,
|
||||
model
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
|
||||
return {
|
||||
text: data.data.choices[0].message.content,
|
||||
usage: {
|
||||
promptTokens: data.data.usage.prompt_tokens,
|
||||
completionTokens: data.data.usage.completion_tokens,
|
||||
totalTokens: data.data.usage.total_tokens
|
||||
},
|
||||
finishReason: data.data.choices[0].finish_reason
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AIServiceError) throw error;
|
||||
throw new Error(`Failed to call vision proxy: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate text using a specific provider and provide images and stream the results
|
||||
* @param prompt - The prompt to generate text from
|
||||
* @param imageUrls - An array of image URLs
|
||||
* @param onChunk - A callback function that receives a chunk of text, can be used to update a UI or display the text in real-time
|
||||
* @param integrationId - The integration ID to use
|
||||
* @param model - The model to use
|
||||
* @returns A promise that resolves to the generated text
|
||||
*/
|
||||
async function proxyGenerateWithImagesStream(prompt: string, imageUrls: string[], onChunk: (text: string) => void, integrationId: string, model: string): Promise<TextGenerationResult> {
|
||||
// Build content array with text and images
|
||||
const content: ContentItem[] = [
|
||||
{ type: 'text', text: prompt }
|
||||
];
|
||||
|
||||
// Add each image URL to the content array
|
||||
for (const url of imageUrls) {
|
||||
content.push({
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: url
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create the request payload exactly matching the expected format
|
||||
const requestBody = {
|
||||
model,
|
||||
webId: CONFIG.webId,
|
||||
integrationId,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content
|
||||
}
|
||||
],
|
||||
stream: true
|
||||
};
|
||||
|
||||
let fullText = '';
|
||||
let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
||||
let finishReason;
|
||||
|
||||
try {
|
||||
const response = await fetch('https://proxy.getcreatr.com/v1/proxy/llm/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new AIServiceError(
|
||||
`Vision proxy streaming error: ${response.status} ${response.statusText}`,
|
||||
response.status,
|
||||
model
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.body) throw new Error('Response body is null or undefined');
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buffer = '';
|
||||
let isStreamActive = true;
|
||||
|
||||
while (isStreamActive) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
isStreamActive = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
const events = buffer.split('\n\n');
|
||||
buffer = events.pop() || '';
|
||||
|
||||
for (const event of events) {
|
||||
if (event.startsWith('data: ')) {
|
||||
const data = event.slice(6).trim();
|
||||
|
||||
if (data === '[DONE]') {
|
||||
isStreamActive = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
const chunkContent = parsed.choices[0]?.delta?.content || '';
|
||||
if (chunkContent) {
|
||||
fullText += chunkContent;
|
||||
onChunk(chunkContent);
|
||||
}
|
||||
|
||||
if (parsed.choices[0]?.finish_reason) {
|
||||
finishReason = parsed.choices[0].finish_reason;
|
||||
}
|
||||
|
||||
// Update usage information from the final chunk
|
||||
if (parsed.usage) {
|
||||
usage = {
|
||||
promptTokens: parsed.usage.prompt_tokens,
|
||||
completionTokens: parsed.usage.completion_tokens,
|
||||
totalTokens: parsed.usage.total_tokens
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing stream chunk:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { text: fullText, usage, finishReason };
|
||||
} catch (error) {
|
||||
if (error instanceof AIServiceError) throw error;
|
||||
throw new Error(`Failed to stream from vision proxy: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Text provider implementations
|
||||
const textProviderImplementations: Record<string, TextProviderImplementation | VisionTextProviderImplementation> = {};
|
||||
|
||||
// Vision-enabled text provider: azure-gpt-4o
|
||||
textProviderImplementations["azure-gpt-4o"] = {
|
||||
async generate(prompt: string): Promise<TextGenerationResult> {
|
||||
return proxyGenerate(prompt, "67e52e6e8e1618ee9ba4117f", "azure-gpt-4o");
|
||||
},
|
||||
async generateStream(prompt: string, onChunk: (text: string) => void): Promise<TextGenerationResult> {
|
||||
return proxyGenerateStream(prompt, onChunk, "67e52e6e8e1618ee9ba4117f", "azure-gpt-4o");
|
||||
},
|
||||
async generateWithImages(prompt: string, imageUrls: string[]): Promise<TextGenerationResult> {
|
||||
return proxyGenerateWithImages(prompt, imageUrls, "67e52e6e8e1618ee9ba4117f", "azure-gpt-4o");
|
||||
},
|
||||
async generateWithImagesStream(prompt: string, imageUrls: string[], onChunk: (text: string) => void): Promise<TextGenerationResult> {
|
||||
return proxyGenerateWithImagesStream(prompt, imageUrls, onChunk, "67e52e6e8e1618ee9ba4117f", "azure-gpt-4o");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { type Metadata } from "next";
|
||||
|
||||
export const SITE_URL = "https://vocalinux.com";
|
||||
export const SITE_NAME = "Vocalinux";
|
||||
export const DEFAULT_OG_IMAGE_PATH = "/og-image.png";
|
||||
export const DEFAULT_OG_IMAGE_ALT =
|
||||
"Dictate locally. In any app. Offline voice dictation for Linux.";
|
||||
export const TWITTER_HANDLE = "@vocahq";
|
||||
export const VOCAHQ_URL = "https://vocahq.com";
|
||||
export const GITHUB_REPO_URL = "https://github.com/VocaHQ/vocalinux";
|
||||
export const X_URL = "https://x.com/vocahq";
|
||||
export const DISCORD_URL = "https://discord.gg/t6muquAJbm";
|
||||
export const EMAIL_URL = "mailto:hello@vocahq.com";
|
||||
|
||||
interface PageMetadataOptions {
|
||||
title: string;
|
||||
description: string;
|
||||
path?: string;
|
||||
keywords?: string[];
|
||||
}
|
||||
|
||||
const normalizePath = (path: string): string => {
|
||||
if (path === "/") {
|
||||
return "/";
|
||||
}
|
||||
|
||||
const trimmed = path.replace(/^\/+|\/+$/g, "");
|
||||
|
||||
// Files such as /vocalinux.png must not get the page trailing slash:
|
||||
// /vocalinux.png/ is a 404 on GitHub Pages.
|
||||
if (/\.[a-z0-9]+$/i.test(trimmed)) {
|
||||
return `/${trimmed}`;
|
||||
}
|
||||
|
||||
return `/${trimmed}/`;
|
||||
};
|
||||
|
||||
export const absoluteUrl = (path = "/"): string => {
|
||||
return new URL(normalizePath(path), SITE_URL).toString();
|
||||
};
|
||||
|
||||
export const buildPageMetadata = ({
|
||||
title,
|
||||
description,
|
||||
path = "/",
|
||||
keywords = [],
|
||||
}: 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: pageTitle,
|
||||
description,
|
||||
keywords,
|
||||
alternates: {
|
||||
canonical,
|
||||
languages: {
|
||||
"en-US": canonical,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
siteName: SITE_NAME,
|
||||
url: canonical,
|
||||
title,
|
||||
description,
|
||||
images: [
|
||||
{
|
||||
url: DEFAULT_OG_IMAGE_PATH,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: DEFAULT_OG_IMAGE_ALT,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title,
|
||||
description,
|
||||
images: [DEFAULT_OG_IMAGE_PATH],
|
||||
creator: TWITTER_HANDLE,
|
||||
site: TWITTER_HANDLE,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
DISCORD_URL,
|
||||
EMAIL_URL,
|
||||
GITHUB_REPO_URL,
|
||||
VOCAHQ_URL,
|
||||
X_URL,
|
||||
} from "@/lib/seo";
|
||||
|
||||
export type NavLink = {
|
||||
href: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const homeNav: NavLink[] = [
|
||||
{ href: "#how-it-works", label: "How it works" },
|
||||
{ href: "#engines", label: "Engines" },
|
||||
{ href: "#privacy", label: "Privacy" },
|
||||
{ href: "#install", label: "Install" },
|
||||
{ href: "#faq", label: "Questions" },
|
||||
];
|
||||
|
||||
export const pageNav: NavLink[] = [
|
||||
{ href: "/", label: "Home" },
|
||||
{ href: "/#how-it-works", label: "How it works" },
|
||||
{ href: "/install/", label: "Install" },
|
||||
{ href: "/screenshots/", label: "Screenshots" },
|
||||
{ href: "/faq/", label: "Questions" },
|
||||
];
|
||||
|
||||
export const mobileExtraNav: NavLink[] = [
|
||||
{ href: "/compare/", label: "Compare engines" },
|
||||
{ href: "/changelog/", label: "Changelog" },
|
||||
{ href: "/troubleshooting/", label: "Troubleshooting" },
|
||||
];
|
||||
|
||||
export const socialLinks: { href: string; label: string; icon: string }[] = [
|
||||
{ href: X_URL, label: "X", icon: "/brand/social/x.svg" },
|
||||
{ href: DISCORD_URL, label: "Discord", icon: "/brand/social/discord.svg" },
|
||||
{ href: GITHUB_REPO_URL, label: "GitHub", icon: "/brand/social/github.svg" },
|
||||
{ href: EMAIL_URL, label: "Email hello@vocahq.com", icon: "/brand/social/mail.svg" },
|
||||
];
|
||||
|
||||
export const footerGroups: { title: string; links: NavLink[] }[] = [
|
||||
{
|
||||
title: "Product",
|
||||
links: [
|
||||
{ href: "/#how-it-works", label: "How it works" },
|
||||
{ href: "/screenshots/", label: "Screenshots" },
|
||||
{ href: "/#engines", label: "Engines" },
|
||||
{ href: "/compare/", label: "Compare engines" },
|
||||
{ href: "/parakeet/", label: "Parakeet engine" },
|
||||
{ href: "/faster-whisper/", label: "Faster Whisper" },
|
||||
{ href: "/alternatives/", label: "Alternatives" },
|
||||
{ href: "/vs-nerd-dictation/", label: "vs Nerd Dictation" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Install",
|
||||
links: [
|
||||
{ href: "/install/", label: "Distro guides" },
|
||||
{ href: "/install/ubuntu/", label: "Ubuntu" },
|
||||
{ href: "/install/debian/", label: "Debian" },
|
||||
{ href: "/install/fedora/", label: "Fedora" },
|
||||
{ href: "/install/arch/", label: "Arch" },
|
||||
{ href: "/install/linux-mint/", label: "Linux Mint" },
|
||||
{ href: "/install/pop-os/", label: "Pop!_OS" },
|
||||
{ href: "/install/opensuse/", label: "openSUSE" },
|
||||
{ href: "/install/nixos/", label: "NixOS" },
|
||||
{ href: "/flatpak/", label: "Flatpak" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Guides",
|
||||
links: [
|
||||
{ href: "/offline/", label: "Offline" },
|
||||
{ href: "/wayland/", label: "Wayland" },
|
||||
{ href: "/hyprland-sway/", label: "Hyprland and Sway" },
|
||||
{ href: "/pipewire/", label: "PipeWire" },
|
||||
{ href: "/gpu-acceleration/", label: "GPU acceleration" },
|
||||
{ href: "/whisper-model-guide/", label: "Whisper models" },
|
||||
{ href: "/desktop-reliability/", label: "Reliability" },
|
||||
{ href: "/remote-api/", label: "Remote API" },
|
||||
{ href: "/voice-typing-vscode/", label: "VS Code" },
|
||||
{ href: "/writers/", label: "For writers" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Features",
|
||||
links: [
|
||||
{ href: "/dictation-pad/", label: "Dictation Pad" },
|
||||
{ href: "/transcribe-audio-files/", label: "Transcribe audio files" },
|
||||
{ href: "/custom-dictionary/", label: "Custom dictionary" },
|
||||
{ href: "/multilingual-dictation/", label: "Multilingual dictation" },
|
||||
{ href: "/global-shortcuts/", label: "Global shortcuts" },
|
||||
{ href: "/shortcuts/", label: "Voice commands" },
|
||||
{ href: "/local-gateway/", label: "Local gateway" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Project",
|
||||
links: [
|
||||
{ href: "/faq/", label: "FAQ" },
|
||||
{ href: "/troubleshooting/", label: "Troubleshooting" },
|
||||
{ href: "/privacy/", label: "Privacy" },
|
||||
{ href: "/changelog/", label: "Changelog" },
|
||||
{ href: GITHUB_REPO_URL, label: "Source" },
|
||||
{ href: VOCAHQ_URL, label: "VocaHQ" },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+24
@@ -0,0 +1,24 @@
|
||||
declare module "react-syntax-highlighter" {
|
||||
import type { ComponentType, CSSProperties, ReactNode } from "react";
|
||||
|
||||
export type SyntaxHighlighterStyle = Record<string, CSSProperties>;
|
||||
|
||||
export interface SyntaxHighlighterProps {
|
||||
children?: ReactNode;
|
||||
language?: string;
|
||||
style?: SyntaxHighlighterStyle;
|
||||
className?: string;
|
||||
customStyle?: CSSProperties;
|
||||
wrapLongLines?: boolean;
|
||||
}
|
||||
|
||||
const SyntaxHighlighter: ComponentType<SyntaxHighlighterProps>;
|
||||
|
||||
export default SyntaxHighlighter;
|
||||
}
|
||||
|
||||
declare module "react-syntax-highlighter/dist/esm/styles/hljs" {
|
||||
import type { SyntaxHighlighterStyle } from "react-syntax-highlighter";
|
||||
|
||||
export const atomOneDark: SyntaxHighlighterStyle;
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module 'tailwindcss/lib/util/flattenColorPalette' {
|
||||
function flattenColorPalette(colors: object): { [key: string]: string };
|
||||
export = flattenColorPalette;
|
||||
}
|
||||
Reference in New Issue
Block a user