This commit is contained in:
@@ -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',
|
||||
];
|
||||
@@ -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}`);
|
||||
Executable
+51
@@ -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()
|
||||
Executable
+14
@@ -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.'
|
||||
Reference in New Issue
Block a user