ZONOS3-Mini / tests /test_ui_js.py
fffiloni's picture
v0.3.4: add detailed rounded playback wave and refine audio player
589dcd3 verified
Raw History Blame Contribute Delete
5.55 kB
"""Executable contracts for waveform geometry and the public API UI adapter."""
import subprocess
from config import ROOT
def test_wave_energy_geometry_and_temporal_continuity():
source = (ROOT / 'assets/wave.js').read_text()
script = source + r'''
const assert = require('node:assert/strict');
assert.equal(voiceEnergy(Array(200).fill(0)),0);
assert.equal(voiceEnergy(Array(200).fill(.8)),0); // DC is not audible energy.
const tone=a=>Array.from({length:2048},(_,i)=>a*Math.sin(i*.18));
assert.ok(voiceEnergy(tone(.1))>voiceEnergy(tone(.015)));
assert.equal(wavePath(0),'M0 70 L1000 70');
assert.ok(voiceEnergy(tone(.015))>.45); // Quiet speech remains visibly responsive.
assert.equal(voiceEnergy(tone(.002)),0); // No animation floor in near-silence.
for(const phase of [0,1,3,12]) {
const d=wavePath(1,phase);
assert.ok(d.startsWith('M0 70 L20 70 C'));
assert.ok(d.endsWith('980.00 70.00 L1000 70'));
const c=[...d.matchAll(/C([\d.]+) ([\d.]+) ([\d.]+) ([\d.]+) ([\d.]+) ([\d.]+)/g)].map(x=>x.slice(1).map(Number));
assert.equal(c[0][1],70);assert.equal(c.at(-1)[3],70);
assert.ok(c.flatMap(p=>[p[1],p[3],p[5]]).every(y=>y>=5 && y<=135));
}
const motion=new WaveMotion();
motion.step(1,1/60);assert.ok(motion.level>0 && motion.level<.32);
for(let i=0;i<60;i++)motion.step(1,1/60);
assert.ok(motion.level>.899);
const before=motion.level;motion.step(0,1/60);assert.ok(motion.level<before && motion.level>.89);
for(let i=0;i<120;i++)motion.step(0,1/60);
assert.equal(motion.level,0);assert.equal(wavePath(motion.level,motion.phase),'M0 70 L1000 70');
const m30=new WaveMotion(),m120=new WaveMotion();
for(let i=0;i<30;i++)m30.step(.6,1/30);
for(let i=0;i<120;i++)m120.step(.6,1/120);
assert.ok(Math.abs(m30.level-m120.level)<.0001);
// A quarter second changes the crest positions even at steady amplitude.
const live=new WaveMotion();live.level=.7;const old=wavePath(live.level,live.phase);
for(let i=0;i<15;i++)live.step(.7,1/60);
assert.ok(live.phase>1);assert.notEqual(wavePath(live.level,live.phase),old);
'''
subprocess.run(['node','-e',script], check=True)
def test_generate_snapshots_custom_settings_in_original_api_order():
source = (ROOT / 'assets/generate.js').read_text()
script = r'''
const assert = require('node:assert/strict');
const settings = {source:'Voice clip',consent:true,normalize:false,seed:123,randomize:false,
temperature:.9,top_p:.8,top_k:27,repetition:1.7,rate:12.5};
const nodes = {'voice-options':{dataset:{settings:JSON.stringify(settings)}},
'status-state':{dataset:{}},'generate-speech':{},'speech-player':{dispatchEvent:()=>{}}};
global.document = {getElementById:id=>nodes[id]};
global.CustomEvent = class {};
const args=['script','picker','Preset','clip',false,true,0,true,.75,.69,80,2.4,0];
const snapshot = (''' + source + r''')(...args);
assert.deepEqual(snapshot,['script','picker','Voice clip','clip',true,false,123,false,.9,.8,27,1.7,12.5]);
assert.equal(nodes['status-state'].dataset.busy,'true');
assert.equal(nodes['generate-speech'].disabled,true);
'''
subprocess.run(['node','-e',script], check=True)
def test_activity_completion_restores_submit_after_validation_error():
source = (ROOT / 'assets/wave.js').read_text()
source += (ROOT / 'assets/player.js').read_text().split("const globalListeners=")[0]
script = r'''
const assert = require('node:assert/strict');
const generate = {disabled:true};
const hostNode = {dataset:{}};
const path = {setAttribute:()=>{}};
const svg = {setAttribute:()=>{},querySelector:()=>path};
global.element = {querySelector:()=>svg,isConnected:true};
global.document = {getElementById:id=>id==='generate-speech'?generate:hostNode};
global.window = {matchMedia:()=>({matches:true})};
global.cancelAnimationFrame=()=>{};global.requestAnimationFrame=()=>1;global.queueMicrotask=()=>{};global.tryAutoplay=()=>{};
''' + source + r'''
setActivity(true);assert.equal(hostNode.dataset.busy,'true');
setActivity(false);assert.equal(generate.disabled,false);assert.equal(hostNode.dataset.busy,'false');
'''
subprocess.run(['node','-e',script], check=True)
def test_playback_wave_retains_signal_texture_and_settles_to_silence():
source = (ROOT / 'assets/wave.js').read_text()
script = source + r'''
const assert=require('node:assert/strict');
const tone=cycles=>Float32Array.from({length:4096},(_,i)=>.12*Math.sin(2*Math.PI*cycles*i/4096));
const slow=new PlaybackWave(),fast=new PlaybackWave();
for(let i=0;i<50;i++){slow.step(tone(8),1/60);fast.step(tone(27),1/60);}
assert.ok(Math.abs(slow.level-fast.level)<.001); // Same energy, different texture.
assert.notEqual(slow.path(),fast.path());
const extrema=shape=>Array.from(shape).slice(1,-1).filter((v,i)=>(v-shape[i])*(shape[i+2]-v)<0).length;
assert.ok(extrema(fast.shape)>extrema(slow.shape)*2);
const raw=tone(18);raw[100]=NaN;raw[150]=Infinity;
fast.step(raw,1/60);
const d=fast.path();assert.ok(!/NaN|Infinity/.test(d));
assert.ok(d.startsWith('M0 70 L20 70 C')&&d.endsWith('980.00 70.00 L1000 70'));
const c=[...d.matchAll(/C([\d.]+) ([\d.]+) ([\d.]+) ([\d.]+) ([\d.]+) ([\d.]+)/g)].map(x=>x.slice(1).map(Number));
assert.ok(c.flatMap(p=>[p[1],p[3],p[5]]).every(y=>y>=6&&y<=134));
for(let i=0;i<120;i++)fast.step(null,1/60);
assert.equal(fast.path(),'M0 70 L1000 70');
for(let i=0;i<20;i++)fast.step(new Float32Array(4096).fill(.2),1/60);
assert.equal(fast.path(),'M0 70 L1000 70'); // DC does not create fake texture.
fast.step(tone(13),1/60);fast.reset();assert.equal(fast.path(),'M0 70 L1000 70');
'''
subprocess.run(['node','-e',script],check=True)