Skip to content
Merged
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
26 changes: 12 additions & 14 deletions .github/workflows/ci-windows.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Compile the Tauri shell on Windows.
# PRs stay --no-bundle. Windows installers are not shipped yet.
# Windows CI: unsigned MSI.
# Tags ship signed artifacts via release-windows.yml.
name: ci-windows

on:
Expand All @@ -14,19 +14,20 @@ concurrency:

jobs:
build:
name: build (windows-latest)
name: msi (windows-latest)
runs-on: windows-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: 20
node-version: 26
cache: npm
cache-dependency-path: |
package-lock.json
app/package-lock.json
sidecar/package-lock.json

- uses: dtolnay/rust-toolchain@stable

Expand All @@ -39,12 +40,9 @@ jobs:
npm ci
npm --prefix app ci

- name: Ensure sidecar-bundle exists
shell: bash
run: mkdir -p src-tauri/sidecar-bundle

- name: Compile shell (no bundle)
run: npm run build:ci
- name: Bundle MSI
run: npx tauri build -b msi

- name: Check release binary
run: test -f src-tauri/target/release/sayit-desktop.exe
- name: Check MSI
shell: bash
run: ls -lh src-tauri/target/release/bundle/msi/*.msi
55 changes: 55 additions & 0 deletions .github/workflows/release-windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Tag v* → signed MSI + GitHub Release assets (merges into latest.json).
# PR/master CI stays unsigned (ci-windows.yml). Do not merge the two.
name: release-windows

on:
push:
tags: ["v*"]

permissions:
contents: write

concurrency:
group: release-windows-${{ github.ref }}
cancel-in-progress: true

jobs:
windows:
name: publish (windows-latest)
runs-on: windows-latest

steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v7
with:
node-version: 26
cache: npm
cache-dependency-path: |
package-lock.json
app/package-lock.json
sidecar/package-lock.json

- uses: dtolnay/rust-toolchain@stable

- uses: swatinem/rust-cache@v2
with:
workspaces: ./src-tauri -> target

- name: Install JS deps
run: |
npm ci
npm --prefix app ci

- uses: tauri-apps/tauri-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
tagName: ${{ github.ref_name }}
releaseName: ${{ github.ref_name }}
generateReleaseNotes: true
args: >-
-b msi
--config {"bundle":{"createUpdaterArtifacts":true}}
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ Crucial reminders for future sessions. See LINUX.md for full architecture.
- Root deps: `@tauri-apps/cli`. App and sidecar have their own `package.json`.
- Dev: `npm run dev` starts sidecar + `tauri dev`. `beforeDevCommand` runs from project root with `--prefix app`.
- Config is `src-tauri/tauri.conf.json5` (`config-json5` on `tauri` + `tauri-build`). Version is `../package.json`; bump **root `package.json` and `src-tauri/Cargo.toml` together**, then tag `v*`.
- CI Linux (`ci-linux.yml`): unsigned `deb,rpm,appimage` on PRs and `master`. macOS/Windows stay `npm run build:ci` (`tauri build --no-bundle`). Local: `build:linux` (deb), `build:appimage`, `build:rpm`.
- Release: push a `v*` tag → `release-linux.yml` (`tauri-action`, signed AppImage + `latest.json`). Do **not** set `createUpdaterArtifacts` in the config (breaks unsigned PR CI). Sign with the Say It key (`~/.tauri/sayit.key`), never the Nucube `TAURI_SIGNING_*` env. `beforeBundleCommand` stages `src-tauri/sidecar-bundle/`.
- CI Linux (`ci-linux.yml`): unsigned `deb,rpm,appimage` on PRs and `master`. CI Windows (`ci-windows.yml`): unsigned `msi`. macOS stays `npm run build:ci` (`tauri build --no-bundle`). Local: `build:linux` (deb), `build:appimage`, `build:rpm`, `build:msi` (Windows host).
- Release: push a `v*` tag → `release-linux.yml` (signed AppImage + `latest.json`) and `release-windows.yml` (signed MSI, merges into the same release / `latest.json`). Do **not** set `createUpdaterArtifacts` in the config (breaks unsigned PR CI). Sign with the Say It key (`~/.tauri/sayit.key`), never the Nucube `TAURI_SIGNING_*` env. `beforeBundleCommand` runs `node scripts/prepare-sidecar-bundle.js` (keeps this-host onnxruntime only).
- GUI binary is `sayit-desktop`. CLI remains `sayit`.
- Sidecar search order: `$SAYIT_SIDECAR_DIR` → bundled resources → `~/.local/share/sayit/sidecar`.

Expand Down
31 changes: 17 additions & 14 deletions LINUX.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,18 +157,21 @@ disk. Anything that needs the API should resolve the token the same way.
bump root `package.json` and `src-tauri/Cargo.toml` together before a `v*` tag.
- CSP in `tauri.conf.json5` must keep `connect-src http://127.0.0.1:7878` or
the webview can't reach the sidecar.
- GUI packages (`npm run build:linux`) run `scripts/prepare-sidecar-bundle.sh`
then embed that tree as `bundle.resources` → `sidecar/`. The window binary
is `sayit-desktop` so it never shadows the CLI `sayit`. Spawn order:
- GUI packages run `node scripts/prepare-sidecar-bundle.js` then embed that
tree as `bundle.resources` → `sidecar/`. The script keeps only this-host
onnxruntime binaries (linux / darwin / win32). The window binary is
`sayit-desktop` so it never shadows the CLI `sayit`. Spawn order:
`$SAYIT_SIDECAR_DIR` → bundled resources → `~/.local/share/sayit/sidecar`.
If 7878 is already healthy, the GUI does not spawn a second engine.
- Auto-update is **AppImage only** (`tauri-plugin-updater`). Push a `v*`
tag; `release-linux.yml` runs `tauri-action`, which signs the AppImage
(CI secrets `TAURI_SIGNING_PRIVATE_KEY` / `_PASSWORD` — not another
project's global env), creates the GitHub Release, and uploads
`latest.json`. `createUpdaterArtifacts` is only in that job's `--config`
so PR AppImage CI stays unsigned. Settings → Check for updates is
user-initiated; it is not telemetry.
- Auto-update is **AppImage on Linux, MSI on Windows** (`tauri-plugin-updater`).
Push a `v*` tag; `release-linux.yml` and `release-windows.yml` run
`tauri-action`, which signs the bundles (CI secrets
`TAURI_SIGNING_PRIVATE_KEY` / `_PASSWORD` — not another project's global
env), creates/updates the GitHub Release, and merges `latest.json`.
`createUpdaterArtifacts` is only in those jobs' `--config` so PR CI stays
unsigned. Settings → Check for updates is user-initiated; it is not
telemetry. The Windows MSI is experimental: sidecar still needs Node on
PATH and mpv; playback/clipboard are Linux-oriented.

## 8. Known limitations (vs the original)

Expand All @@ -180,9 +183,9 @@ disk. Anything that needs the API should resolve the token the same way.
its phonemizer WASM is English-only, so those ids fail at generate.
The `VOICES` table in `engine.js` lists what actually works.
5. Single in-flight job; no queue (§3).
6. Linux only — nothing here is tested on macOS/Windows, though the sidecar
and CLI are platform-agnostic in principle (mpv/aplay are the
platform-specific bits).
6. Linux is the tested product. Windows CI ships an experimental MSI; macOS
still compiles without bundling. Sidecar/CLI are platform-agnostic in
principle (mpv/aplay and clipboard tools are the Linux-specific bits).

## 9. Invariants — do not break these

Expand All @@ -193,6 +196,6 @@ disk. Anything that needs the API should resolve the token the same way.
- **Models come from the catalog** — do not hardcode Hugging Face ids in the UI.
- **Offline after first model download**; no analytics, no telemetry, no
passive clipboard monitoring. AppImage update checks hit GitHub only
when the user clicks Check for updates.
when the user clicks Check for updates (AppImage / Windows MSI).
- CLI command surface stays compatible with the original where the feature
exists.
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ replacing Apple-specific layers with Tauri, Svelte, and kokoro-js.
<img src="docs/screenshots/desktop-speak.png" alt="Say It desktop app — Speak tab" width="720">
</p>

Linux (X11 and Wayland) is built and tested. macOS and Windows should compile;
help wanted.
Linux (X11 and Wayland) is built and tested. Windows CI builds an experimental
MSI (needs Node and mpv on the machine; playback is not a full Windows port).
macOS should compile; help wanted.

## Highlights

Expand Down Expand Up @@ -68,7 +69,9 @@ and selected.
### Desktop app

The Linux GUI is an **AppImage**. It embeds the sidecar. Needs **Node ≥ 20**
and **mpv** on the machine (same as the CLI). No sudo.
and **mpv** on the machine (same as the CLI). No sudo. Tags also publish an
experimental **Windows MSI** (same Node + mpv requirement; not a full Windows
port).

Download it from
[Releases](https://github.com/ildella/sayit/releases), then:
Expand All @@ -81,8 +84,8 @@ chmod +x SayIt-*.AppImage
The window binary is `sayit-desktop`; it does not replace the CLI `sayit`.
You can run both: whoever starts first owns port 7878; the other connects.

Auto-update (Settings → Check for updates) only applies to the AppImage.
`.deb` / `.rpm` stay distro packages.
Auto-update (Settings → Check for updates) applies to the AppImage and the
experimental Windows MSI. `.deb` / `.rpm` stay distro packages.

### Terminal

Expand Down Expand Up @@ -158,7 +161,8 @@ of spawning a second one.
```sh
npm run build:appimage # AppImage with sidecar inside (Linux CI)
npm run build:linux # .deb (optional)
npm run build:ci # compile the shell, skip installers (macOS/Windows CI)
npm run build:msi # MSI with sidecar inside (Windows CI; Windows host)
npm run build:ci # compile the shell, skip installers (macOS CI)
```

After pulling updates, re-run `npm run setup` and restart the service so a
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"build:linux": "tauri build -b deb",
"build:appimage": "tauri build -b appimage",
"build:rpm": "tauri build -b rpm",
"build:msi": "tauri build -b msi",
"setup": "sh scripts/setup-sidecar.sh",
"tauri": "tauri"
},
Expand Down
77 changes: 77 additions & 0 deletions scripts/prepare-sidecar-bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env node
// Stage a production sidecar tree for Tauri resources (GUI packages).
// Invoked by beforeBundleCommand — not by `tauri dev` or `build:ci`.
'use strict';

const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');

const ROOT = path.resolve(__dirname, '..');
const SRC = path.join(ROOT, 'sidecar');
const DEST = path.join(ROOT, 'src-tauri', 'sidecar-bundle');

function fail(msg) {
console.error(`prepare-sidecar-bundle: ${msg}`);
process.exit(1);
}

if (!fs.existsSync(path.join(SRC, 'package.json')) || !fs.existsSync(path.join(SRC, 'package-lock.json'))) {
fail('missing sidecar/package.json or package-lock.json');
}

fs.rmSync(DEST, { recursive: true, force: true });
fs.mkdirSync(DEST, { recursive: true });
fs.copyFileSync(path.join(SRC, 'package.json'), path.join(DEST, 'package.json'));
fs.copyFileSync(path.join(SRC, 'package-lock.json'), path.join(DEST, 'package-lock.json'));
fs.cpSync(path.join(SRC, 'src'), path.join(DEST, 'src'), { recursive: true });

const emptyNpmrc = path.join(os.tmpdir(), 'sayit-empty-npmrc');
fs.writeFileSync(emptyNpmrc, '');

const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const ci = spawnSync(npm, ['ci', '--omit=dev', `--userconfig=${emptyNpmrc}`], {
cwd: DEST,
stdio: 'inherit',
env: { ...process.env, npm_config_allow_scripts: '' },
shell: process.platform === 'win32',
});
if (ci.status !== 0) fail('npm ci failed');

if (!fs.existsSync(path.join(DEST, 'src', 'index.js'))) {
fail('src/index.js missing after copy');
}

// onnxruntime-node ships every OS/arch plus CUDA plugins. linuxdeploy walks
// those ELF files, then dies on libcublasLt. Keep this host only.
const ortOs = process.platform;
const ortArch = process.arch === 'arm64' ? 'arm64' : 'x64';
const ort = path.join(DEST, 'node_modules', 'onnxruntime-node', 'bin', 'napi-v3');
if (fs.existsSync(ort)) {
for (const name of fs.readdirSync(ort)) {
const p = path.join(ort, name);
if (fs.statSync(p).isDirectory() && name !== ortOs) {
fs.rmSync(p, { recursive: true, force: true });
}
}
const osDir = path.join(ort, ortOs);
if (fs.existsSync(osDir)) {
for (const name of fs.readdirSync(osDir)) {
const p = path.join(osDir, name);
if (fs.statSync(p).isDirectory() && name !== ortArch) {
fs.rmSync(p, { recursive: true, force: true });
}
}
const binDir = path.join(osDir, ortArch);
if (fs.existsSync(binDir)) {
for (const f of fs.readdirSync(binDir)) {
if (/onnxruntime_providers_(cuda|tensorrt)/i.test(f)) {
fs.rmSync(path.join(binDir, f), { force: true });
}
}
}
}
}

console.log(`prepare-sidecar-bundle: ${DEST}`);
48 changes: 2 additions & 46 deletions scripts/prepare-sidecar-bundle.sh
Original file line number Diff line number Diff line change
@@ -1,49 +1,5 @@
#!/bin/sh
# Stage a production sidecar tree for Tauri resources (GUI .deb / .rpm).
# Invoked by beforeBundleCommand — not by `tauri dev` or `build:ci`.
# Thin wrapper so existing `sh scripts/prepare-sidecar-bundle.sh` calls still work.
set -eu

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SRC="$ROOT/sidecar"
DEST="$ROOT/src-tauri/sidecar-bundle"

if [ ! -f "$SRC/package.json" ] || [ ! -f "$SRC/package-lock.json" ]; then
echo "prepare-sidecar-bundle: missing sidecar/package.json or package-lock.json" >&2
exit 1
fi

rm -rf "$DEST"
mkdir -p "$DEST"
cp "$SRC/package.json" "$SRC/package-lock.json" "$DEST/"
cp -R "$SRC/src" "$DEST/src"

cd "$DEST"
# Isolate from ~/.npmrc; package.json allowScripts still permits onnxruntime-node.
if ! npm_config_allow_scripts= npm ci --omit=dev --userconfig /dev/null; then
echo "prepare-sidecar-bundle: npm ci failed" >&2
exit 1
fi

if [ ! -f "$DEST/src/index.js" ]; then
echo "prepare-sidecar-bundle: src/index.js missing after copy" >&2
exit 1
fi

# onnxruntime-node ships every OS/arch plus CUDA plugins. linuxdeploy walks
# those ELF files, then dies on libcublasLt. Keep this host only.
case "$(uname -m)" in
x86_64) ort_arch=x64 ;;
aarch64|arm64) ort_arch=arm64 ;;
*) ort_arch=x64 ;;
esac
ort="$DEST/node_modules/onnxruntime-node/bin/napi-v3"
if [ -d "$ort" ]; then
find "$ort" -mindepth 1 -maxdepth 1 -type d ! -name linux -exec rm -rf {} +
if [ -d "$ort/linux" ]; then
find "$ort/linux" -mindepth 1 -maxdepth 1 -type d ! -name "$ort_arch" -exec rm -rf {} +
fi
rm -f "$ort/linux/$ort_arch"/libonnxruntime_providers_cuda.so \
"$ort/linux/$ort_arch"/libonnxruntime_providers_tensorrt.so
fi

echo "prepare-sidecar-bundle: $DEST"
exec node "$ROOT/scripts/prepare-sidecar-bundle.js"
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json5
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
devUrl: "http://localhost:1420",
beforeDevCommand: "npm --prefix app run dev",
beforeBuildCommand: "node -e \"require('fs').mkdirSync('src-tauri/sidecar-bundle',{recursive:true})\" && npm --prefix app run build",
beforeBundleCommand: "sh scripts/prepare-sidecar-bundle.sh",
beforeBundleCommand: "node scripts/prepare-sidecar-bundle.js",
},
app: {
withGlobalTauri: true,
Expand Down
Loading