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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/linux-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ jobs:
run: |
python3 -m pip install pillow fastapi python-multipart
python3 -m unittest discover -v
python3 -m compileall -q app.py backend_gemma.py backend_mlx.py backend_vlm.py image_preprocessing.py
python3 -m compileall -q app.py backend_gemma.py backend_gemma_cloud.py backend_mlx.py backend_vlm.py provider_config.py image_preprocessing.py snip.py
bash -n packaging/build-deb.sh packaging/install-user.sh packaging/localtex-launcher run.sh
shellcheck packaging/build-deb.sh packaging/install-user.sh packaging/localtex-launcher packaging/macos/build.sh run.sh
sed -n '/<script>/,/<\/script>/p' static/index.html | sed '/<script>/d;/<\/script>/d' | node --check

- name: Build Debian package
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/macos-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
- name: Run tests
run: |
python -m unittest discover -v
python -m py_compile backend_mlx.py packaging/macos/desktop_macos.py
python -m py_compile app.py backend_gemma_cloud.py backend_mlx.py provider_config.py packaging/macos/desktop_macos.py

- name: Build app, ZIP, and DMG
env:
Expand Down
92 changes: 83 additions & 9 deletions .github/workflows/windows-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ on:
options:
- cpu
- cuda
- vulkan

permissions:
contents: write
Expand Down Expand Up @@ -71,38 +72,111 @@ jobs:
python -m pip install --upgrade pip
python -m pip install -r requirements-windows.txt

- name: Replace runtime with CUDA wheels
if: env.ACCELERATION == 'cuda'
- name: Install Vulkan SDK
if: env.ACCELERATION == 'vulkan'
run: |
python -m pip uninstall -y llama-cpp-python torch
python -m pip install torch --index-url https://download.pytorch.org/whl/cu124
python -m pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu124
Set-Content -Path packaging/windows/cuda_enabled.txt -Value "CUDA build"
choco install vulkan-sdk -y --no-progress
$sdk = Get-ChildItem "C:\VulkanSDK" -Directory | Sort-Object Name -Descending | Select-Object -First 1
if (-not $sdk) { throw "Vulkan SDK installation was not found." }
"VULKAN_SDK=$($sdk.FullName)" >> $env:GITHUB_ENV
"$($sdk.FullName)\Bin" >> $env:GITHUB_PATH

- name: Select llama.cpp acceleration backend
run: |
& ./packaging/windows/build-llama-backend.ps1 -Acceleration $env:ACCELERATION

- name: Verify acceleration runtime
run: |
if ($env:ACCELERATION -eq "cuda") {
python -c "import torch; assert torch.version.cuda, 'CPU PyTorch wheel remained in CUDA build'; print('PyTorch CUDA', torch.version.cuda)"
}
@'
import os
from pathlib import Path

acceleration = os.environ["ACCELERATION"]
if acceleration in {"cuda", "vulkan"}:
import importlib.util

llama_spec = importlib.util.find_spec("llama_cpp")
if llama_spec is None or not llama_spec.submodule_search_locations:
raise ModuleNotFoundError("llama_cpp package is missing")
llama_lib = Path(next(iter(llama_spec.submodule_search_locations))) / "lib"
if acceleration == "cuda":
import torch

cuda_backend = llama_lib / "ggml-cuda.dll"
if not cuda_backend.is_file():
raise FileNotFoundError("CUDA llama.cpp backend DLL is missing")
imports = cuda_backend.read_bytes().lower()
for dependency in (b"cudart64_12.dll", b"cublas64_12.dll", b"nvcuda.dll"):
if dependency not in imports:
raise RuntimeError(f"{dependency.decode()} is not linked by ggml-cuda.dll")
torch_lib = Path(torch.__file__).parent / "lib"
for dependency in ("cudart64_12.dll", "cublas64_12.dll"):
if not (torch_lib / dependency).is_file():
raise FileNotFoundError(f"PyTorch CUDA runtime is missing {dependency}")
print(f"Verified CUDA backend and runtime DLLs in {llama_lib}")
elif acceleration == "vulkan":
vulkan_backend = llama_lib / "ggml-vulkan.dll"
if not vulkan_backend.is_file():
raise FileNotFoundError("Vulkan llama.cpp backend DLL is missing")
if b"vulkan-1.dll" not in vulkan_backend.read_bytes().lower():
raise RuntimeError("ggml-vulkan.dll is not linked to the Vulkan loader")
print(f"Verified Vulkan backend DLL in {llama_lib}")
else:
import llama_cpp
from llama_cpp import llama_cpp as native

info = native.llama_print_system_info().decode("utf-8", "replace")
print(info)
'@ | python -

- name: Run source checks
run: |
python -m py_compile desktop.py packaging/windows/generate_icon.py packaging/windows/generate_version_info.py
python -c "import ast, pathlib; [ast.parse(pathlib.Path(p).read_text(encoding='utf-8')) for p in ('app.py', 'backend_gemma.py', 'backend_vlm.py', 'image_preprocessing.py', 'snip.py')]"
$tokens = $null
$parseErrors = $null
$null = [System.Management.Automation.Language.Parser]::ParseFile("packaging/windows/build-llama-backend.ps1", [ref]$tokens, [ref]$parseErrors)
if ($parseErrors) { throw ($parseErrors | Out-String) }
python -c "import ast, pathlib; [ast.parse(pathlib.Path(p).read_text(encoding='utf-8')) for p in ('app.py', 'backend_gemma.py', 'backend_gemma_cloud.py', 'backend_mlx.py', 'backend_vlm.py', 'provider_config.py', 'image_preprocessing.py', 'snip.py')]"
python -m unittest discover -v
python packaging/windows/generate_version_info.py $env:RELEASE_TAG
python packaging/windows/generate_icon.py

- name: Build native desktop application
run: |
pyinstaller --clean --noconfirm packaging/windows/kevintex.spec
Copy-Item -Force "packaging/windows/KevinTex.exe.config" "dist/KevinTex/KevinTex.exe.config"

- name: Smoke-test packaged application
run: |
if (-not (Test-Path "dist/KevinTex/KevinTex.exe.config")) {
throw "Packaged .NET application configuration is missing."
}
$process = Start-Process -FilePath "dist/KevinTex/KevinTex.exe" -ArgumentList "--smoke-test" -PassThru -Wait
if ($process.ExitCode -ne 0) {
throw "Packaged application smoke test failed with exit code $($process.ExitCode)."
}
if (-not (Test-Path "dist/KevinTex/_internal/static/index.html")) {
throw "Packaged frontend data is missing."
}
if ($env:ACCELERATION -eq "cuda") {
foreach ($dependency in @("ggml-cuda.dll", "cudart64_12.dll", "cublas64_12.dll")) {
if (-not (Get-ChildItem "dist/KevinTex" -Recurse -Filter $dependency -File)) {
throw "Packaged CUDA build is missing $dependency."
}
}
}
if ($env:ACCELERATION -eq "vulkan") {
if (-not (Get-ChildItem "dist/KevinTex" -Recurse -Filter "ggml-vulkan.dll" -File)) {
throw "Packaged Vulkan build is missing ggml-vulkan.dll."
}
}

- name: Create versioned archive
run: |
$suffix = if ($env:ACCELERATION -eq "cuda") { "-cuda" } else { "" }
$suffix = if ($env:ACCELERATION -eq "cpu") { "" } else { "-$env:ACCELERATION" }
if ($env:ACCELERATION -eq "cuda") {
# GitHub release assets are capped below 2 GiB. The CUDA runtime is
# large, so publish standard 7-Zip multi-volume files that extract
Expand All @@ -112,7 +186,7 @@ jobs:
if ($LASTEXITCODE -ne 0) { throw "7-Zip packaging failed." }
"ARCHIVE_GLOB=$archive.*" >> $env:GITHUB_ENV
} else {
$archive = "KevinTex-$env:VERSION-windows-x64.zip"
$archive = "KevinTex-$env:VERSION-windows-x64$suffix.zip"
Compress-Archive -Path "dist/KevinTex/*" -DestinationPath $archive
"ARCHIVE_GLOB=$archive" >> $env:GITHUB_ENV
}
Expand Down
96 changes: 73 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@
<img src="assets/kevintex-banner.png" alt="KevinTex — Screenshot to LaTeX" width="900">
</p>

# KevinTex — Snip & Get, fully offline
# KevinTex — Snip & Get

A local, free, unlimited formula-image → LaTeX converter (a self-hosted
alternative to SimpleTex). Paste, drop, or **snip** a screenshot of a math
formula and get editable Markdown + LaTeX with a live rendered preview.
Recognition runs entirely on your machine using the quantized
Recognition defaults to the quantized
[Gemma 4 E2B-it](https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF)
vision-language model through llama.cpp — no cloud APIs, accounts, or quotas.
vision-language model through llama.cpp. On first launch, you can either
download those local weights or use hosted Gemma 4 through a Google AI Studio
API key.

## Features

- **Snip button** (the computer-with-+ icon) — click it, drag a screen region,
and the capture is converted instantly. Fully self-contained (Pillow +
tkinter region selector); no `gnome-screenshot`/`flameshot`/portal needed.
and the capture is converted instantly. It uses Pillow + tkinter directly,
with optional `gnome-screenshot`, `grim`, `spectacle`, or `ffmpeg` fallbacks
when the desktop session blocks Pillow access.
- **Paste (Ctrl+V), drag-and-drop, or browse** for a formula image (PNG/JPG/BMP/WEBP)
- **Text sharpener** with persisted Off / Auto / Strong modes — conservative
contrast normalization, smart upscaling, scan denoising, and edge sharpening
Expand All @@ -25,7 +28,8 @@ vision-language model through llama.cpp — no cloud APIs, accounts, or quotas.
fraction/root/integral/matrix starters. Drawings go directly to local Gemma.
- **Voice-to-LaTeX** — dictate a formula through the microphone; Gemma turns
spoken math into editable Markdown + LaTeX entirely on-device through MLX on
Apple Silicon and llama.cpp on Windows/Linux.
Apple Silicon and llama.cpp on Windows/Linux, or through the audio-capable
Gemini model when Google AI Studio is selected.
- **Image preparation tools** — rotate before OCR, invert dark screenshots, and
compare the original with the exact processed image sent to recognition.
- **Live KaTeX preview** (bundled locally — the app works with no internet at all)
Expand All @@ -37,22 +41,31 @@ vision-language model through llama.cpp — no cloud APIs, accounts, or quotas.
- **Recognize again** using the current source image and active Thinking setting
- **Download `.tex`** output and keyboard shortcuts (`Ctrl/Cmd+K`, `Ctrl/Cmd+S`)
- **Native desktop builds** for Ubuntu, Windows, and Apple Silicon macOS
- **Hardware acceleration** with CUDA on Linux/Windows and MLX/Metal on Apple Silicon
- **Hardware acceleration** with NVIDIA CUDA, AMD ROCm/HIP, Intel/AMD
Vulkan, Intel SYCL, and Apple MLX/Metal; optimized CPU builds remain available
- **Switchable backend** — Gemma is the default; `LOCALTEX_BACKEND=lfm-vl` or
`LOCALTEX_BACKEND=pix2tex` selects an alternative local backend. The macOS
application selects `LOCALTEX_BACKEND=mlx` automatically.
- **Local or cloud Gemma** — choose **Download model weights** or **Use Google
AI Studio API** on first launch. Change the choice later in Settings. The
hosted option uses `gemma-4-26b-a4b-it` for images and `gemini-3.5-flash`
for voice. Cloud images and voice recordings are sent to Google, and usage
follows your AI Studio quota/billing.

## Install

Prebuilt packages are attached to each [GitHub release](https://github.com/EV3KevinDEV/KevinTex/releases):

- **Ubuntu:** `kevintex_1.2.4_all.deb`
- **Windows:** portable `KevinTex-1.2.4-windows-x64.zip` containing `KevinTex.exe`
- **Apple Silicon macOS:** `KevinTex-1.2.4-macOS-arm64.dmg` or `.zip`
- **Ubuntu:** `kevintex_1.2.10_all.deb`
- **Windows:** portable `KevinTex-1.2.10-windows-x64.zip` containing `KevinTex.exe`
- **Windows GPU:** `KevinTex-1.2.10-windows-x64-cuda.7z.001` or
`KevinTex-1.2.10-windows-x64-vulkan.zip`
- **Apple Silicon macOS:** `KevinTex-1.2.10-macOS-arm64.dmg` or `.zip`

The Windows and macOS applications open in a native window. Model weights are
not bundled; they download to the current user's application-data directory on
first launch. The macOS build requires an M-series Mac and uses MLX/Metal.
The Windows and macOS applications open in a native window. Local model weights
are not bundled; if you choose the local provider, they download to the current
user's application-data directory. The macOS build requires an M-series Mac
and uses MLX/Metal for its local provider.

### Ubuntu desktop app

Expand All @@ -62,7 +75,7 @@ Two options:

```bash
packaging/build-deb.sh
sudo apt install ./dist/kevintex_1.2.4_all.deb
sudo apt install ./dist/kevintex_1.2.10_all.deb
```

**Current user only (no root):**
Expand All @@ -74,9 +87,36 @@ packaging/install-user.sh
Either way you get a **KevinTex** entry in the applications menu with its own
icon. Launching it starts the local server and opens the app in its own
native window (Chrome/Chromium app mode; falls back to your browser).
Closing the window stops the server. On a machine without the model
environment, the first launch shows a one-time setup dialog that downloads
PyTorch, llama.cpp, and the Gemma GGUF weights.
Closing the window stops the server. On first launch, the app shows a provider
setup screen. The local choice downloads PyTorch/llama.cpp model weights; the
AI Studio choice asks for a key from
<https://aistudio.google.com/app/apikey> and does not download model weights.

### Linux and Windows hardware support

| Hardware | Recommended backend | Linux | Windows |
| --- | --- | --- | --- |
| Intel or AMD CPU | CPU | Automatic fallback | Standard ZIP |
| NVIDIA GPU | CUDA 12.4 | Automatic when the driver is detected | CUDA archive |
| AMD GPU | ROCm/HIP | Automatic when ROCm is installed | Local HIP SDK build |
| Intel or AMD GPU | Vulkan | Automatic with Vulkan build tools | Vulkan ZIP |
| Intel GPU | SYCL/oneAPI | Explicit local build | Explicit local build |

The Linux launcher auto-detects a usable backend and stores the choice under
`~/.local/share/localtex/acceleration`. Override it for the next launch with:

```bash
KEVINTEX_ACCELERATION=cpu kevintex
KEVINTEX_ACCELERATION=rocm kevintex
KEVINTEX_ACCELERATION=vulkan kevintex
KEVINTEX_ACCELERATION=sycl kevintex
```

ROCm requires the AMD ROCm SDK and access to `/dev/kfd`. Vulkan source builds
need `build-essential cmake libvulkan-dev glslc spirv-headers`. SYCL requires
Intel oneAPI (`icx` and `icpx`). Changing the override rebuilds only the
llama.cpp Python backend; downloaded model weights remain untouched. Hardware
and driver support still follows each vendor's compatibility list.

The app window's **Snip** button (computer-with-+ icon) opens a fullscreen
region selector: drag a box around a formula and it's converted immediately —
Expand All @@ -89,9 +129,8 @@ tool required.
./run.sh
```

Then open http://127.0.0.1:8321 (the script opens it for you). The first
launch downloads the Gemma weights (one time); after that it
is fully offline.
Then open http://127.0.0.1:8321 (the script opens it for you). The first launch
asks whether to download the local Gemma weights or configure Google AI Studio.

## Manual setup (if moving to another machine)

Expand Down Expand Up @@ -124,9 +163,11 @@ The same fields are accepted by `POST /api/convert`; `/api/snip` accepts them as
query parameters. Conversion responses include the applied preprocessing
metadata.

`POST /api/voice` accepts a 16 kHz mono WAV file in the `audio` multipart field
and an optional `thinking` boolean. It is available with the default `gemma`
backend and with `LOCALTEX_BACKEND=mlx`.
`POST /api/voice` accepts a short mono WAV file in the `audio` multipart field
and an optional `thinking` boolean. It is available with local Gemma backends
and the hosted AI Studio provider. Cloud voice requests use Gemini's documented
[inline audio input](https://ai.google.dev/gemini-api/docs/generate-content/audio)
and are limited to 14 MiB to stay below its encoded request limit.

## Performance and resource controls

Expand All @@ -139,5 +180,14 @@ The following optional environment variables tune local inference:
- `LOCALTEX_N_GPU_LAYERS` — llama.cpp GPU offload (`-1` means all)
- `LOCALTEX_N_BATCH` — llama.cpp prompt batch size
- `LOCALTEX_N_THREADS` — CPU inference threads
- `KEVINTEX_ACCELERATION=auto|cpu|cuda|rocm|vulkan|sycl` — select the Linux
llama.cpp runtime (`auto` is the default)
- `LOCALTEX_MAX_UPLOAD_BYTES` — upload cap (default 20 MiB)
- `LOCALTEX_INFERENCE_QUEUE_TIMEOUT` — wait before returning busy
- `LOCALTEX_PROVIDER=local|cloud` — optionally select the Gemma provider before startup
- `GEMINI_API_KEY` — optionally provide the Google AI Studio key through the environment
- `LOCALTEX_GEMINI_AUDIO_MODEL` — override the cloud voice model (default `gemini-3.5-flash`)

The Settings UI stores a key in the per-user `provider.json` file with
user-only permissions where supported. The key is not stored in browser
`localStorage` or returned by the provider status API.
Loading
Loading