feat(identity): rename user-visible identity to voculinux with one-time legacy import

The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.

On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.

Refs #18
This commit is contained in:
soubarnak
2026-11-06 01:29:39 +05:30
parent c565e6d946
commit ea7be5f111
251 changed files with 2449 additions and 1991 deletions
+1 -1
View File
@@ -232,7 +232,7 @@ def _build(python: str, output: Path, diagnostics: Path) -> None:
env,
diagnostics / "inputs.log",
)
with tempfile.TemporaryDirectory(prefix="vocalinux-wheel-") as temporary:
with tempfile.TemporaryDirectory(prefix="voculinux-wheel-") as temporary:
scratch = Path(temporary)
source = scratch / "snapshot"
source.mkdir()
+1 -1
View File
@@ -6,7 +6,7 @@ check_deps() {
MISSING=0
WARNINGS=0
echo "Checking Vocalinux system dependencies..."
echo "Checking Voculinux system dependencies..."
echo ""
# Check for Python 3.11+ (requires-python in pyproject.toml)
+5 -5
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Regenerate src/vocalinux/utils/model_checksums.txt from upstream metadata.
Vocalinux downloads 40MB-2GB speech models at install time and on first run,
Voculinux downloads 40MB-2GB speech models at install time and on first run,
then hands them to native code (whisper.cpp loads its ggml files through
``ctypes.cdll``). Nothing about that is safe unless we know what we received,
so every downloader checks the file against a hash pinned *in this repository*
@@ -46,7 +46,7 @@ from typing import Iterable, NamedTuple
REPO_ROOT = Path(__file__).resolve().parents[1]
SRC = REPO_ROOT / "src"
MANIFEST = SRC / "vocalinux" / "utils" / "model_checksums.txt"
MANIFEST = SRC / "voculinux" / "utils" / "model_checksums.txt"
HF_REPO = "ggerganov/whisper.cpp"
HF_API = f"https://huggingface.co/api/models/{HF_REPO}?blobs=true"
@@ -65,7 +65,7 @@ class Entry(NamedTuple):
def _fetch_json(url: str):
request = urllib.request.Request(url, headers={"User-Agent": "vocalinux-checksum-generator"})
request = urllib.request.Request(url, headers={"User-Agent": "voculinux-checksum-generator"})
with urllib.request.urlopen(request, timeout=TIMEOUT) as response: # noqa: S310
return json.load(response)
@@ -93,7 +93,7 @@ def _download_and_hash(url: str, expected_md5: str) -> tuple[str, int]:
for rather than pinning either one.
"""
sha256, md5, size = hashlib.sha256(), hashlib.md5(), 0
request = urllib.request.Request(url, headers={"User-Agent": "vocalinux-checksum-generator"})
request = urllib.request.Request(url, headers={"User-Agent": "voculinux-checksum-generator"})
with urllib.request.urlopen(request, timeout=TIMEOUT) as response: # noqa: S310
for chunk in iter(lambda: response.read(1024 * 1024), b""):
sha256.update(chunk)
@@ -306,7 +306,7 @@ def _vosk_entries(wanted: Iterable[str], refresh: bool) -> list[Entry]:
def _render(entries: list[Entry], revision: str) -> str:
width = max(len(entry.filename) for entry in entries)
lines = [
"# Checksums for every speech model Vocalinux downloads.",
"# Checksums for every speech model Voculinux downloads.",
"#",
"# Generated by scripts/generate-model-checksums.py -- do not edit by hand.",
"# Regenerate with `just model-checksums` after adding a model to",
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Generate Vocalinux notification WAVs.
"""Generate Voculinux notification WAVs.
Writes 16-bit PCM mono files at 44.1 kHz into resources/sounds/:
+20 -20
View File
@@ -14,7 +14,7 @@
# fetch+reset over the existing clone.
#
# The remote is a local mirror so the gate answers for *this commit*: the tag
# is published from HEAD's tree and VOCALINUX_REPO_URL points the bootstrap at
# is published from HEAD's tree and VOCULINUX_REPO_URL points the bootstrap at
# it. A gate that cloned the public repo would install the last release, not
# the checkout under test. The tag's own install.sh is the bootstrap: the #701
# guardrail allows the bootstrap to come from main, and this commit's copy is
@@ -37,10 +37,10 @@ INSTALL_HOME="/home/$INSTALL_USER"
# Opaque on purpose: a name the public repository cannot already carry, so a
# silent fallback to GitHub fails loudly instead of installing the wrong ref.
GATE_TAG="${GATE_TAG:-v0.0.0-remote-gate}"
MIRROR="/srv/vocalinux-remote.git"
MIRROR="/srv/voculinux-remote.git"
BOOTSTRAP="/tmp/vl.sh"
CLONE_DIR="$INSTALL_HOME/.local/share/vocalinux-install"
REMOTE_VENV="$INSTALL_HOME/.local/share/vocalinux/venv"
CLONE_DIR="$INSTALL_HOME/.local/share/voculinux-install"
REMOTE_VENV="$INSTALL_HOME/.local/share/voculinux/venv"
fail() {
echo "FAIL: $*" >&2
@@ -48,11 +48,11 @@ fail() {
exit 1
}
# install.sh writes a full transcript under ~/.local/state/vocalinux/. It is the
# install.sh writes a full transcript under ~/.local/state/voculinux/. It is the
# only place the per-distro package manager output survives.
dump_install_log() {
local log
log="$(ls -1t "$INSTALL_HOME"/.local/state/vocalinux/install-*.log 2>/dev/null | head -n1 || true)"
log="$(ls -1t "$INSTALL_HOME"/.local/state/voculinux/install-*.log 2>/dev/null | head -n1 || true)"
if [ -n "$log" ]; then
echo "== last 200 lines of $log ==" >&2
tail -n 200 "$log" >&2
@@ -142,9 +142,9 @@ chmod 0644 "$BOOTSTRAP"
run_remote_install() {
# cd to $INSTALL_HOME so the bootstrap sees no checkout: local mode is chosen
# by a pyproject.toml naming vocalinux in the working directory, and the
# by a pyproject.toml naming voculinux in the working directory, and the
# remote path is the one under test.
su - "$INSTALL_USER" -c "cd '$INSTALL_HOME' && VOCALINUX_REPO_URL='file://$MIRROR' bash '$BOOTSTRAP' --auto --skip-models '--tag=$GATE_TAG'"
su - "$INSTALL_USER" -c "cd '$INSTALL_HOME' && VOCULINUX_REPO_URL='file://$MIRROR' bash '$BOOTSTRAP' --auto --skip-models '--tag=$GATE_TAG'"
}
# The user's literal `curl | bash`: installer code reaches bash on a pipe while
@@ -187,7 +187,7 @@ drip "$bootstrap" | bash -s -- "$@"
EOF
chmod 0755 "$PIPED_FEED"
run_remote_install_piped() {
su - "$INSTALL_USER" -c "cd '$INSTALL_HOME' && VOCALINUX_REPO_URL='file://$MIRROR' script -qec \"'$PIPED_FEED' '$BOOTSTRAP' --auto --skip-models '--tag=$GATE_TAG'\" /dev/null"
su - "$INSTALL_USER" -c "cd '$INSTALL_HOME' && VOCULINUX_REPO_URL='file://$MIRROR' script -qec \"'$PIPED_FEED' '$BOOTSTRAP' --auto --skip-models '--tag=$GATE_TAG'\" /dev/null"
}
assert_remote_install() {
@@ -223,20 +223,20 @@ assert_remote_install() {
[ -x "$REMOTE_VENV/bin/python" ] \
|| fail "no remote venv at $REMOTE_VENV; the handoff's --venv-dir did not reach the tagged installer"
# The remote marker relocates the activation helper to ~/.local/bin only when
# VOCALINUX_REMOTE_INSTALL survives the exec.
[ -f "$INSTALL_HOME/.local/bin/activate-vocalinux.sh" ] \
|| fail "no ~/.local/bin/activate-vocalinux.sh; the remote marker did not survive the handoff"
# VOCULINUX_REMOTE_INSTALL survives the exec.
[ -f "$INSTALL_HOME/.local/bin/activate-voculinux.sh" ] \
|| fail "no ~/.local/bin/activate-voculinux.sh; the remote marker did not survive the handoff"
# --skip-models forwarded: a model in the data dir means the tagged installer
# re-parsed arguments the handoff did not deliver.
if [ -d "$INSTALL_HOME/.local/share/vocalinux/models" ]; then
[ -z "$(find "$INSTALL_HOME/.local/share/vocalinux/models" -type f -print -quit)" ] \
if [ -d "$INSTALL_HOME/.local/share/voculinux/models" ]; then
[ -z "$(find "$INSTALL_HOME/.local/share/voculinux/models" -type f -print -quit)" ] \
|| fail "models were downloaded; --skip-models did not survive the handoff"
fi
echo "== Assert: the installed app is there and answers =="
[ -x "$INSTALL_HOME/.local/bin/vocalinux" ] || fail "no wrapper at ~/.local/bin/vocalinux"
[ -x "$INSTALL_HOME/.local/bin/vocalinux-gui" ] || fail "no wrapper at ~/.local/bin/vocalinux-gui"
[ -f "$INSTALL_HOME/.local/share/applications/vocalinux.desktop" ] \
[ -x "$INSTALL_HOME/.local/bin/voculinux" ] || fail "no wrapper at ~/.local/bin/voculinux"
[ -x "$INSTALL_HOME/.local/bin/voculinux-gui" ] || fail "no wrapper at ~/.local/bin/voculinux-gui"
[ -f "$INSTALL_HOME/.local/share/applications/voculinux.desktop" ] \
|| fail "no desktop entry; install_desktop_entry did not run"
# The version the wrapper reports must be the tag's own, not an assertion
@@ -244,13 +244,13 @@ assert_remote_install() {
local expected_version version_output
expected_version="$(grep -oP '__version__\s*=\s*"\K[^"]+' "$CLONE_DIR/src/vocalinux/version.py")" \
|| fail "cannot read __version__ from the clone's version.py"
version_output="$(su - "$INSTALL_USER" -c "'$INSTALL_HOME/.local/bin/vocalinux' --version")" \
version_output="$(su - "$INSTALL_USER" -c "'$INSTALL_HOME/.local/bin/voculinux' --version")" \
|| fail "the installed wrapper cannot report a version"
case "$version_output" in
*"$expected_version"*) ;;
*) fail "wrapper reports '$version_output', expected the tag's $expected_version" ;;
esac
echo " vocalinux --version: $version_output"
echo " voculinux --version: $version_output"
# Same blind spots as the local gate: the injection binaries are reached
# through subprocess, and the IBus runtime only where the package list
@@ -320,7 +320,7 @@ su - "$INSTALL_USER" -c "git -C '$CLONE_DIR' reset --hard '$AWAY_SHA' \
[ -z "$(git -C "$CLONE_DIR" tag -l "$GATE_TAG")" ] \
|| fail "the clone kept refs/tags/$GATE_TAG; the re-run proves nothing"
run_remote_install_piped || fail "the terminal-backed piped install exited non-zero over the existing clone"
grep -l "Updating existing clone" "$INSTALL_HOME"/.local/state/vocalinux/install-*.log >/dev/null 2>&1 \
grep -l "Updating existing clone" "$INSTALL_HOME"/.local/state/voculinux/install-*.log >/dev/null 2>&1 \
|| fail "no install log shows 'Updating existing clone'; the fetch arm did not run"
assert_remote_install
+7 -7
View File
@@ -17,7 +17,7 @@
# exercise a path no user takes.
#
# Local mode only: install.sh chooses it by finding a pyproject.toml naming
# vocalinux in the working directory, so extracting the tree and running from it
# voculinux in the working directory, so extracting the tree and running from it
# tests this commit. Remote mode (curl | bash, which clones the latest tag) is
# not covered here.
#
@@ -33,7 +33,7 @@ INSTALL_HOME="/home/$INSTALL_USER"
# Local mode puts the venv inside the tree, which is why the checkout is copied
# rather than written to: a bind-mounted host tree would come back owned by a
# container uid with a venv in it.
TREE="$INSTALL_HOME/vocalinux"
TREE="$INSTALL_HOME/voculinux"
fail() {
echo "FAIL: $*" >&2
@@ -41,11 +41,11 @@ fail() {
exit 1
}
# install.sh writes a full transcript under ~/.local/state/vocalinux/. It is the
# install.sh writes a full transcript under ~/.local/state/voculinux/. It is the
# only place the per-distro package manager output survives.
dump_install_log() {
local log
log="$(ls -1t "$INSTALL_HOME"/.local/state/vocalinux/install-*.log 2>/dev/null | head -n1 || true)"
log="$(ls -1t "$INSTALL_HOME"/.local/state/voculinux/install-*.log 2>/dev/null | head -n1 || true)"
if [ -n "$log" ]; then
echo "== last 200 lines of $log ==" >&2
tail -n 200 "$log" >&2
@@ -132,8 +132,8 @@ echo " install took $(( $(date +%s) - START ))s"
echo "== Smoke: is there an installation, and does it import =="
VENV="$TREE/venv"
[ -x "$VENV/bin/python" ] || fail "no venv interpreter at $VENV/bin/python"
[ -x "$INSTALL_HOME/.local/bin/vocalinux" ] || fail "no wrapper at ~/.local/bin/vocalinux"
[ -f "$INSTALL_HOME/.local/share/applications/vocalinux.desktop" ] \
[ -x "$INSTALL_HOME/.local/bin/voculinux" ] || fail "no wrapper at ~/.local/bin/voculinux"
[ -f "$INSTALL_HOME/.local/share/applications/voculinux.desktop" ] \
|| fail "no desktop entry; install_desktop_entry did not run"
# The injection backends are reached through subprocess, not GI, so no typelib
@@ -159,7 +159,7 @@ esac
# Through the wrapper rather than the venv directly: the wrapper is what the
# desktop entry and the user's PATH invoke, and it carries the GI_TYPELIB_PATH
# and pywhispercpp library-path resolution the bare console script does not.
su - "$INSTALL_USER" -c "'$INSTALL_HOME/.local/bin/vocalinux' --version" \
su - "$INSTALL_USER" -c "'$INSTALL_HOME/.local/bin/voculinux' --version" \
|| fail "the installed wrapper cannot report a version"
su - "$INSTALL_USER" -c "'$VENV/bin/python' - " <<'PY' || fail "the installation does not import"
+6 -6
View File
@@ -22,7 +22,7 @@ from pathlib import Path
from packaging.version import Version
ROOT = Path(__file__).resolve().parents[1]
TEMPLATE = ROOT / "packaging/aur/vocalinux/PKGBUILD"
TEMPLATE = ROOT / "packaging/aur/voculinux/PKGBUILD"
REPOSITORY = "VocaHQ/vocalinux"
@@ -33,14 +33,14 @@ def digest(path: Path) -> str:
def sdist_version(sdist: Path) -> str:
"""Require one canonical Vocalinux sdist with matching top-level metadata."""
match = re.fullmatch(r"vocalinux-([0-9][A-Za-z0-9.+!]*)\.tar\.gz", sdist.name)
"""Require one canonical Voculinux sdist with matching top-level metadata."""
match = re.fullmatch(r"voculinux-([0-9][A-Za-z0-9.+!]*)\.tar\.gz", sdist.name)
if not match:
raise ValueError("expected a canonical vocalinux-VERSION.tar.gz sdist")
raise ValueError("expected a canonical voculinux-VERSION.tar.gz sdist")
version = match[1]
if str(Version(version)) != version:
raise ValueError("sdist filename version is not canonical")
root = f"vocalinux-{version}"
root = f"voculinux-{version}"
with tarfile.open(sdist, "r:gz") as archive:
members = archive.getmembers()
if any(
@@ -55,7 +55,7 @@ def sdist_version(sdist: Path) -> str:
assert stream is not None
with stream:
info = BytesParser().parse(stream)
if info.get_all("Name") != ["vocalinux"] or info.get_all("Version") != [version]:
if info.get_all("Name") != ["voculinux"] or info.get_all("Version") != [version]:
raise ValueError("sdist metadata does not match its filename")
return version
+1 -1
View File
@@ -47,7 +47,7 @@ from packaging.utils import canonicalize_name
REPO_ROOT = Path(__file__).resolve().parents[1]
EXPORT = REPO_ROOT / "requirements" / "vad.txt"
DEPS_YAML = REPO_ROOT / "packaging" / "flatpak" / "python3-dependencies.yaml"
MANIFEST = REPO_ROOT / "packaging" / "flatpak" / "com.vocalinux.Vocalinux.yml"
MANIFEST = REPO_ROOT / "packaging" / "flatpak" / "com.voculinux.Voculinux.yml"
#: The environment the Flatpak build resolves for. GNOME 50 ships Python 3.13.
#: numpy's lock entry splits at `python_full_version >= '3.12'`; onnxruntime's
+7 -7
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""
Vocalinux Remote API Test Server
Voculinux Remote API Test Server
A simple mock server for testing Vocalinux's remote API feature.
A simple mock server for testing Voculinux's remote API feature.
Supports whisper.cpp (/inference) and OpenAI/FunASR-compatible
(/v1/audio/transcriptions) formats.
@@ -22,7 +22,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
class RemoteAPITestHandler(BaseHTTPRequestHandler):
"""HTTP request handler for testing Vocalinux remote API."""
"""HTTP request handler for testing Voculinux remote API."""
def log_message(self, format, *args):
"""Custom log format with timestamp."""
@@ -37,7 +37,7 @@ class RemoteAPITestHandler(BaseHTTPRequestHandler):
self.end_headers()
response = {
"status": "ok",
"message": "Vocalinux Remote API Test Server",
"message": "Voculinux Remote API Test Server",
"endpoints": [
"/inference (whisper.cpp format)",
"/v1/audio/transcriptions (OpenAI/FunASR format)",
@@ -138,7 +138,7 @@ class RemoteAPITestHandler(BaseHTTPRequestHandler):
if "test" in filename.lower():
return "This is a test transcription from the mock server."
elif "hello" in filename.lower():
return "Hello, this is Vocalinux testing the remote API."
return "Hello, this is Voculinux testing the remote API."
elif "long" in filename.lower():
return (
"This is a longer transcription to test how the system handles "
@@ -159,7 +159,7 @@ class DelayedHTTPServer(HTTPServer):
def main():
parser = argparse.ArgumentParser(
description="Vocalinux Remote API Test Server"
description="Voculinux Remote API Test Server"
)
parser.add_argument(
"--port",
@@ -183,7 +183,7 @@ def main():
args = parser.parse_args()
print("=" * 60)
print("Vocalinux Remote API Test Server")
print("Voculinux Remote API Test Server")
print("=" * 60)
print(f"Listening on: http://{args.host}:{args.port}")
print(f"Processing delay: {args.delay}s")
+1 -1
View File
@@ -26,7 +26,7 @@ import subprocess
import sys
MANIFEST = "SHA256SUMS"
PYPI_PROJECT = "vocalinux"
PYPI_PROJECT = "voculinux"
PYPI_SUFFIXES = (".whl", ".tar.gz")
PYPI_TIMEOUT = 30