File size: 8,960 Bytes
1b2da16
 
 
ba8c1f3
1b2da16
 
 
ba8c1f3
 
1b2da16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319d860
1b2da16
 
 
 
319d860
1b2da16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319d860
1b2da16
 
 
 
ba8c1f3
1b2da16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ba8c1f3
1b2da16
 
319d860
1b2da16
 
 
 
 
ba8c1f3
1b2da16
ba8c1f3
 
1b2da16
 
 
 
 
 
ba8c1f3
1b2da16
ba8c1f3
1b2da16
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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);