diff --git a/nginx/default.conf b/nginx/default.conf index ae92bbe..70cd90f 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -12,7 +12,7 @@ server { location ^~ /tts/piper-wasm-v1/ { types { application/wasm wasm; - application/javascript js; + application/javascript js mjs; application/json json; application/octet-stream onnx; text/plain txt; diff --git a/piper-onnx-config.mjs b/piper-onnx-config.mjs new file mode 100644 index 0000000..03313dc --- /dev/null +++ b/piper-onnx-config.mjs @@ -0,0 +1,7 @@ +const assetRoot = '/tts/piper-wasm-v1/onnx/'; + +export function createOnnxWasmPaths(origin) { + const absoluteAssetRoot = new URL(assetRoot, origin).href; + return absoluteAssetRoot; +} + diff --git a/piper-worker.js b/piper-worker.js index 5ff91a4..116d559 100644 --- a/piper-worker.js +++ b/piper-worker.js @@ -1,5 +1,6 @@ import {TtsSession} from '@mintplex-labs/piper-tts-web'; import {localPiperModelUrl} from './piper-model-routing.mjs'; +import {createOnnxWasmPaths} from './piper-onnx-config.mjs'; const VOICE_ID = 'sv_SE-nst-medium'; const ASSET_ROOT = '/tts/piper-wasm-v1'; @@ -24,7 +25,7 @@ async function getSession(onProgress) { voiceId: VOICE_ID, progress: onProgress, wasmPaths: { - onnxWasm: `${ASSET_ROOT}/onnx/`, + onnxWasm: createOnnxWasmPaths(self.location.origin), piperData: `${ASSET_ROOT}/piper/piper_phonemize.data`, piperWasm: `${ASSET_ROOT}/piper/piper_phonemize.wasm`, }, diff --git a/scripts/build-piper.mjs b/scripts/build-piper.mjs index 71e15a3..e0cb93f 100644 --- a/scripts/build-piper.mjs +++ b/scripts/build-piper.mjs @@ -64,10 +64,16 @@ await mkdir(join(assetRoot, 'piper'), {recursive: true}); for (const file of voiceFiles) await downloadVoiceFile(file); await cp(join(root, 'PIPER_VOICE_NOTICES.txt'), join(assetRoot, 'voices', 'ATTRIBUTION.txt')); -const wasmFiles = (await readdir(onnxSource)) - .filter(name => name.endsWith('.wasm')); -if (wasmFiles.length === 0) throw new Error('ONNX Runtime WebAssembly files are missing'); -for (const file of wasmFiles) { +const onnxRuntimeFiles = (await readdir(onnxSource)) + .filter(name => name.endsWith('.wasm') || name.endsWith('.mjs')); +const requiredOnnxRuntimeFiles = [ + 'ort-wasm-simd-threaded.wasm', + 'ort-wasm-simd-threaded.mjs', +]; +for (const file of requiredOnnxRuntimeFiles) { + if (!onnxRuntimeFiles.includes(file)) throw new Error(`Missing ONNX Runtime asset: ${file}`); +} +for (const file of onnxRuntimeFiles) { await cp(join(onnxSource, file), join(assetRoot, 'onnx', file)); } for (const file of [piperWasm, piperData]) { @@ -106,7 +112,8 @@ for (const file of voiceFiles) { } const workerBundle = await (await import('node:fs/promises')).readFile(join(assetRoot, 'piper-worker.js'), 'utf8'); if (!workerBundle.includes('https://huggingface.co/diffusionstudio/piper-voices/resolve/main/') || - !workerBundle.includes('/tts/piper-wasm-v1/voices/')) { + !workerBundle.includes('/tts/piper-wasm-v1/voices/') || + !workerBundle.includes('/tts/piper-wasm-v1/onnx/')) { throw new Error('The Piper worker is missing its same-origin model download route.'); } diff --git a/scripts/check-frontend.sh b/scripts/check-frontend.sh index bbff805..4ba0b3f 100755 --- a/scripts/check-frontend.sh +++ b/scripts/check-frontend.sh @@ -11,6 +11,7 @@ grep -q 'Cross-Origin-Embedder-Policy "require-corp"' nginx/default.conf grep -q 'add_header Cross-Origin-Embedder-Policy "require-corp" always;' nginx/default.conf grep -q 'add_header Cross-Origin-Resource-Policy "same-origin" always;' nginx/default.conf grep -q 'application/javascript js;' nginx/default.conf +grep -q 'application/javascript js mjs;' nginx/default.conf grep -q "request('/version')" index.html grep -Fq "credentials: 'same-origin'" index.html grep -q 'id="dialogue" aria-label="Dialoghistorik" role="log" aria-live="polite"' index.html @@ -47,6 +48,9 @@ grep -q 'new Worker(workerUrl)' piper-runtime.js grep -q 'worker.js?v=' piper-runtime.js grep -q 'worker.onmessageerror' piper-runtime.js grep -q 'localPiperModelUrl' piper-worker.js +grep -q 'createOnnxWasmPaths(self.location.origin)' piper-worker.js +grep -q 'ort-wasm-simd-threaded.mjs' scripts/build-piper.mjs +grep -q 'ort-wasm-simd-threaded.wasm' scripts/build-piper.mjs grep -q 'PIPER_MODEL_ORIGIN' piper-model-routing.mjs grep -q '/tts/piper-wasm-v1/voices/' piper-model-routing.mjs grep -q 'same-origin.*voice' README.md diff --git a/tests/piper-onnx-config.test.cjs b/tests/piper-onnx-config.test.cjs new file mode 100644 index 0000000..1e78094 --- /dev/null +++ b/tests/piper-onnx-config.test.cjs @@ -0,0 +1,15 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +test('ONNX Runtime receives an absolute same-origin base URL', async () => { + const {createOnnxWasmPaths} = await import('../piper-onnx-config.mjs'); + const wasmPaths = createOnnxWasmPaths('https://ai-spel.k3s.k0.nu'); + assert.equal(wasmPaths, 'https://ai-spel.k3s.k0.nu/tts/piper-wasm-v1/onnx/'); + assert.equal(new URL('ort-wasm-simd-threaded.mjs', wasmPaths).href, + 'https://ai-spel.k3s.k0.nu/tts/piper-wasm-v1/onnx/ort-wasm-simd-threaded.mjs'); +}); + +test('ONNX Runtime base URL rejects an invalid worker origin', async () => { + const {createOnnxWasmPaths} = await import('../piper-onnx-config.mjs'); + assert.throws(() => createOnnxWasmPaths('undefined'), TypeError); +});