commit 4ebe1bb9ecdce7cd11f860262e890a4464dd5a06 Author: Ein Anderssono Date: Fri Oct 16 07:24:54 2026 +0200 Add static Piper asset service diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1cb6220 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.gitea +deploy +scripts +README.md +AGENTS.md +VERSION +node_modules +package-lock.json diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml new file mode 100644 index 0000000..3e41770 --- /dev/null +++ b/.gitea/workflows/publish.yaml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08a1ae5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +public/static/ +node_modules/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1ad6076 --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1094f02 --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/PIPER_VOICE_NOTICES.txt b/PIPER_VOICE_NOTICES.txt new file mode 100644 index 0000000..ccd5196 --- /dev/null +++ b/PIPER_VOICE_NOTICES.txt @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f97ddb --- /dev/null +++ b/README.md @@ -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. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/deploy/argocd/application.yaml b/deploy/argocd/application.yaml new file mode 100644 index 0000000..146d537 --- /dev/null +++ b/deploy/argocd/application.yaml @@ -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 diff --git a/deploy/helm/ai-spel-static/Chart.yaml b/deploy/helm/ai-spel-static/Chart.yaml new file mode 100644 index 0000000..c8b585e --- /dev/null +++ b/deploy/helm/ai-spel-static/Chart.yaml @@ -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" diff --git a/deploy/helm/ai-spel-static/templates/deployment.yaml b/deploy/helm/ai-spel-static/templates/deployment.yaml new file mode 100644 index 0000000..598e1d4 --- /dev/null +++ b/deploy/helm/ai-spel-static/templates/deployment.yaml @@ -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: {} diff --git a/deploy/helm/ai-spel-static/templates/ingress.yaml b/deploy/helm/ai-spel-static/templates/ingress.yaml new file mode 100644 index 0000000..26214f0 --- /dev/null +++ b/deploy/helm/ai-spel-static/templates/ingress.yaml @@ -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 }} diff --git a/deploy/helm/ai-spel-static/templates/service.yaml b/deploy/helm/ai-spel-static/templates/service.yaml new file mode 100644 index 0000000..24ffbfa --- /dev/null +++ b/deploy/helm/ai-spel-static/templates/service.yaml @@ -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 diff --git a/deploy/helm/ai-spel-static/values.yaml b/deploy/helm/ai-spel-static/values.yaml new file mode 100644 index 0000000..2565dfa --- /dev/null +++ b/deploy/helm/ai-spel-static/values.yaml @@ -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 diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..4edfd21 --- /dev/null +++ b/nginx/default.conf @@ -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; + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..eaab145 --- /dev/null +++ b/package-lock.json @@ -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" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..821bf8c --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/scripts/assets.mjs b/scripts/assets.mjs new file mode 100644 index 0000000..8061d44 --- /dev/null +++ b/scripts/assets.mjs @@ -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', +]; diff --git a/scripts/build-assets.mjs b/scripts/build-assets.mjs new file mode 100644 index 0000000..ec5e0fa --- /dev/null +++ b/scripts/build-assets.mjs @@ -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}`); diff --git a/scripts/bump-version.py b/scripts/bump-version.py new file mode 100755 index 0000000..6932179 --- /dev/null +++ b/scripts/bump-version.py @@ -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() diff --git a/scripts/check-static.sh b/scripts/check-static.sh new file mode 100755 index 0000000..4611067 --- /dev/null +++ b/scripts/check-static.sh @@ -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.' diff --git a/tests/assets.test.cjs b/tests/assets.test.cjs new file mode 100644 index 0000000..c0bbb93 --- /dev/null +++ b/tests/assets.test.cjs @@ -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')); +});