ZONOS3-Mini / assets /reference.js
fffiloni's picture
v0.3.0: cancel pending microphone capture when changing voice source
ba8c1f3 verified
Raw History Blame Contribute Delete
8.96 kB
const host=document.getElementById('reference-clip');
const q=s=>element.querySelector(s), player=q('audio'), status=q('.reference-status');
const listeners=new AbortController(), on=(n,e,f)=>n.addEventListener(e,f,{signal:listeners.signal});
let original=null, current=null, objectURL=null, generation=0, recorder=null, stream=null, timer=0, recordStarted=0, recordingRequested=false, disposed=false;
const minutes=s=>`${Math.floor(s/60)}:${String(Math.floor(s%60)).padStart(2,'0')}`;
function busy(value) {
host.dataset.busy=String(value); q('[data-upload]').disabled=value; q('[data-apply]').disabled=value;
const generate=document.getElementById('generate-speech');
if(generate)generate.disabled=(value && document.getElementById('voice-options')?.dataset.clip==='true') || document.getElementById('status-state')?.dataset.busy==='true';
}
function tell(text) { status.textContent=text; }
function setFileData(data) { host.dataset.file=JSON.stringify(data); props.value=JSON.stringify(data); }
function stopTracks() { stream?.getTracks().forEach(t=>t.stop());stream=null;clearInterval(timer); }
function wav(buffer,start,end) {
const rate=buffer.sampleRate, first=Math.floor(start*rate), frames=Math.floor(end*rate)-first;
const bytes=new ArrayBuffer(44+frames*2), v=new DataView(bytes);
const str=(o,s)=>[...s].forEach((c,i)=>v.setUint8(o+i,c.charCodeAt(0)));
str(0,'RIFF');v.setUint32(4,36+frames*2,true);str(8,'WAVE');str(12,'fmt ');v.setUint32(16,16,true);v.setUint16(20,1,true);v.setUint16(22,1,true);v.setUint32(24,rate,true);v.setUint32(28,rate*2,true);v.setUint16(32,2,true);v.setUint16(34,16,true);str(36,'data');v.setUint32(40,frames*2,true);
const channels=Array.from({length:buffer.numberOfChannels},(_,i)=>buffer.getChannelData(i));
for(let i=0;i<frames;i++) { let sample=0;for(const c of channels) sample+=c[first+i]/channels.length;sample=Math.max(-1,Math.min(1,sample));v.setInt16(44+i*2,sample<0?sample*32768:sample*32767,true); }
return new File([bytes],'reference.wav',{type:'audio/wav'});
}
async function decode(file) {
const AudioContextClass=window.AudioContext||window.webkitAudioContext;
const context=new AudioContextClass();
try {return await context.decodeAudioData(await file.arrayBuffer());}
finally {await context.close();}
}
function preview(item, preserveSelection=false) {
player.pause();if(objectURL) URL.revokeObjectURL(objectURL);
objectURL=URL.createObjectURL(item.file);player.src=objectURL;current=item;
q('.reference-player').hidden=false;q('[data-trim]').hidden=false;q('[data-remove]').hidden=false;
q('[data-file-name]').textContent=item.name;q('[data-duration]').textContent=minutes(item.buffer.duration);
if(!preserveSelection){q('[data-start]').value='0';q('[data-end]').value=original.buffer.duration.toFixed(2);}
q('[data-start]').max=q('[data-end]').max=original.buffer.duration;
q('[data-play]').textContent='Play';q('[data-time]').textContent='0:00';q('[data-seek]').value='0';
}
async function accept(file,fromTrim=false) {
if(!file) return;
const version=++generation;busy(true);tell('Preparing voice clip…');
try {
if(file.size>25*1024*1024) throw Error('Choose an audio file smaller than 25 MB.');
const buffer=await decode(file);
if(disposed || version!==generation) return;
if(buffer.duration<.5) throw Error('The clip must be at least 0.5 seconds long.');
if(buffer.duration>60) throw Error('Choose a clip no longer than 60 seconds.');
tell('Uploading voice clip…');
const uploaded=await upload(file);
if(disposed || version!==generation) return;
const item={file,buffer,name:file.name};if(!fromTrim) original=item;
setFileData({...uploaded,orig_name:file.name,mime_type:file.type||'audio/wav',meta:{_type:'gradio.FileData'}});
preview(item,fromTrim);tell('Voice clip ready.');
} catch(error) { if(version===generation) tell((error.message||'Could not load this audio.')+(current?' Your previous clip is still selected.':'')); }
finally {if(version===generation) busy(false);}
}
function remove() {
generation++;recordingRequested=false;busy(false);stopTracks();if(recorder?.state==='recording') recorder.stop();recorder=null;
player.pause();player.removeAttribute('src');player.load();if(objectURL)URL.revokeObjectURL(objectURL);objectURL=null;original=current=null;setFileData(null);
q('[data-file]').value='';q('[data-file-name]').textContent='Drop a voice clip here';
for(const selector of ['.reference-player','[data-trim]','[data-remove]'])q(selector).hidden=true;
q('[data-record]').textContent='Record voice';tell('');
}
on(q('[data-upload]'),'click',()=>q('[data-file]').click());
on(q('.drop-area'),'click',()=>q('[data-file]').click());
on(q('.drop-area'),'keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();q('[data-file]').click();}});
on(q('[data-file]'),'change',e=>accept(e.target.files[0]));
on(q('.drop-area'),'dragover',e=>{e.preventDefault();q('.drop-area').dataset.over='true';});
on(q('.drop-area'),'dragleave',()=>q('.drop-area').dataset.over='false');
on(q('.drop-area'),'drop',e=>{e.preventDefault();q('.drop-area').dataset.over='false';accept(e.dataTransfer.files[0]);});
on(q('[data-remove]'),'click',remove);
on(q('[data-play]'),'click',async()=>{try{if(player.paused)await player.play();else player.pause();}catch{tell('Playback unavailable. Choose a different audio file.');}});
on(player,'play',()=>q('[data-play]').textContent='Pause');on(player,'pause',()=>q('[data-play]').textContent=player.ended?'Replay':'Play');
on(player,'timeupdate',()=>{q('[data-time]').textContent=minutes(player.currentTime);if(player.duration)q('[data-seek]').value=player.currentTime/player.duration*1000;});
on(q('[data-seek]'),'input',()=>{if(Number.isFinite(player.duration))player.currentTime=+q('[data-seek]').value/1000*player.duration;});
on(q('[data-apply]'),'click',()=>{
if(!original)return;const start=+q('[data-start]').value,end=+q('[data-end]').value;
if(!Number.isFinite(start+end)||start<0||end>original.buffer.duration||end-start<.5||end-start>60){tell('Select 0.5–60 seconds within the clip.');return;}
accept(wav(original.buffer,start,end),true);
});
on(q('[data-reset]'),'click',()=>{if(original)accept(original.file);});
on(q('[data-record]'),'click',async()=>{
if(recorder?.state==='recording'){recorder.stop();stopTracks();return;}
if(host.dataset.busy==='true')return;
const version=++generation;recordingRequested=true;busy(true);tell('Waiting for microphone permission…');
try {
if(!navigator.mediaDevices?.getUserMedia || !window.MediaRecorder)throw Error('Recording is unavailable in this browser. Choose an audio file.');
player.pause();document.getElementById('speech-player')?.dispatchEvent(new CustomEvent('zonos:pause'));
const acquired=await navigator.mediaDevices.getUserMedia({audio:true});
if(disposed||version!==generation){acquired.getTracks().forEach(t=>t.stop());return;}
stream=acquired;const type=['audio/webm;codecs=opus','audio/mp4','audio/webm'].find(t=>MediaRecorder.isTypeSupported(t));
recorder=new MediaRecorder(stream,type?{mimeType:type}:undefined);const chunks=[];
recorder.ondataavailable=e=>{if(e.data.size)chunks.push(e.data);};
recorder.onerror=()=>{if(disposed||version!==generation)return;recordingRequested=false;generation++;stopTracks();if(recorder?.state==='recording')recorder.stop();q('[data-record]').textContent='Record voice';busy(false);tell('Recording failed. Try uploading a clip.');};
recorder.onstop=()=>{
if(disposed||version!==generation)return;
recordingRequested=false;stopTracks();q('[data-record]').textContent='Record voice';busy(false);
if(disposed||version!==generation)return;
const mime=recorder.mimeType,extension=mime.includes('mp4')?'m4a':'webm';
accept(new File(chunks,`recording.${extension}`,{type:mime}));
};
recorder.start();recordStarted=performance.now();q('[data-record]').textContent='Stop recording';
timer=setInterval(()=>{const seconds=(performance.now()-recordStarted)/1000;tell('Recording · '+minutes(seconds));if(seconds>=59){recorder.stop();stopTracks();}},200);
} catch(error){if(version!==generation)return;recordingRequested=false;stopTracks();busy(false);tell(error.message||'Microphone unavailable. Choose an audio file.');}
});
on(host,'zonos:source',e=>{if(e.detail==='Preset' && recordingRequested){recordingRequested=false;generation++;if(recorder?.state==='recording')recorder.stop();stopTracks();q('[data-record]').textContent='Record voice';busy(false);tell('Recording cancelled.');}else busy(host.dataset.busy==='true');});
function dispose(){disposed=true;generation++;stopTracks();if(recorder?.state==='recording')recorder.stop();player.pause();if(objectURL)URL.revokeObjectURL(objectURL);listeners.abort();observer.disconnect();}
const observer=new MutationObserver(()=>{if(!element.isConnected)dispose();});observer.observe(document.documentElement,{childList:true,subtree:true});
on(window,'pagehide',dispose);setFileData(null);busy(false);