Show local AI activity feedback
Build and publish static assets / publish (push) Successful in 8m21s

This commit is contained in:
Ein Anderssono
2026-10-19 12:36:43 +02:00
parent 2794b8904c
commit 214f3b5cbd
9 changed files with 67 additions and 45 deletions
+27 -27
View File
@@ -40,28 +40,28 @@ jobs:
test -s public/static/piper-wasm-v1/voices/sv/sv_SE/lisa/medium/sv_SE-lisa-medium.onnx
test -s public/static/piper-wasm-v1/voices/sv/sv_SE/nst/medium/sv_SE-nst-medium.onnx
test -s public/static/piper-wasm-v1/voices/ATTRIBUTION.txt
test -s public/static/wllama-gemma3-v1/manifest.json
test -s public/static/wllama-gemma3-v1/licenses/GEMMA_USER_TERMS.html
test -s public/static/wllama-gemma3-v1/licenses/GEMMA_TERMS.html
test -s public/static/wllama-gemma3-v1/licenses/GEMMA_PROHIBITED_USE_POLICY.html
test -s public/static/wllama-gemma3-v1/licenses/GEMMA_NOTICE.txt
test -s public/static/wllama-gemma3-v1/licenses/GEMMA_SOURCE.txt
test -s public/static/wllama-gemma3-v1/licenses/WLLAMA_LICENSE.txt
test -s public/static/wllama-gemma3-v1/licenses/LLAMA_CPP_LICENSE.txt
test -s public/static/wllama-gemma3-v2/manifest.json
test -s public/static/wllama-gemma3-v2/licenses/GEMMA_USER_TERMS.html
test -s public/static/wllama-gemma3-v2/licenses/GEMMA_TERMS.html
test -s public/static/wllama-gemma3-v2/licenses/GEMMA_PROHIBITED_USE_POLICY.html
test -s public/static/wllama-gemma3-v2/licenses/GEMMA_NOTICE.txt
test -s public/static/wllama-gemma3-v2/licenses/GEMMA_SOURCE.txt
test -s public/static/wllama-gemma3-v2/licenses/WLLAMA_LICENSE.txt
test -s public/static/wllama-gemma3-v2/licenses/LLAMA_CPP_LICENSE.txt
python3 - <<'PY'
import json
m = json.load(open("public/static/wllama-gemma3-v1/manifest.json", encoding="utf-8"))
m = json.load(open("public/static/wllama-gemma3-v2/manifest.json", encoding="utf-8"))
assert m["model"]["bytes"] == 806058240
assert m["model"]["sha256"] == "8ccc5cd1f1b3602548715ae25a66ed73fd5dc68a210412eea643eb20eb75a135"
assert len(m["license"]["acceptance_id"]) == 64
required = ["user_terms", "terms", "prohibited_use_policy", "notice", "source", "runtime", "llama_cpp_runtime"]
assert all(m["license"][key].startswith("/static/") for key in required)
PY
test -s public/static/wllama-gemma3-v1/browser-assistance.yaml
test -s public/static/wllama-gemma3-v1/local-ai.js
test -s public/static/wllama-gemma3-v1/wllama/esm/index.js
test -s public/static/wllama-gemma3-v1/wllama/esm/wasm/wllama.wasm
test "$(stat -c %s public/static/wllama-gemma3-v1/models/gemma-3-1b-it-Q4_K_M.gguf)" = 806058240
test -s public/static/wllama-gemma3-v2/browser-assistance.yaml
test -s public/static/wllama-gemma3-v2/local-ai.js
test -s public/static/wllama-gemma3-v2/wllama/esm/index.js
test -s public/static/wllama-gemma3-v2/wllama/esm/wasm/wllama.wasm
test "$(stat -c %s public/static/wllama-gemma3-v2/models/gemma-3-1b-it-Q4_K_M.gguf)" = 806058240
grep -q 'version.json' Dockerfile
- name: Ensure Helm is available
@@ -174,21 +174,21 @@ jobs:
/static/piper-wasm-v1/onnx/ort-wasm-simd-threaded.wasm \
/static/piper-wasm-v1/piper/piper_phonemize.data \
/static/piper-wasm-v1/voices/sv/sv_SE/nst/medium/sv_SE-nst-medium.onnx \
/static/wllama-gemma3-v1/manifest.json \
/static/wllama-gemma3-v1/browser-assistance.yaml \
/static/wllama-gemma3-v1/local-ai.js \
/static/wllama-gemma3-v1/wllama/esm/index.js \
/static/wllama-gemma3-v1/wllama/esm/wasm/wllama.wasm \
/static/wllama-gemma3-v1/models/gemma-3-1b-it-Q4_K_M.gguf; do
/static/wllama-gemma3-v2/manifest.json \
/static/wllama-gemma3-v2/browser-assistance.yaml \
/static/wllama-gemma3-v2/local-ai.js \
/static/wllama-gemma3-v2/wllama/esm/index.js \
/static/wllama-gemma3-v2/wllama/esm/wasm/wllama.wasm \
/static/wllama-gemma3-v2/models/gemma-3-1b-it-Q4_K_M.gguf; do
curl -fsSI "https://ai-spel.k3s.k0.nu$path" >/dev/null
done
for path in \
/static/wllama-gemma3-v1/licenses/GEMMA_TERMS.html \
/static/wllama-gemma3-v1/licenses/GEMMA_USER_TERMS.html \
/static/wllama-gemma3-v1/licenses/GEMMA_PROHIBITED_USE_POLICY.html \
/static/wllama-gemma3-v1/licenses/GEMMA_NOTICE.txt \
/static/wllama-gemma3-v1/licenses/GEMMA_SOURCE.txt \
/static/wllama-gemma3-v1/licenses/WLLAMA_LICENSE.txt \
/static/wllama-gemma3-v1/licenses/LLAMA_CPP_LICENSE.txt; do
/static/wllama-gemma3-v2/licenses/GEMMA_TERMS.html \
/static/wllama-gemma3-v2/licenses/GEMMA_USER_TERMS.html \
/static/wllama-gemma3-v2/licenses/GEMMA_PROHIBITED_USE_POLICY.html \
/static/wllama-gemma3-v2/licenses/GEMMA_NOTICE.txt \
/static/wllama-gemma3-v2/licenses/GEMMA_SOURCE.txt \
/static/wllama-gemma3-v2/licenses/WLLAMA_LICENSE.txt \
/static/wllama-gemma3-v2/licenses/LLAMA_CPP_LICENSE.txt; do
curl -fsSI "https://ai-spel.k3s.k0.nu$path" >/dev/null
done
+1 -1
View File
@@ -7,7 +7,7 @@
- Git release commit/tag, image, Helm chart, and manifest use the same version.
- Use Harbor project `ai-spel`, Argo CD project `ai-spel`, and Kubernetes namespace `ai-spel`.
- `/static/piper-wasm-v1/` is immutable and cached for one year. Incompatible asset updates require a new path and coordinated frontend change.
- `/static/wllama-gemma3-v1/` serves the client adapter, pinned Wllama WebAssembly/runtime, the Gemma 3 1B Q4_K_M model, its manifest, and its YAML prompt. Browser runtime requests must remain same-origin under `/static/`; upstream URLs are allowed only in the static CI asset fetcher, never in client runtime code. Any changed adapter/model/runtime/prompt gets a new immutable path and coordinated frontend update.
- `/static/wllama-gemma3-v2/` serves the client adapter, pinned Wllama WebAssembly/runtime, the Gemma 3 1B Q4_K_M model, its manifest, and its YAML prompt. Browser runtime requests must remain same-origin under `/static/`; upstream URLs are allowed only in the static CI asset fetcher, never in client runtime code. Any changed adapter/model/runtime/prompt gets a new immutable path and coordinated frontend update.
- Gemma publication is blocked unless the static release includes the applicable Gemma Terms of Use copy, prohibited-use policy reference, required Gemma notice, model provenance, and third-party runtime/license notices. The frontend must obtain explicit user acceptance before model download/use; do not publish or enable Gemma until both the legal resources and the acceptance gate are implemented and verified in CI.
- Publish `/static/version.json` with `version` generated from `VERSION` in the same image as the Helm appVersion; keep it uncached and verify its value after rollout. The frontend footer displays this release independently from frontend and API versions.
- No secrets or runtime network access.
+1 -1
View File
@@ -11,7 +11,7 @@ Assets under `/static/piper-wasm-v1/` are served with `Cache-Control: public, ma
- `/static/piper-wasm-v1/voices/` — Alma, Lisa, and NST Swedish voices, plus attribution.
- `/static/piper-wasm-v1/onnx/` — ONNX Runtime WASM and ES module loader.
- `/static/piper-wasm-v1/piper/` — Piper phonemizer WASM and data.
- `/static/wllama-gemma3-v1/` — local AI client adapter, integrity-pinned Wllama WebAssembly runtime, Gemma 3 1B Q4_K_M (806 MB), static manifest, and YAML assistance prompt. The client downloads these only from this server and caches the model on-device.
- `/static/wllama-gemma3-v2/` — local AI client adapter, integrity-pinned Wllama WebAssembly runtime, Gemma 3 1B Q4_K_M (806 MB), static manifest, and YAML assistance prompt. The client downloads these only from this server and caches the model on-device.
- Gemma distribution must also include the applicable Gemma Terms of Use copy, required notice, prohibited-use policy reference, source/conversion provenance, and all runtime/model license notices. The frontend must obtain explicit acceptance before download or use. Do not release the Gemma files until those resources and the client acceptance gate are implemented and checked by CI.
- `/static/version.txt` — active deployment version, without long-lived caching.
- `/static/version.json` — active deployment version as `{"version":"<semver>"}`, generated from the same `VERSION` value used by the image and Helm chart and checked after deployment.
+19 -4
View File
@@ -1,6 +1,6 @@
/* Browser-only, best-effort assistance. All executable/model/prompt assets come from our static service. */
(function (global) {
const base = '/static/wllama-gemma3-v1/';
const base = '/static/wllama-gemma3-v2/';
const dbName = 'ai-spel-local-assistance-v1';
let enginePromise;
let loadedEngine;
@@ -161,12 +161,27 @@
}
async function assist(message, transcript, worldSnapshot) {
if (!enabled || !loadedEngine || !await isAccepted()) return {};
return boundedInfer('prepare_turn', {player_message: message.slice(0, 1200), recent_dialogue: transcript.slice(-12).map(line => line.slice(0, 240)), world_snapshot: JSON.stringify(worldSnapshot).slice(0, 1200)}, 5000);
announce({type: 'activity', text: 'Lokal Gemma 3 körs på den här enheten för att förbereda handlingen.'});
try {
const result = await boundedInfer('prepare_turn', {player_message: message.slice(0, 1200), recent_dialogue: transcript.slice(-12).map(line => line.slice(0, 240)), world_snapshot: JSON.stringify(worldSnapshot).slice(0, 1200)}, 5000);
announce({type: 'activity', text: 'Lokal Gemma 3 är klar. Originalhandlingen skickas nu vidare till berättartjänsten.'});
return result;
} catch (error) {
announce({type: 'activity', text: 'Lokal Gemma 3 kunde inte förbereda handlingen. Originaltexten skickas ändå vidare till berättartjänsten.'});
throw error;
}
}
async function offline(message, transcript, worldSnapshot) {
if (!enabled || !loadedEngine || !await isAccepted()) return '';
const result = await boundedInfer('offline_help', {player_message: message.slice(0, 1200), recent_dialogue: transcript.slice(-12).map(line => line.slice(0, 240)), world_snapshot: JSON.stringify(worldSnapshot).slice(0, 1200)}, 10000);
return String(result.offline_reply || 'Jag kan inte skapa ett lokalt förslag med den information som finns sparad.');
announce({type: 'activity', text: 'Lokal Gemma 3 arbetar offline med ett osparat hjälpförslag.'});
try {
const result = await boundedInfer('offline_help', {player_message: message.slice(0, 1200), recent_dialogue: transcript.slice(-12).map(line => line.slice(0, 240)), world_snapshot: JSON.stringify(worldSnapshot).slice(0, 1200)}, 10000);
announce({type: 'activity', text: 'Lokal Gemma 3 har skapat ett osparat hjälpförslag.'});
return String(result.offline_reply || 'Jag kan inte skapa ett lokalt förslag med den information som finns sparad.');
} catch (error) {
announce({type: 'activity', text: 'Lokal Gemma 3 kunde inte skapa ett offlineförslag.'});
throw error;
}
}
async function settings(command) {
const normalized = command.trim().toLocaleLowerCase('sv');
+3 -3
View File
@@ -21,21 +21,21 @@ server {
try_files $uri =404;
}
location = /static/wllama-gemma3-v1/manifest.json {
location = /static/wllama-gemma3-v2/manifest.json {
default_type application/json;
add_header Cache-Control "no-cache" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
try_files $uri =404;
}
location = /static/wllama-gemma3-v1/local-ai.js {
location = /static/wllama-gemma3-v2/local-ai.js {
default_type application/javascript;
add_header Cache-Control "no-cache" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
try_files $uri =404;
}
location ~ ^/static/wllama-gemma3-v1/licenses/ {
location ~ ^/static/wllama-gemma3-v2/licenses/ {
types {
text/html html;
text/plain txt;
+1 -1
View File
@@ -10,7 +10,7 @@ const root = process.cwd();
const output = join(root, 'public', 'static', assetPrefix);
const onnxSource = join(root, 'node_modules', 'onnxruntime-web', 'dist');
const piperSource = join(root, 'node_modules', '@diffusionstudio', 'piper-wasm', 'build');
const localAiPrefix = 'wllama-gemma3-v1';
const localAiPrefix = 'wllama-gemma3-v2';
const wllamaUrl = 'https://registry.npmjs.org/@wllama/wllama/-/wllama-3.8.1.tgz';
const wllamaSha512 = 'Pl7BEJhPU8OHnV/yhn3DFZYhLLOdSHh+4TND79WCftHnlUAdQPAsyc/CfIOY+4mVxvf5VW3Xw3Uf7BBfotS1gw==';
const gemmaUrl = 'https://huggingface.co/ggml-org/gemma-3-1b-it-GGUF/resolve/f9c28bcd85737ffc5aef028638d3341d49869c27/gemma-3-1b-it-Q4_K_M.gguf';
+4 -4
View File
@@ -12,14 +12,14 @@ grep -q 'application/wasm wasm;' nginx/default.conf
grep -Fq 'try_files $uri =404;' nginx/default.conf
grep -q 'version.json' Dockerfile
grep -q 'location = /static/version.json' nginx/default.conf
grep -q 'location = /static/wllama-gemma3-v1/manifest.json' nginx/default.conf
grep -q 'location = /static/wllama-gemma3-v1/local-ai.js' nginx/default.conf
grep -q 'location = /static/wllama-gemma3-v2/manifest.json' nginx/default.conf
grep -q 'location = /static/wllama-gemma3-v2/local-ai.js' nginx/default.conf
grep -q 'Cache-Control "no-cache"' nginx/default.conf
grep -q 'text/html html;' nginx/default.conf
grep -q 'application/octet-stream onnx data gguf;' nginx/default.conf
grep -q "client', 'local-ai.js" scripts/build-assets.mjs
grep -q 'wllama-gemma3-v1' scripts/build-assets.mjs
grep -q 'test -s public/static/wllama-gemma3-v1/local-ai.js' .gitea/workflows/publish.yaml
grep -q 'wllama-gemma3-v2' scripts/build-assets.mjs
grep -q 'test -s public/static/wllama-gemma3-v2/local-ai.js' .gitea/workflows/publish.yaml
grep -q 'lokal ai godkänn villkor' client/local-ai.js
grep -q 'acceptance_id' scripts/build-assets.mjs
grep -q 'GEMMA_TERMS.html' .gitea/workflows/publish.yaml
+3 -3
View File
@@ -21,7 +21,7 @@ test('browser local AI prompt declares non-authoritative, bounded assistance tas
test('local AI runtime and model are published only under the owned static origin', async () => {
const source = fs.readFileSync(path.join(__dirname, '..', 'scripts', 'build-assets.mjs'), 'utf8');
assert.ok(source.includes('wllama-gemma3-v1'));
assert.ok(source.includes('wllama-gemma3-v2'));
assert.ok(source.includes('wllama-3.8.1.tgz'));
assert.ok(source.includes('gemma-3-1b-it-Q4_K_M.gguf'));
assert.ok(source.includes('manifest.json'));
@@ -45,6 +45,6 @@ test('Gemma terms, prohibited-use policy, notice, provenance, and runtime licens
assert.match(terms, /must include the use restrictions/);
assert.doesNotMatch(terms, /<script|google-signin|google-analytics/);
const nginx = fs.readFileSync(path.join(__dirname, '..', 'nginx', 'default.conf'), 'utf8');
assert.match(nginx, /location = \/static\/wllama-gemma3-v1\/manifest\.json[\s\S]*?Cache-Control "no-cache"/);
assert.match(nginx, /location = \/static\/wllama-gemma3-v1\/local-ai\.js[\s\S]*?Cache-Control "no-cache"/);
assert.match(nginx, /location = \/static\/wllama-gemma3-v2\/manifest\.json[\s\S]*?Cache-Control "no-cache"/);
assert.match(nginx, /location = \/static\/wllama-gemma3-v2\/local-ai\.js[\s\S]*?Cache-Control "no-cache"/);
});
+8 -1
View File
@@ -8,7 +8,7 @@ const source = fs.readFileSync(path.join(__dirname, '..', 'client', 'local-ai.js
test('local AI runtime rejects assets outside the same-origin static service', () => {
assert.match(source, /parsed\.origin !== location\.origin/);
assert.match(source, /parsed\.pathname\.startsWith\('\/static\/'\)/);
assert.match(source, /\/static\/wllama-gemma3-v1\//);
assert.match(source, /\/static\/wllama-gemma3-v2\//);
assert.doesNotMatch(source, /huggingface\.co|registry\.npmjs\.org/);
});
@@ -19,6 +19,13 @@ test('local assistance includes preflight, bounded continuity, and unsaved offli
assert.match(source, /Jag kan inte skapa ett lokalt förslag/);
});
test('local AI reports when on-device inference starts, finishes, or fails', () => {
assert.match(source, /type: 'activity', text: 'Lokal Gemma 3 körs på den här enheten/);
assert.match(source, /Lokal Gemma 3 är klar\. Originalhandlingen skickas nu vidare/);
assert.match(source, /Lokal Gemma 3 kunde inte förbereda handlingen/);
assert.match(source, /Lokal Gemma 3 arbetar offline med ett osparat hjälpförslag/);
});
test('local diagnostics are opt-in, stored in IndexedDB, exportable, and deletable', () => {
assert.match(source, /ai-spel-local-ai-logs/);
assert.match(source, /indexedDB\.open/);