Add static Piper asset service
Build and publish static assets / publish (push) Failing after 9s

This commit is contained in:
Ein Anderssono
2026-10-16 07:24:54 +02:00
commit 4ebe1bb9ec
22 changed files with 1215 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
.git
.gitea
deploy
scripts
README.md
AGENTS.md
VERSION
node_modules
package-lock.json
+154
View File
@@ -0,0 +1,154 @@
name: Build and publish static assets
on:
push:
branches: [main]
tags: ["v*"]
permissions:
contents: write
jobs:
publish:
runs-on: large
if: ${{ !startsWith(github.ref, 'refs/heads/') || !contains(github.event.head_commit.message, '[auto-version-bump]') }}
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN }}
persist-credentials: true
- name: Check source and release metadata
shell: bash
run: |
set -euo pipefail
python3 scripts/bump-version.py --check
scripts/check-static.sh
- name: Fetch and verify pinned static assets
shell: bash
run: |
set -euo pipefail
npm install --no-audit --no-fund
npm run build:assets
test -s public/static/piper-wasm-v1/piper/piper_phonemize.wasm
test -s public/static/piper-wasm-v1/piper/piper_phonemize.data
test -s public/static/piper-wasm-v1/onnx/ort-wasm-simd-threaded.mjs
test -s public/static/piper-wasm-v1/onnx/ort-wasm-simd-threaded.wasm
test -s public/static/piper-wasm-v1/voices/sv/sv_SE/alma/medium/sv_SE-alma-medium.onnx
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
- name: Ensure Helm is available
shell: bash
env:
HELM_VERSION: v3.17.3
run: |
set -euo pipefail
if command -v helm >/dev/null 2>&1; then helm version --short; exit 0; fi
mkdir -p "$HOME/.local/bin" "$RUNNER_TEMP/helm"
archive="$RUNNER_TEMP/helm/helm-${HELM_VERSION}-linux-amd64.tar.gz"
curl -fsSLo "$archive" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
curl -fsSLo "$archive.sha256sum" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz.sha256sum"
(cd "$RUNNER_TEMP/helm" && sha256sum -c "$(basename "$archive").sha256sum")
tar -xzf "$archive" -C "$RUNNER_TEMP/helm"
install "$RUNNER_TEMP/helm/linux-amd64/helm" "$HOME/.local/bin/helm"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Lint and render Helm chart
shell: bash
run: |
set -euo pipefail
helm lint deploy/helm/ai-spel-static
helm template ai-spel-static deploy/helm/ai-spel-static --namespace ai-spel > "$RUNNER_TEMP/static-manifest.yaml"
grep -q 'kind: Deployment' "$RUNNER_TEMP/static-manifest.yaml"
grep -q 'kind: Service' "$RUNNER_TEMP/static-manifest.yaml"
grep -q 'path: /static' "$RUNNER_TEMP/static-manifest.yaml"
- name: Build and push commit image
if: startsWith(github.ref, 'refs/heads/')
shell: bash
env:
HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }}
HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }}
run: |
set -euo pipefail
printf '%s' "$HARBOR_PASSWORD" | docker login harbor.k3s.k0.nu --username "$HARBOR_USERNAME" --password-stdin
unset HARBOR_PASSWORD
version="$(cat VERSION)"
tag="harbor.k3s.k0.nu/ai-spel/static:edge-${GITHUB_SHA:0:12}"
docker build --platform linux/amd64 --pull --build-arg "STATIC_VERSION=$version" --tag "$tag" .
docker image push "$tag"
- name: Bump version and tag release commit
id: bump
if: startsWith(github.ref, 'refs/heads/')
shell: bash
run: |
set -euo pipefail
git fetch origin main --quiet
test "$(git rev-parse origin/main)" = "$GITHUB_SHA"
version="$(python3 scripts/bump-version.py)"
git config user.name "Gitea Actions"
git config user.email "actions@users.noreply.gitea"
git add VERSION deploy/helm/ai-spel-static/Chart.yaml package-lock.json
git commit -m "Release $version [auto-version-bump]"
git tag -a "v$version" -m "v$version"
git push origin HEAD:main "refs/tags/v$version"
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Publish versioned image, chart, and manifest
if: ${{ startsWith(github.ref, 'refs/tags/') || steps.bump.outputs.version != '' }}
shell: bash
env:
AUTO_RELEASE_VERSION: ${{ steps.bump.outputs.version }}
HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }}
HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }}
ORAS_VERSION: 1.2.3
run: |
set -euo pipefail
version="$(cat VERSION)"
if [[ "$GITHUB_REF" == refs/tags/* ]]; then test "$GITHUB_REF_NAME" = "v$version"; else test "$AUTO_RELEASE_VERSION" = "$version"; fi
python3 scripts/bump-version.py --check
printf '%s' "$HARBOR_PASSWORD" | docker login harbor.k3s.k0.nu --username "$HARBOR_USERNAME" --password-stdin
unset HARBOR_PASSWORD
image="harbor.k3s.k0.nu/ai-spel/static:$version"
docker build --platform linux/amd64 --pull --build-arg "STATIC_VERSION=$version" --tag "$image" .
docker image push "$image"
publish_dir="$(mktemp -d)"
trap 'rm -rf "$publish_dir"' EXIT
helm lint deploy/helm/ai-spel-static
helm package deploy/helm/ai-spel-static --destination "$publish_dir" --version "$version" --app-version "$version"
registry_config="$RUNNER_TEMP/helm/registry.json"
mkdir -p "$(dirname "$registry_config")"
printf '{}' > "$registry_config"
printf '%s' "$HARBOR_PASSWORD" | helm registry login harbor.k3s.k0.nu --username "$HARBOR_USERNAME" --password-stdin --registry-config "$registry_config"
helm push "$publish_dir/ai-spel-static-$version.tgz" oci://harbor.k3s.k0.nu/ai-spel --registry-config "$registry_config"
chart_metadata="$(helm show chart oci://harbor.k3s.k0.nu/ai-spel/ai-spel-static --version "$version" --registry-config "$registry_config")"
test "$(printf '%s\n' "$chart_metadata" | awk '$1 == "version:" { print $2 }')" = "$version"
helm template ai-spel-static "$publish_dir/ai-spel-static-$version.tgz" --namespace ai-spel > "$publish_dir/static-manifest.yaml"
mkdir -p "$RUNNER_TEMP/oras"
archive="$RUNNER_TEMP/oras/oras_${ORAS_VERSION}_linux_amd64.tar.gz"
curl -fsSLo "$archive" "https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_amd64.tar.gz"
echo 'b4efc97a91f471f323f193ea4b4d63d8ff443ca3aab514151a30751330852827 '"$archive" | sha256sum -c -
tar -xzf "$archive" -C "$RUNNER_TEMP/oras" oras
printf '%s' "$HARBOR_PASSWORD" | "$RUNNER_TEMP/oras/oras" login harbor.k3s.k0.nu --username "$HARBOR_USERNAME" --password-stdin
unset HARBOR_PASSWORD
(cd "$publish_dir" && "$RUNNER_TEMP/oras/oras" push --artifact-type application/vnd.ai-spel.static-manifest.v1 "harbor.k3s.k0.nu/ai-spel/static-manifest:$version" "static-manifest.yaml:application/yaml")
ready=false
for attempt in $(seq 1 120); do
deployed="$(curl -fsS https://ai-spel.k3s.k0.nu/static/version.txt 2>/dev/null || true)"
if [[ "$deployed" == "$version" ]]; then ready=true; break; fi
sleep 5
done
[[ "$ready" == true ]] || { echo "Static service did not report version $version"; exit 1; }
for path in \
/static/piper-wasm-v1/onnx/ort-wasm-simd-threaded.mjs \
/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; do
curl -fsSI "https://ai-spel.k3s.k0.nu$path" >/dev/null
done
+2
View File
@@ -0,0 +1,2 @@
public/static/
node_modules/
+10
View File
@@ -0,0 +1,10 @@
# AI-spel static asset service
- Own only public, version-pinned Piper and ONNX speech assets.
- Serve under `/static/` on `https://ai-spel.k3s.k0.nu`.
- `VERSION` is the only release version source. Use `scripts/bump-version.py`.
- Gitea Actions on the existing `large` runner verifies, builds, and publishes. Never run tests, builds, containers, images, or binaries locally; do not create runners.
- 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.
- No secrets or runtime network access.
+11
View File
@@ -0,0 +1,11 @@
FROM nginxinc/nginx-unprivileged:1.27-alpine
ARG STATIC_VERSION
RUN test -n "$STATIC_VERSION"
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY public/static/ /usr/share/nginx/html/static/
USER root
RUN printf '%s\n' "$STATIC_VERSION" > /usr/share/nginx/html/static/version.txt && \
chmod -R a+rX /usr/share/nginx/html/static
USER 101
EXPOSE 8080
+26
View File
@@ -0,0 +1,26 @@
Swedish Piper voice models
==========================
This service distributes the Swedish medium Piper models Alma, Lisa, and NST
from the rhasspy/piper-voices repository, pinned to revision
c10ece1aade47bb51c153c893d14e5bf8e5b7117. Model weights are published under
/static/piper-wasm-v1/voices/ after integrity verification in CI.
Alma (sv_SE-alma-medium)
Source: https://huggingface.co/rhasspy/piper-voices/tree/c10ece1aade47bb51c153c893d14e5bf8e5b7117/sv/sv_SE/alma/medium
The model card states CC BY 4.0 and identifies the upstream voice project at
https://huggingface.co/yeagersthlm/piper-voice-sv-alma.
Lisa (sv_SE-lisa-medium)
Source: https://huggingface.co/rhasspy/piper-voices/tree/c10ece1aade47bb51c153c893d14e5bf8e5b7117/sv/sv_SE/lisa/medium
The upstream model card describes this as fine-tuned from the Norwegian
talesyntese medium voice.
NST (sv_SE-nst-medium)
Source: https://huggingface.co/rhasspy/piper-voices/tree/c10ece1aade47bb51c153c893d14e5bf8e5b7117/sv/sv_SE/nst/medium
The upstream model card identifies the NST Swedish TTS dataset as CC0 and
credits KBLab at the National Library of Sweden for training the voice.
The upstream model files and model cards remain the source of truth for license
terms. Exact source revision and per-file integrity checks are in
scripts/assets.mjs.
+30
View File
@@ -0,0 +1,30 @@
# AI-spel static assets
This repository publishes Piper/ONNX runtime assets and Swedish voice models through a dedicated Nginx service. The game keeps the same origin: ingress routes `/static/` here, `/` to the frontend, and `/api` to Rust.
The service contains only public, version-pinned files. Gitea Actions on the existing `large` runner downloads and verifies model files, runs checks, builds a `linux/amd64` image, bumps the patch version, and publishes the image, Helm chart, and rendered manifest to Harbor. Argo CD tracks the latest chart in the existing `ai-spel` project and namespace.
## Asset URLs and caching
Assets under `/static/piper-wasm-v1/` are served with `Cache-Control: public, max-age=31536000, immutable`. Keep this path and its contents stable. To change assets, publish under a new asset path and update the frontend to reference it after the new static release is healthy.
- `/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/version.txt` — active deployment version, without long-lived caching.
The frontend worker remains bundled with the frontend; it fetches these large assets from same-origin static URLs. The browser does not request models from an external host.
## Argo CD bootstrap
After the first chart has been published, bootstrap the app once from `deploy/argocd/application.yaml`:
```sh
kubectl -n argocd apply -f deploy/argocd/application.yaml
```
It tracks the newest chart (`*`), uses automated sync/self-heal without pruning, and deploys to `ai-spel`. Later releases do not need manual Argo actions.
## Local policy
Do not build or run locally. CI runs static checks, tests, image build, and Helm lint/render on the existing runner; runtime verification happens on Kubernetes.
+1
View File
@@ -0,0 +1 @@
0.1.0
+22
View File
@@ -0,0 +1,22 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ai-spel-static
namespace: argocd
spec:
project: ai-spel
source:
repoURL: harbor.k3s.k0.nu/ai-spel
chart: ai-spel-static
targetRevision: "*"
helm:
releaseName: ai-spel-static
destination:
server: https://kubernetes.default.svc
namespace: ai-spel
syncPolicy:
automated:
prune: false
selfHeal: true
syncOptions:
- CreateNamespace=false
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: ai-spel-static
description: Long-lived static Piper assets for the Swedish life simulator
type: application
version: 0.1.0
appVersion: "0.1.0"
@@ -0,0 +1,64 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
replicas: {{ .Values.replicaCount }}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: {{ .Values.deployment.maxUnavailable }}
maxSurge: {{ .Values.deployment.maxSurge }}
minReadySeconds: {{ .Values.deployment.minReadySeconds }}
progressDeadlineSeconds: {{ .Values.deployment.progressDeadlineSeconds }}
selector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
imagePullSecrets:
- name: {{ .Values.image.pullSecretName | quote }}
containers:
- name: static
image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 2
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 10
resources:
{{- toYaml .Values.resources | nindent 12 }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsNonRoot: true
volumeMounts:
- name: nginx-cache
mountPath: /var/cache/nginx
- name: nginx-run
mountPath: /var/run
volumes:
- name: nginx-cache
emptyDir: {}
- name: nginx-run
emptyDir: {}
@@ -0,0 +1,24 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}
annotations:
spec:
ingressClassName: {{ .Values.ingress.className | quote }}
tls:
- hosts:
- {{ .Values.ingress.host | quote }}
secretName: {{ .Values.ingress.tlsSecret | quote }}
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: /static
pathType: Prefix
backend:
service:
name: {{ .Release.Name }}
port:
number: {{ .Values.service.port }}
{{- end }}
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
type: ClusterIP
selector:
app.kubernetes.io/instance: {{ .Release.Name }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
+25
View File
@@ -0,0 +1,25 @@
replicaCount: 1
deployment:
maxUnavailable: 0
maxSurge: 1
minReadySeconds: 5
progressDeadlineSeconds: 600
image:
repository: harbor.k3s.k0.nu/ai-spel/static
pullPolicy: IfNotPresent
pullSecretName: harbor-pull
service:
port: 80
targetPort: 8080
ingress:
enabled: true
className: nginx
host: ai-spel.k3s.k0.nu
tlsSecret: ai-spel-tls
resources:
requests:
cpu: 5m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
+33
View File
@@ -0,0 +1,33 @@
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
location = /healthz {
access_log off;
return 204;
}
location = /static/version.txt {
default_type text/plain;
add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always;
try_files $uri =404;
}
location ^~ /static/ {
types {
application/wasm wasm;
application/javascript js mjs;
application/json json;
application/octet-stream onnx data;
text/plain txt;
}
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
try_files $uri =404;
}
location / {
return 404;
}
}
+610
View File
@@ -0,0 +1,610 @@
{
"name": "ai-spel-static-assets",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ai-spel-static-assets",
"dependencies": {
"@diffusionstudio/piper-wasm": "1.0.0",
"@mintplex-labs/piper-tts-web": "1.0.3",
"esbuild": "0.25.5",
"onnxruntime-web": "1.20.1"
}
},
"node_modules/@diffusionstudio/piper-wasm": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@diffusionstudio/piper-wasm/-/piper-wasm-1.0.0.tgz",
"integrity": "sha512-4Hgh7wJ1PuGEVgVi1CpducESnyXwjFRJN4zXjYRpxgquM+Zh6Eqq8ZaiGpCKm4yXvT1Y2SPQXbv0dEckFD9NaQ==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
"integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@mintplex-labs/piper-tts-web": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@mintplex-labs/piper-tts-web/-/piper-tts-web-1.0.3.tgz",
"integrity": "sha512-vjYiEgigm0JHt9yKxj0euQd2eHIhtbGgWyeySLTPG+CdCH9RMf10WtczpeslboUYAvX5Eaxh6RMK+IuHU2rNhw==",
"license": "MIT",
"peerDependencies": {
"onnxruntime-web": "^1.18.0"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
"integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
"integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
"integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz",
"integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==",
"license": "BSD-3-Clause"
},
"node_modules/@types/node": {
"version": "26.6.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz",
"integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==",
"license": "MIT",
"dependencies": {
"undici-types": "~8.9.0"
}
},
"node_modules/esbuild": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.5",
"@esbuild/android-arm": "0.25.5",
"@esbuild/android-arm64": "0.25.5",
"@esbuild/android-x64": "0.25.5",
"@esbuild/darwin-arm64": "0.25.5",
"@esbuild/darwin-x64": "0.25.5",
"@esbuild/freebsd-arm64": "0.25.5",
"@esbuild/freebsd-x64": "0.25.5",
"@esbuild/linux-arm": "0.25.5",
"@esbuild/linux-arm64": "0.25.5",
"@esbuild/linux-ia32": "0.25.5",
"@esbuild/linux-loong64": "0.25.5",
"@esbuild/linux-mips64el": "0.25.5",
"@esbuild/linux-ppc64": "0.25.5",
"@esbuild/linux-riscv64": "0.25.5",
"@esbuild/linux-s390x": "0.25.5",
"@esbuild/linux-x64": "0.25.5",
"@esbuild/netbsd-arm64": "0.25.5",
"@esbuild/netbsd-x64": "0.25.5",
"@esbuild/openbsd-arm64": "0.25.5",
"@esbuild/openbsd-x64": "0.25.5",
"@esbuild/sunos-x64": "0.25.5",
"@esbuild/win32-arm64": "0.25.5",
"@esbuild/win32-ia32": "0.25.5",
"@esbuild/win32-x64": "0.25.5"
}
},
"node_modules/flatbuffers": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz",
"integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==",
"license": "SEE LICENSE IN LICENSE.txt"
},
"node_modules/guid-typescript": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
"integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
"license": "ISC"
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/onnxruntime-common": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.20.1.tgz",
"integrity": "sha512-YiU0s0IzYYC+gWvqD1HzLc46Du1sXpSiwzKb63PACIJr6LfL27VsXSXQvt68EzD3V0D5Bc0vyJTjmMxp0ylQiw==",
"license": "MIT"
},
"node_modules/onnxruntime-web": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.20.1.tgz",
"integrity": "sha512-TePF6XVpLL1rWVMIl5Y9ACBQcyCNFThZON/jgElNd9Txb73CIEGlklhYR3UEr1cp5r0rbGI6nDwwrs79g7WjoA==",
"license": "MIT",
"dependencies": {
"flatbuffers": "^1.12.0",
"guid-typescript": "^1.0.9",
"long": "^5.2.3",
"onnxruntime-common": "1.20.1",
"platform": "^1.3.6",
"protobufjs": "^7.2.4"
}
},
"node_modules/platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
"license": "MIT"
},
"node_modules/protobufjs": {
"version": "7.6.6",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz",
"integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.5",
"@protobufjs/eventemitter": "^1.1.1",
"@protobufjs/fetch": "^1.1.1",
"@protobufjs/float": "^1.0.2",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.1",
"@types/node": ">=13.7.0",
"long": "^5.3.2"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/undici-types": {
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
"license": "MIT"
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "ai-spel-static-assets",
"private": true,
"description": "Pinned Piper and ONNX static assets for AI-spel",
"scripts": {"build:assets": "node scripts/build-assets.mjs"},
"dependencies": {
"@diffusionstudio/piper-wasm": "1.0.0",
"@mintplex-labs/piper-tts-web": "1.0.3",
"esbuild": "0.25.5",
"onnxruntime-web": "1.20.1"
}
}
+17
View File
@@ -0,0 +1,17 @@
export const assetPrefix = 'piper-wasm-v1';
export const voiceRevision = 'c10ece1aade47bb51c153c893d14e5bf8e5b7117';
export const voiceRepository = 'https://huggingface.co/rhasspy/piper-voices/resolve';
export const voiceFiles = [
{path: 'sv/sv_SE/alma/medium/sv_SE-alma-medium.onnx', size: 63434611, sha256: 'f75455eb6afeb1db9e2905d5bd7e4115afcd3710e0d399b6a40981766d32fedc'},
{path: 'sv/sv_SE/alma/medium/sv_SE-alma-medium.onnx.json', size: 6059, gitBlobSha1: '7eaf3ef20af4a74e9cee9d29b98de3d9fcec0bdc'},
{path: 'sv/sv_SE/lisa/medium/sv_SE-lisa-medium.onnx', size: 63511038, sha256: '94cae912b31d6e9140d3f5160f1815951588600c7a9e43d539ba1e81a110d131'},
{path: 'sv/sv_SE/lisa/medium/sv_SE-lisa-medium.onnx.json', size: 7239, gitBlobSha1: 'b2b5fb0f8af2aae4ddfb81eaa7709c6b8faa412b'},
{path: 'sv/sv_SE/nst/medium/sv_SE-nst-medium.onnx', size: 63104526, sha256: 'df011f56825a59dd1efc080c38a65a1ef70407e60f63050e9246f43a3d7e471e'},
{path: 'sv/sv_SE/nst/medium/sv_SE-nst-medium.onnx.json', size: 4157, gitBlobSha1: '9766b9769436046a610a505fda925032e12a66b3'},
];
export const requiredOnnxFiles = [
'ort-wasm-simd-threaded.wasm',
'ort-wasm-simd-threaded.mjs',
];
+67
View File
@@ -0,0 +1,67 @@
import {createHash} from 'node:crypto';
import {createWriteStream} from 'node:fs';
import {cp, mkdir, readdir, rm, stat, rename, unlink} from 'node:fs/promises';
import {join} from 'node:path';
import {Readable, Transform} from 'node:stream';
import {pipeline} from 'node:stream/promises';
import {assetPrefix, requiredOnnxFiles, voiceFiles, voiceRepository, voiceRevision} from './assets.mjs';
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');
async function downloadVoiceFile(file) {
const destination = join(output, 'voices', file.path);
const temporary = `${destination}.part`;
await mkdir(join(destination, '..'), {recursive: true});
const response = await fetch(`${voiceRepository}/${voiceRevision}/${file.path}`);
if (!response.ok || !response.body) throw new Error(`Unable to download ${file.path}: HTTP ${response.status}`);
const sha256 = createHash('sha256');
const sha1 = createHash('sha1').update(Buffer.from(`blob ${file.size}\0`));
let size = 0;
const verify = new Transform({
transform(chunk, _encoding, callback) {
size += chunk.length;
sha256.update(chunk);
sha1.update(chunk);
callback(null, chunk);
},
});
try {
await pipeline(Readable.fromWeb(response.body), verify, createWriteStream(temporary));
if (size !== file.size || (file.sha256 && sha256.digest('hex') !== file.sha256) ||
(file.gitBlobSha1 && sha1.digest('hex') !== file.gitBlobSha1)) {
throw new Error(`Integrity check failed for ${file.path} (${size} bytes)`);
}
await rename(temporary, destination);
} catch (error) {
await unlink(temporary).catch(() => {});
throw error;
}
}
await rm(join(root, 'public', 'static'), {recursive: true, force: true});
await mkdir(join(output, 'onnx'), {recursive: true});
await mkdir(join(output, 'piper'), {recursive: true});
for (const file of voiceFiles) await downloadVoiceFile(file);
await cp(join(root, 'PIPER_VOICE_NOTICES.txt'), join(output, 'voices', 'ATTRIBUTION.txt'));
const onnxFiles = (await readdir(onnxSource)).filter(name => name.endsWith('.wasm') || name.endsWith('.mjs'));
for (const file of requiredOnnxFiles) {
if (!onnxFiles.includes(file)) throw new Error(`Missing ONNX Runtime asset: ${file}`);
}
for (const file of onnxFiles) await cp(join(onnxSource, file), join(output, 'onnx', file));
for (const file of ['piper_phonemize.wasm', 'piper_phonemize.data']) {
const source = join(piperSource, file);
const fileStat = await stat(source).catch(() => null);
if (!fileStat?.isFile() || fileStat.size === 0) throw new Error(`Missing Piper runtime file: ${file}`);
await cp(source, join(output, 'piper', file));
}
for (const file of voiceFiles) {
const downloaded = join(output, 'voices', file.path);
if ((await stat(downloaded)).size !== file.size) throw new Error(`Missing verified voice file: ${file.path}`);
}
console.log(`Static Piper assets prepared at ${output}`);
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Update and validate the repository's single release version source."""
import argparse
from pathlib import Path
import re
VERSION_FILE = Path("VERSION")
CHART_FILE = Path("deploy/helm/ai-spel-static/Chart.yaml")
SEMVER = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
def read_version() -> str:
version = VERSION_FILE.read_text().strip()
if not SEMVER.fullmatch(version):
raise SystemExit(f"Invalid semantic version in {VERSION_FILE}: {version}")
return version
def check(version: str) -> None:
chart = CHART_FILE.read_text()
chart_version = re.findall(r"(?m)^version:\s*(\S+)\s*$", chart)
app_version = re.findall(r'(?m)^appVersion:\s*"(\S+)"\s*$', chart)
if chart_version != [version] or app_version != [version]:
raise SystemExit(
f"Version drift: VERSION={version}, chart version={chart_version}, "
f"appVersion={app_version}"
)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true")
args = parser.parse_args()
current = read_version()
check(current)
if args.check:
print(current)
return
major, minor, patch = map(int, current.split("."))
next_version = f"{major}.{minor}.{patch + 1}"
chart = CHART_FILE.read_text()
chart = re.sub(r"(?m)^version:\s*" + re.escape(current) + r"\s*$", f"version: {next_version}", chart, count=1)
chart = re.sub(r'(?m)^appVersion:\s*"' + re.escape(current) + r'"\s*$', f'appVersion: "{next_version}"', chart, count=1)
VERSION_FILE.write_text(f"{next_version}\n")
CHART_FILE.write_text(chart)
print(next_version)
if __name__ == "__main__":
main()
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
grep -q 'runAsNonRoot: true' deploy/helm/ai-spel-static/templates/deployment.yaml
grep -q 'readOnlyRootFilesystem: true' deploy/helm/ai-spel-static/templates/deployment.yaml
grep -q 'maxUnavailable: 0' deploy/helm/ai-spel-static/values.yaml
grep -q 'maxSurge: 1' deploy/helm/ai-spel-static/values.yaml
grep -q 'path: /static' deploy/helm/ai-spel-static/templates/ingress.yaml
grep -q 'max-age=31536000, immutable' nginx/default.conf
grep -q 'application/javascript js mjs;' nginx/default.conf
grep -q 'application/wasm wasm;' nginx/default.conf
grep -Fq 'try_files $uri =404;' nginx/default.conf
node --test tests/*.test.cjs
echo 'Static asset source checks passed.'
+11
View File
@@ -0,0 +1,11 @@
const test = require('node:test');
const assert = require('node:assert/strict');
test('Piper asset manifest pins every Swedish voice and required ONNX loader files', async () => {
const {assetPrefix, voiceFiles, requiredOnnxFiles} = await import('../scripts/assets.mjs');
assert.equal(assetPrefix, 'piper-wasm-v1');
assert.deepEqual([...new Set(voiceFiles.map(file => file.path.match(/sv_SE\/(\w+)\//)?.[1]))].sort(), ['alma', 'lisa', 'nst']);
assert.ok(voiceFiles.every(file => Number.isInteger(file.size) && (file.sha256 || file.gitBlobSha1)));
assert.ok(requiredOnnxFiles.includes('ort-wasm-simd-threaded.mjs'));
assert.ok(requiredOnnxFiles.includes('ort-wasm-simd-threaded.wasm'));
});