Build Wisp: on-device transcription studio (web + native, one codebase)
Private, offline speech-to-text that runs Whisper on the user's own device — free, no account, no per-minute fees. Replaces Otter.ai / Rev. - Pure, tested engine: chunking, overlap timestamp-stitching, exports (SRT/VTT/TXT/MD/JSON), WAV codec, resampler, job queue, model catalog (142 tests). - Platform-abstracted TranscriptionEngine: transformers.js on web (loaded from CDN at runtime to dodge Metro's onnxruntime-web bundling limits), whisper.rn on native. Shared pipeline orchestrates decode -> chunk -> transcribe -> stitch. - Cross-platform StorageRepo (Dexie web / expo-sqlite native), Zod-validated. - UI: library + search, import, live-progress transcription, synced click-to-seek editor, multi-format export; model picker + privacy in settings. - Web ships as a single-page PWA with COOP/COEP isolation for threaded WASM; Docker (nginx) image + Traefik compose for wisp.briggen.dev. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
# .gitea/workflows/ci.yml
|
||||
# ---------------------------------------------------------------------------
|
||||
# Wisp CI/CD for Gitea Actions (GitHub-Actions-compatible syntax).
|
||||
#
|
||||
# Triggers:
|
||||
# - push to master -> test, then (on success) deploy-web + build-apk
|
||||
# - pull_request -> test only (no deploys)
|
||||
#
|
||||
# Jobs:
|
||||
# test Lint-gate: typecheck + vitest in the Bun container.
|
||||
# deploy-web Build the nginx web image and ship it to briggen.dev WITHOUT a
|
||||
# registry (docker save | gzip | scp | docker load | compose up).
|
||||
# build-apk Build the Android toolchain image, prebuild + sign + assemble a
|
||||
# release APK, scp it to the server as /srv/wisp/web/wisp.apk, and
|
||||
# upload it as a CI artifact.
|
||||
#
|
||||
# Required repo secrets (Settings -> Actions -> Secrets):
|
||||
# SSH_HOST, SSH_USER, SSH_KEY (deploy + apk upload)
|
||||
# ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD,
|
||||
# ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD (apk signing)
|
||||
# See docs/DEPLOY.md and scripts/gen-keystore.sh.
|
||||
#
|
||||
# Runner assumptions: a Gitea Actions runner with Docker available on the host
|
||||
# (the deploy/apk jobs run docker build/save/load and ssh/scp). The `test` job
|
||||
# runs inside the oven/bun:1 container.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
|
||||
# Cancel superseded runs on the same ref to save runner time.
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# =========================================================================
|
||||
# test — typecheck + unit tests. Gates both deploy jobs.
|
||||
# =========================================================================
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
# Run the test job directly inside the Bun image so bun is preinstalled and
|
||||
# the environment matches the web build's builder stage.
|
||||
container:
|
||||
image: oven/bun:1
|
||||
steps:
|
||||
# Check out the repository at the triggering commit.
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Install dependencies exactly as locked (fails on lockfile drift).
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
# TypeScript strict typecheck (tsc --noEmit).
|
||||
- name: Typecheck
|
||||
run: bun run typecheck
|
||||
|
||||
# Vitest unit/property tests (vitest run).
|
||||
- name: Test
|
||||
run: bun run test
|
||||
|
||||
# =========================================================================
|
||||
# deploy-web — build image, transfer registry-free, compose up on server.
|
||||
# Runs only on push to master, after `test` passes.
|
||||
# =========================================================================
|
||||
deploy-web:
|
||||
needs: test
|
||||
# Guard: only deploy for pushes to master (never on PRs or other branches).
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Build the production web image on the runner. Build context is the repo
|
||||
# root so the Dockerfile can install deps and run the Expo export.
|
||||
- name: Build web image
|
||||
run: docker build -f docker/web.Dockerfile -t wisp-web:latest .
|
||||
|
||||
# Make the server's host key known so the subsequent ssh/scp don't fail on
|
||||
# host-key verification and we are not vulnerable to a blind MITM. We key
|
||||
# off SSH_HOST; this writes the server's public host keys into known_hosts.
|
||||
- name: Add server to known_hosts
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
ssh-keyscan -H "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts
|
||||
chmod 644 ~/.ssh/known_hosts
|
||||
|
||||
# Install the deploy SSH private key from the secret into the agent.
|
||||
- name: Configure SSH key
|
||||
run: |
|
||||
install -m 600 /dev/null ~/.ssh/id_deploy
|
||||
printf '%s\n' "${{ secrets.SSH_KEY }}" > ~/.ssh/id_deploy
|
||||
chmod 600 ~/.ssh/id_deploy
|
||||
|
||||
# Registry-free image transfer: stream `docker save | gzip` straight over
|
||||
# ssh to `gunzip | docker load` on the server. Avoids writing a big tarball
|
||||
# to disk and avoids needing a Docker registry entirely.
|
||||
- name: Ship image to server (save | gzip | ssh docker load)
|
||||
run: |
|
||||
docker save wisp-web:latest | gzip \
|
||||
| ssh -i ~/.ssh/id_deploy "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" \
|
||||
'gunzip | docker load'
|
||||
|
||||
# Recreate the service from the freshly loaded image. The compose file must
|
||||
# already exist at /srv/wisp/docker-compose.yml (one-time server setup).
|
||||
# `pull_policy: never` in the compose file keeps it from trying a registry pull.
|
||||
- name: Restart service via docker compose
|
||||
run: |
|
||||
ssh -i ~/.ssh/id_deploy "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" \
|
||||
'docker compose -f /srv/wisp/docker-compose.yml up -d --remove-orphans'
|
||||
|
||||
# Optional: prune dangling images left behind by repeated loads so the
|
||||
# server disk doesn't fill up with old wisp-web layers.
|
||||
- name: Prune dangling images on server
|
||||
run: |
|
||||
ssh -i ~/.ssh/id_deploy "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" \
|
||||
'docker image prune -f'
|
||||
|
||||
# =========================================================================
|
||||
# build-apk — reproducible signed release APK, shipped to server + artifact.
|
||||
# Runs only on push to master, after `test` passes.
|
||||
# =========================================================================
|
||||
build-apk:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Build the Android toolchain image (JDK 17 + Android SDK + Bun). This is
|
||||
# the reproducible environment described in docker/android.Dockerfile.
|
||||
- name: Build Android builder image
|
||||
run: docker build -f docker/android.Dockerfile -t wisp-android:latest .
|
||||
|
||||
# Run the full Android build INSIDE the toolchain container with the repo
|
||||
# bind-mounted at /workspace. All secrets are passed via -e so they live
|
||||
# only in the container's environment (not on the command line / git).
|
||||
#
|
||||
# Steps inside the container:
|
||||
# 1. bun install (frozen) — JS deps for prebuild + autolinking.
|
||||
# 2. expo prebuild --platform android --no-install — generate android/.
|
||||
# 3. scripts/ci-android-sign.sh — decode keystore + patch build.gradle.
|
||||
# 4. ./gradlew assembleRelease — produce the signed release APK.
|
||||
#
|
||||
# The bind mount means the generated android/ + APK are visible on the
|
||||
# runner afterwards (for scp + artifact upload below).
|
||||
- name: Build signed release APK
|
||||
run: |
|
||||
docker run --rm \
|
||||
-v "${{ github.workspace }}:/workspace" \
|
||||
-w /workspace \
|
||||
-e ANDROID_KEYSTORE_BASE64="${{ secrets.ANDROID_KEYSTORE_BASE64 }}" \
|
||||
-e ANDROID_KEYSTORE_PASSWORD="${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" \
|
||||
-e ANDROID_KEY_ALIAS="${{ secrets.ANDROID_KEY_ALIAS }}" \
|
||||
-e ANDROID_KEY_PASSWORD="${{ secrets.ANDROID_KEY_PASSWORD }}" \
|
||||
wisp-android:latest \
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
bun install --frozen-lockfile
|
||||
bunx expo prebuild --platform android --no-install
|
||||
bash scripts/ci-android-sign.sh
|
||||
cd android
|
||||
./gradlew assembleRelease --no-daemon
|
||||
'
|
||||
|
||||
# Locate the assembled APK. The default Gradle output for the release variant
|
||||
# is android/app/build/outputs/apk/release/app-release.apk. Capture the path
|
||||
# for the next steps and fail loudly if it's missing.
|
||||
- name: Locate APK
|
||||
id: apk
|
||||
run: |
|
||||
APK_PATH="android/app/build/outputs/apk/release/app-release.apk"
|
||||
if [[ ! -f "$APK_PATH" ]]; then
|
||||
echo "APK not found at $APK_PATH" >&2
|
||||
find android/app/build/outputs -name '*.apk' -print >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "path=$APK_PATH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Known_hosts + SSH key for the scp upload (same pattern as deploy-web).
|
||||
- name: Add server to known_hosts
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
ssh-keyscan -H "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts
|
||||
chmod 644 ~/.ssh/known_hosts
|
||||
|
||||
- name: Configure SSH key
|
||||
run: |
|
||||
install -m 600 /dev/null ~/.ssh/id_deploy
|
||||
printf '%s\n' "${{ secrets.SSH_KEY }}" > ~/.ssh/id_deploy
|
||||
chmod 600 ~/.ssh/id_deploy
|
||||
|
||||
# Copy the signed APK to the server where nginx serves it at /wisp.apk.
|
||||
# docker-compose mounts /srv/wisp/web/wisp.apk into the container's web root.
|
||||
# Ensure the target dir exists, then scp the APK into place.
|
||||
- name: Upload APK to server (/srv/wisp/web/wisp.apk)
|
||||
run: |
|
||||
ssh -i ~/.ssh/id_deploy "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" \
|
||||
'mkdir -p /srv/wisp/web'
|
||||
scp -i ~/.ssh/id_deploy \
|
||||
"${{ steps.apk.outputs.path }}" \
|
||||
"${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/srv/wisp/web/wisp.apk"
|
||||
|
||||
# Also publish the APK as a CI artifact so it can be downloaded from the
|
||||
# run's summary page even without server access.
|
||||
- name: Upload APK artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wisp-release-apk
|
||||
path: ${{ steps.apk.outputs.path }}
|
||||
if-no-files-found: error
|
||||
Reference in New Issue
Block a user