@seams/voice-capture 0.67.1 → 0.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -2
- package/dist/browser/card.js +33 -32
- package/dist/browser/chunks/{microphone-CU5gQG6w.js → microphone-BQVF_tVV.js} +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/esm/card/phraseCard.js +5 -8
- package/dist/esm/card/prompt.js +47 -34
- package/dist/esm/card/rowMotion.js +8 -0
- package/dist/esm/card/surface.js +39 -10
- package/dist/esm/index.js +2 -2
- package/dist/esm/microphone.js +18 -2
- package/dist/example/example.js +8 -2
- package/dist/example/index.html +1 -0
- package/dist/types/card/index.d.ts +1 -1
- package/dist/types/card/prompt.d.ts +4 -3
- package/dist/types/card/rowMotion.d.ts +5 -0
- package/dist/types/card/surface.d.ts +18 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/microphone.d.ts +7 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
const e={assetBase:null,microphonePreference:null,styleNonce:null,label:`Voice capture`,place:`this page`,site:`this site`,recoveryHint:`Reload the page and try again.`,cacheName:`seams-voice-capture-models-v1`};let t=e;function n(n){let r=new URL(n.assetBase,globalThis.document?.baseURI);r.pathname.endsWith(`/`)||(r.pathname+=`/`),t={assetBase:r,microphonePreference:n.microphonePreference??null,styleNonce:n.styleNonce??null,label:n.label??e.label,place:n.place??e.place,site:n.site??e.site,recoveryHint:n.recoveryHint??e.recoveryHint,cacheName:n.cacheName??e.cacheName}}function r(){return t}function i(){let e=t.styleNonce;return(typeof e==`function`?e():e)??void 0}function a(e){if(!t.assetBase)throw Error(`Call configureVoiceCapture({ assetBase }) before you load a voice asset.`);return new URL(e,t.assetBase)}const o=3e4;var s=class extends Error{constructor(e,t,n){super(t,n),this.kind=e,this.name=`VoiceAssetLoadError`}};async function c(){try{return await caches.open(r().cacheName)}catch{return null}}var l=class{pending=null;progress={kind:`checking_cache`};listeners=new Set;constructor(e){this.asset=e}get size(){return this.asset.bytes}async download(e){e&&(this.listeners.add(e),e(this.progress));try{this.pending||=this.bytes().catch(this.failed);let e=await this.pending;return this.publish({kind:`ready`}),e}finally{e&&this.listeners.delete(e)}}failed=e=>{throw this.pending=null,this.progress={kind:`checking_cache`},e};publish(e){this.progress=e;for(let t of this.listeners)t(e)}async valid(e){if(e.byteLength!==this.asset.bytes)return!1;let t=new Uint8Array(await crypto.subtle.digest(`SHA-256`,e)),n=``;for(let e of t)n+=e.toString(16).padStart(2,`0`);return n===this.asset.sha256}async bytes(){let{fileName:e,sha256:t}=this.asset;this.publish({kind:`checking_cache`});let n=await c(),r=new URL(`${e}?sha256=${t}`,a(``)).href;try{let e=await n?.match(r);if(e){let t=await e.arrayBuffer();if(await this.valid(t))return t;await n?.delete(r)}}catch{}let i;try{i=await this.fetchBytes()}catch(e){if(!(e instanceof s)||e.kind!==`connection`)throw e;i=await this.fetchBytes()}if(n)try{await n.put(r,new Response(i,{headers:{"Content-Type":`application/octet-stream`}}))}catch{}return i}async fetchBytes(){let{fileName:e}=this.asset,t=typeof this.asset.label==`function`?this.asset.label():this.asset.label,n=new AbortController,i=setTimeout(u.bind(null,n),o);this.publish({kind:`downloading`,received:0});try{let c=await fetch(a(e),{credentials:`omit`,cache:`reload`,signal:n.signal});if(!c.ok||!c.body){let e=[408,429,500,502,503,504].includes(c.status);throw new s(e?`connection`:`unavailable`,e?`${t} download was interrupted. Check your connection and retry.`:`${t} is unavailable from ${r().site}. ${r().recoveryHint}`)}let l=c.body.getReader(),f=new Uint8Array(this.asset.bytes),p=0;try{for(;;){let e=await l.read();if(e.done)break;if(clearTimeout(i),i=setTimeout(u.bind(null,n),o),p+e.value.length>f.length)throw new s(`integrity`,`${t} size is invalid. Retry loading.`);f.set(e.value,p),p+=e.value.length,this.publish({kind:`downloading`,received:p})}}finally{await l.cancel().catch(d),l.releaseLock()}if(p!==f.length||!await this.valid(f.buffer))throw new s(`integrity`,`${t} integrity check failed. Retry loading.`);return f.buffer}catch(e){throw e instanceof s?e:new s(`connection`,`${t} download was interrupted. Check your connection and retry.`,{cause:e})}finally{clearTimeout(i),n.abort()}}};function u(e){e.abort()}function d(){}const f=new l({fileName:`ort-wasm-simd-threaded.wasm`,sha256:`3398c10d07d229bd91b364548e130e0e51a8e5704b88c7c083ebbeb78842dee2`,bytes:14239897,label:()=>`${r().label} engine`});var p=class{asset;session=null;constructor(e){this.asset=new l(e)}prepare(){return this.session||=this.load().catch(this.failed),this.session}async run(e){let t=await this.prepare(),{Tensor:n}=await import(`./ort.wasm.bundle.min-CWqlff_l.js`),r=new n(`float32`,e,[1,e.length]);try{return await t.run({samples:r})}finally{r.dispose()}}failed=e=>{throw this.session=null,e};async load(){try{let e=await import(`./ort.wasm.bundle.min-CWqlff_l.js`);e.env.wasm.numThreads=1,e.env.wasm.wasmPaths=a(``).href,e.env.wasm.wasmBinary=await f.download();let t=await this.asset.download();return await e.InferenceSession.create(t,{executionProviders:[`wasm`]})}catch(e){if(e instanceof s)throw e;let{label:t,recoveryHint:n}=r();throw console.error(`[${t}] inference engine could not start`,e),new s(`initialization`,`${t} could not start. Retry loading. If it still fails, ${n.charAt(0).toLowerCase()}${n.slice(1)}`,{cause:e})}}};const m=new p({fileName:`silero-vad.onnx`,sha256:`2623a2953f6ff3d2c1e61740c6cdb7168133479b267dfef114a4a3cc5bdd788f`,bytes:2327524,label:`Speech detection`});async function h(){return new _(await m.prepare(),await import(`./ort.wasm.bundle.min-CWqlff_l.js`))}const g={probability:async()=>1,clear:()=>{}};var _=class{state=new Float32Array(256);context=new Float32Array(64);constructor(e,t){this.session=e,this.ort=t}async probability(e){if(e.length!==512)throw Error(`Speech detection requires a 512-sample frame.`);let t=new Float32Array(576);t.set(this.context),t.set(e,64);let n=new this.ort.Tensor(`float32`,t,[1,576]),r=new this.ort.Tensor(`float32`,this.state,[2,1,128]),i=new this.ort.Tensor(`int64`,BigInt64Array.of(16000n),[]);try{let t=await this.session.run({input:n,state:r,sr:i});try{let n=t.output?.data,r=t.stateN?.data;if(!(n instanceof Float32Array)||n.length!==1||!Number.isFinite(n[0])||n[0]<0||n[0]>1||!(r instanceof Float32Array)||r.length!==256)throw Error(`Speech detection returned an invalid result.`);return this.state.set(r),this.context.set(e.subarray(448)),n[0]}finally{for(let e of Object.values(t))e.dispose()}}finally{n.dispose(),r.dispose(),i.dispose(),t.fill(0)}}clear(){this.state.fill(0),this.context.fill(0)}};function v(e,t){return t!==null&&e>=t?800:1500}function y(e){return e.trim().replace(/[.!?,;:]+$/,``).split(/\s+/).filter(Boolean).reduce((e,t)=>e+b(t),0)*200}function b(e){let t=e.match(/[A-Za-z]+/g)?.length??0,n=e.match(/[0-9]/g)?.length??0,r=e.match(/[.\-_@]/g)?.length??0;return Math.max(1,t+n+r)}const x=16e3,S=8*x;function C(e){let t=Math.min(e.length,S),n=new Uint8Array(44+t*2),r=new DataView(n.buffer);w(n,0,`RIFF`),r.setUint32(4,36+t*2,!0),w(n,8,`WAVEfmt `),r.setUint32(16,16,!0),r.setUint16(20,1,!0),r.setUint16(22,1,!0),r.setUint32(24,x,!0),r.setUint32(28,x*2,!0),r.setUint16(32,2,!0),r.setUint16(34,16,!0),w(n,36,`data`),r.setUint32(40,t*2,!0);for(let n=0;n<t;n+=1){let t=Math.max(-1,Math.min(1,e[n]??0));r.setInt16(44+n*2,Math.round(t<0?t*32768:t*32767),!0)}return n}function w(e,t,n){for(let r=0;r<n.length;r+=1)e[t+r]=n.charCodeAt(r)}let T={kind:`unread`};function E(e){return typeof e==`string`&&e.length>0&&e!==`default`&&e!==`communications`}async function D(){if(T.kind!==`unread`)return T;let e={kind:`system_default`};try{let t=await r().microphonePreference?.read();if(typeof t==`object`&&t&&`kind`in t&&t.kind===`device`){let n=`deviceId`in t?t.deviceId:null;E(n)&&(e={kind:`device`,deviceId:n})}}catch{}return T.kind===`unread`&&(T=e),T}function O(e){let t={channelCount:1,echoCancellation:!0,noiseSuppression:!0,autoGainControl:!0};return e.kind===`device`&&(t.deviceId={exact:e.deviceId}),{audio:t,video:!1}}async function k(e){if(E(e.deviceId))return{kind:`device`,deviceId:e.deviceId};if(e.groupId)try{let t=await navigator.mediaDevices.enumerateDevices();for(let n of t)if(n.kind===`audioinput`&&n.groupId===e.groupId&&E(n.deviceId))return{kind:`device`,deviceId:n.deviceId}}catch{}return{kind:`system_default`}}async function A(e){e.throwIfAborted();let t=await D();e.throwIfAborted();let n,r=t.kind===`device`?`preferred`:`default`;try{n=await navigator.mediaDevices.getUserMedia(O(t))}catch(i){if(e.throwIfAborted(),t.kind!==`device`||!(i instanceof DOMException)||i.name!==`OverconstrainedError`&&i.name!==`NotFoundError`)throw i;n=await navigator.mediaDevices.getUserMedia(O({kind:`system_default`})),r=`replacement`}let i=j.bind(null,n);e.addEventListener(`abort`,i,{once:!0});try{e.throwIfAborted();let t=n.getAudioTracks()[0];if(!t||t.readyState!==`live`)throw new F;let i=t.getSettings(),a=await k(i);if(e.throwIfAborted(),t.readyState!==`live`)throw new F;return{stream:n,preference:a,selection:r,label:t.label||`Default microphone`,processing:{noiseSuppression:i.noiseSuppression??null,echoCancellation:i.echoCancellation??null,autoGainControl:i.autoGainControl??null,sampleRate:i.sampleRate??null}}}catch(e){throw i(),e}finally{e.removeEventListener(`abort`,i)}}function j(e){for(let t of e.getTracks())t.stop()}async function M(e){e.preference.kind!==`device`||e.selection===`replacement`||await P(e.preference)}async function N(){await P({kind:`system_default`})}async function P(e){T=e;try{await r().microphonePreference?.save(e)}catch{}}var F=class extends Error{constructor(){super(`The microphone disconnected or stopped. Check your microphone and try again.`),this.name=`VoiceMicrophoneInterruptedError`}};function I(e){return e.noise===`high`?`Background noise is high. Move closer to the microphone and try again.`:`Audio too brief or quiet: ${(e.voicedMs/1e3).toFixed(2)} of ${(e.requiredVoicedMs/1e3).toFixed(2)} seconds detected.`}var L=class extends Error{constructor(e){super(I(e)),this.quality=e,this.name=`InsufficientVoiceAudioError`}};const R=.006,z=8e3;function B(){if(V(),!navigator.userActivation.isActive){let{label:e,place:t}=r();throw Error(`Open ${t} and select Record to use ${e}.`)}}function V(){if(document.visibilityState!==`visible`||!document.hasFocus()){let{label:e,place:t}=r();throw Error(`Keep ${t} open and focused to use ${e}.`)}}function H(){return Error(`${r().label} recording was cancelled.`)}function U(e){return e.detector===`level`?async()=>g:h}async function W(e,t,n={}){let r=n.expectedSpeechMs??null,i=U(n);if(e.aborted)throw H();let a=crypto.randomUUID();t({kind:`opening`});let o=await A(e),{stream:s}=o;try{if(e.aborted)throw H();let n=await new G(s,e,t,r,i).result.finally(t.bind(null,{kind:`processing`}));if(e.aborted)throw H();let c=await new OfflineAudioContext(1,x,x).decodeAudioData(n.bytes),l=new OfflineAudioContext(1,Math.ceil(c.duration*x),x),u=l.createBufferSource();u.buffer=c,u.connect(l.destination),u.start();try{let r=await l.startRendering();try{if(e.aborted)throw H();let s=r.getChannelData(0),c=await K(s,e,await i()),l={attemptId:a,capturedAtMs:Date.now(),durationMs:r.duration*1e3,recordingMs:n.recordingMs,endedBy:n.endedBy,voicedMs:c.voicedMs,peakFrameRms:c.peakFrameRms,levelThreshold:R,requiredVoicedMs:600,microphone:o.label,microphoneSelection:o.selection,processing:o.processing,backgroundRms:c.backgroundRms,speechToBackgroundDb:c.speechToBackgroundDb,noise:c.noise};if(t({kind:`measured`,quality:l}),c.kind===`insufficient_audio`)return{kind:`insufficient_audio`,quality:l};let u=s.slice(Math.max(0,c.first-3200),Math.min(s.length,c.last+3200));return await M(o),e.aborted&&(u.fill(0),e.throwIfAborted()),{kind:`captured`,samples:u,quality:l,lastSoundAtMs:n.lastSoundAtMs}}finally{q(r)}}finally{q(c)}}finally{for(let e of s.getTracks())e.stop()}}var G=class{recorder;chunks=[];timer=null;completion=Y();startedAtMs=performance.now();ended=null;activity={kind:`waiting`,activeMs:0};audioContext=null;worklet=null;detector=null;pending=Promise.resolve();queuedFrames=0;closed=!1;result=this.completion.promise;constructor(e,t,n,r,i){this.stream=e,this.signal=t,this.onCapture=n,this.expectedSpeechMs=r,this.createDetector=i,this.recorder=new MediaRecorder(e),this.recorder.addEventListener(`dataavailable`,this.data),this.recorder.addEventListener(`stop`,this.stopped),this.recorder.addEventListener(`error`,this.failed);for(let t of e.getAudioTracks())t.addEventListener(`ended`,this.interrupted),t.addEventListener(`mute`,this.interrupted);t.addEventListener(`abort`,this.cancelled,{once:!0}),this.timer=setTimeout(this.startupFailed,z),this.start()}async start(){try{if(await this.startPauseDetection(),this.signal.throwIfAborted(),this.startedAtMs=performance.now(),this.closed)return;this.recorder.start(),this.timer!==null&&clearTimeout(this.timer),this.timer=setTimeout(this.stop.bind(this,`limit`),z),this.onCapture({kind:`listening`})}catch(e){this.cleanup(),this.completion.reject(e instanceof Error?e:Error(`Speech detection could not start.`))}}data=e=>{this.chunks.push(e.data)};stop=e=>{this.recorder.state!==`inactive`&&(this.ended={endedBy:e,recordingMs:performance.now()-this.startedAtMs,lastSoundAtMs:this.activity.kind===`speaking`?this.activity.lastSoundAtMs:null},this.recorder.stop(),this.stopTracks())};cancelled=()=>{if(this.recorder.state===`inactive`){this.cleanup(),this.completion.reject(H());return}this.stop(`stream_ended`)};startupFailed=()=>{this.cleanup(),this.completion.reject(Error(`Speech detection startup timed out. Please try again.`))};detectorFailed=e=>{this.cleanup(),this.completion.reject(e instanceof Error?e:Error(`Speech detection failed.`))};failed=()=>{this.cleanup(),this.completion.reject(Error(`${r().label} could not record this microphone.`))};interrupted=()=>{this.cleanup(),this.completion.reject(new F)};stopped=()=>{let e=this.ended;if(this.cleanup(),this.signal.aborted){this.completion.reject(H());return}if(e===null){this.completion.reject(new F);return}new Blob(this.chunks,{type:this.recorder.mimeType}).arrayBuffer().then(this.complete.bind(this,e),this.completion.reject.bind(this.completion))};complete=(e,t)=>{if(this.signal.aborted){this.completion.reject(H());return}this.completion.resolve({bytes:t,...e})};async startPauseDetection(){let e=new AudioContext({sampleRate:x});this.audioContext=e;let t=e.resume();if(this.detector=await this.createDetector(),this.signal.throwIfAborted(),e.sampleRate!==16e3)throw Error(`Speech detection requires 16 kHz audio.`);await e.audioWorklet.addModule(a(`voice-activity-worklet.js`)),this.signal.throwIfAborted();let n=new AudioWorkletNode(e,`voice-activity-frames`);this.worklet=n,n.port.onmessage=this.receiveFrame,e.createMediaStreamSource(this.stream).connect(n),n.connect(e.destination),await t}receiveFrame=e=>{if(!(e.data instanceof Float32Array)||e.data.length!==512){this.failed();return}if(this.closed||this.recorder.state!==`recording`){e.data.fill(0);return}if(++this.queuedFrames>32){e.data.fill(0),this.failed();return}this.pending=this.pending.then(this.processFrame.bind(this,e.data)).catch(this.detectorFailed)};async processFrame(e){let t=this.detector;try{if(this.closed||!t)return;let n=await t.probability(e);if(this.closed){t.clear();return}let r=0;for(let t of e)r+=t*t;let i=Math.sqrt(r/e.length),a=n>=.5&&i>=R;this.updateActivity(a),this.onCapture({kind:`activity`,rms:i,speech:a})}finally{e.fill(0),--this.queuedFrames}}updateActivity(e){let t=performance.now();switch(this.activity.kind){case`waiting`:{let n=e?this.activity.activeMs+32:0;this.activity=n>=100?{kind:`speaking`,lastSoundAtMs:t,voicedMs:n}:{kind:`waiting`,activeMs:n};return}case`speaking`:{let{lastSoundAtMs:n,voicedMs:r}=this.activity;if(e){this.activity={kind:`speaking`,lastSoundAtMs:t,voicedMs:r+32};return}t-n>=v(r,this.expectedSpeechMs)&&this.stop(`pause`);return}default:return X(this.activity)}}stopPauseDetection=()=>{this.closed=!0,this.worklet&&(this.worklet.port.onmessage=null,this.worklet.port.close(),this.worklet.disconnect()),this.worklet=null,this.detector?.clear(),this.detector=null;let e=this.audioContext;this.audioContext=null,e&&e.close().catch(J)};stopTracks(){for(let e of this.stream.getTracks())e.stop()}cleanup(){for(let e of this.stream.getAudioTracks())e.removeEventListener(`ended`,this.interrupted),e.removeEventListener(`mute`,this.interrupted);this.stopPauseDetection(),this.stopTracks(),this.timer!==null&&clearTimeout(this.timer),this.signal.removeEventListener(`abort`,this.cancelled),this.recorder.removeEventListener(`dataavailable`,this.data),this.recorder.removeEventListener(`stop`,this.stopped),this.recorder.removeEventListener(`error`,this.failed)}};async function K(e,t,n){let r=-1,i=0,a=0,o=0,s=0,c=0,l=0,u=new Float32Array(512);try{for(let d=0;d<e.length;d+=u.length){t.throwIfAborted(),u.fill(0);let f=Math.min(u.length,e.length-d);u.set(e.subarray(d,d+f));let p=0;for(let e of u)p+=e*e;let m=Math.sqrt(p/f);if(o=Math.max(o,m),!(await n.probability(u)>=.5&&m>=R)){s+=p,c+=f;continue}r<0&&(r=d),i=d+f,a+=f,l+=p}}finally{u.fill(0),n.clear()}let d=Math.sqrt(s/Math.max(1,c)),f=Math.sqrt(l/Math.max(1,a)),p=a&&c?20*Math.log10(Math.max(f,1e-8)/Math.max(d,1e-8)):null,m={backgroundRms:d,speechToBackgroundDb:p,noise:d>=R&&(p===null||p<10)?`high`:c?`normal`:`unknown`},h=a/x*1e3;return r<0||h<600?{kind:`insufficient_audio`,voicedMs:h,peakFrameRms:o,...m}:{kind:`captured`,first:r,last:i,voicedMs:h,peakFrameRms:o,...m}}function q(e){for(let t=0;t<e.numberOfChannels;t+=1)e.getChannelData(t).fill(0)}function J(){}function Y(){let e,t;return{promise:new Promise((n,r)=>{e=n,t=r}),resolve:e,reject:t}}function X(e){throw Error(`Unexpected speech activity: ${String(e)}`)}export{i as C,n as S,m as _,I as a,f as b,M as c,x as d,C as f,g,h,B as i,N as l,y as m,W as n,F as o,v as p,V as r,A as s,L as t,S as u,p as v,r as x,s as y};
|
|
1
|
+
const e={assetBase:null,microphonePreference:null,styleNonce:null,label:`Voice capture`,place:`this page`,site:`this site`,recoveryHint:`Reload the page and try again.`,cacheName:`seams-voice-capture-models-v1`};let t=e;function n(n){let r=new URL(n.assetBase,globalThis.document?.baseURI);r.pathname.endsWith(`/`)||(r.pathname+=`/`),t={assetBase:r,microphonePreference:n.microphonePreference??null,styleNonce:n.styleNonce??null,label:n.label??e.label,place:n.place??e.place,site:n.site??e.site,recoveryHint:n.recoveryHint??e.recoveryHint,cacheName:n.cacheName??e.cacheName}}function r(){return t}function i(){let e=t.styleNonce;return(typeof e==`function`?e():e)??void 0}function a(e){if(!t.assetBase)throw Error(`Call configureVoiceCapture({ assetBase }) before you load a voice asset.`);return new URL(e,t.assetBase)}const o=3e4;var s=class extends Error{constructor(e,t,n){super(t,n),this.kind=e,this.name=`VoiceAssetLoadError`}};async function c(){try{return await caches.open(r().cacheName)}catch{return null}}var l=class{pending=null;progress={kind:`checking_cache`};listeners=new Set;constructor(e){this.asset=e}get size(){return this.asset.bytes}async download(e){e&&(this.listeners.add(e),e(this.progress));try{this.pending||=this.bytes().catch(this.failed);let e=await this.pending;return this.publish({kind:`ready`}),e}finally{e&&this.listeners.delete(e)}}failed=e=>{throw this.pending=null,this.progress={kind:`checking_cache`},e};publish(e){this.progress=e;for(let t of this.listeners)t(e)}async valid(e){if(e.byteLength!==this.asset.bytes)return!1;let t=new Uint8Array(await crypto.subtle.digest(`SHA-256`,e)),n=``;for(let e of t)n+=e.toString(16).padStart(2,`0`);return n===this.asset.sha256}async bytes(){let{fileName:e,sha256:t}=this.asset;this.publish({kind:`checking_cache`});let n=await c(),r=new URL(`${e}?sha256=${t}`,a(``)).href;try{let e=await n?.match(r);if(e){let t=await e.arrayBuffer();if(await this.valid(t))return t;await n?.delete(r)}}catch{}let i;try{i=await this.fetchBytes()}catch(e){if(!(e instanceof s)||e.kind!==`connection`)throw e;i=await this.fetchBytes()}if(n)try{await n.put(r,new Response(i,{headers:{"Content-Type":`application/octet-stream`}}))}catch{}return i}async fetchBytes(){let{fileName:e}=this.asset,t=typeof this.asset.label==`function`?this.asset.label():this.asset.label,n=new AbortController,i=setTimeout(u.bind(null,n),o);this.publish({kind:`downloading`,received:0});try{let c=await fetch(a(e),{credentials:`omit`,cache:`reload`,signal:n.signal});if(!c.ok||!c.body){let e=[408,429,500,502,503,504].includes(c.status);throw new s(e?`connection`:`unavailable`,e?`${t} download was interrupted. Check your connection and retry.`:`${t} is unavailable from ${r().site}. ${r().recoveryHint}`)}let l=c.body.getReader(),f=new Uint8Array(this.asset.bytes),p=0;try{for(;;){let e=await l.read();if(e.done)break;if(clearTimeout(i),i=setTimeout(u.bind(null,n),o),p+e.value.length>f.length)throw new s(`integrity`,`${t} size is invalid. Retry loading.`);f.set(e.value,p),p+=e.value.length,this.publish({kind:`downloading`,received:p})}}finally{await l.cancel().catch(d),l.releaseLock()}if(p!==f.length||!await this.valid(f.buffer))throw new s(`integrity`,`${t} integrity check failed. Retry loading.`);return f.buffer}catch(e){throw e instanceof s?e:new s(`connection`,`${t} download was interrupted. Check your connection and retry.`,{cause:e})}finally{clearTimeout(i),n.abort()}}};function u(e){e.abort()}function d(){}const f=new l({fileName:`ort-wasm-simd-threaded.wasm`,sha256:`3398c10d07d229bd91b364548e130e0e51a8e5704b88c7c083ebbeb78842dee2`,bytes:14239897,label:()=>`${r().label} engine`});var p=class{asset;session=null;constructor(e){this.asset=new l(e)}prepare(){return this.session||=this.load().catch(this.failed),this.session}async run(e){let t=await this.prepare(),{Tensor:n}=await import(`./ort.wasm.bundle.min-CWqlff_l.js`),r=new n(`float32`,e,[1,e.length]);try{return await t.run({samples:r})}finally{r.dispose()}}failed=e=>{throw this.session=null,e};async load(){try{let e=await import(`./ort.wasm.bundle.min-CWqlff_l.js`);e.env.wasm.numThreads=1,e.env.wasm.wasmPaths=a(``).href,e.env.wasm.wasmBinary=await f.download();let t=await this.asset.download();return await e.InferenceSession.create(t,{executionProviders:[`wasm`]})}catch(e){if(e instanceof s)throw e;let{label:t,recoveryHint:n}=r();throw console.error(`[${t}] inference engine could not start`,e),new s(`initialization`,`${t} could not start. Retry loading. If it still fails, ${n.charAt(0).toLowerCase()}${n.slice(1)}`,{cause:e})}}};const m=new p({fileName:`silero-vad.onnx`,sha256:`2623a2953f6ff3d2c1e61740c6cdb7168133479b267dfef114a4a3cc5bdd788f`,bytes:2327524,label:`Speech detection`});async function h(){return new _(await m.prepare(),await import(`./ort.wasm.bundle.min-CWqlff_l.js`))}const g={probability:async()=>1,clear:()=>{}};var _=class{state=new Float32Array(256);context=new Float32Array(64);constructor(e,t){this.session=e,this.ort=t}async probability(e){if(e.length!==512)throw Error(`Speech detection requires a 512-sample frame.`);let t=new Float32Array(576);t.set(this.context),t.set(e,64);let n=new this.ort.Tensor(`float32`,t,[1,576]),r=new this.ort.Tensor(`float32`,this.state,[2,1,128]),i=new this.ort.Tensor(`int64`,BigInt64Array.of(16000n),[]);try{let t=await this.session.run({input:n,state:r,sr:i});try{let n=t.output?.data,r=t.stateN?.data;if(!(n instanceof Float32Array)||n.length!==1||!Number.isFinite(n[0])||n[0]<0||n[0]>1||!(r instanceof Float32Array)||r.length!==256)throw Error(`Speech detection returned an invalid result.`);return this.state.set(r),this.context.set(e.subarray(448)),n[0]}finally{for(let e of Object.values(t))e.dispose()}}finally{n.dispose(),r.dispose(),i.dispose(),t.fill(0)}}clear(){this.state.fill(0),this.context.fill(0)}};function v(e,t){return t!==null&&e>=t?800:1500}function y(e){return e.trim().replace(/[.!?,;:]+$/,``).split(/\s+/).filter(Boolean).reduce((e,t)=>e+b(t),0)*200}function b(e){let t=e.match(/[A-Za-z]+/g)?.length??0,n=e.match(/[0-9]/g)?.length??0,r=e.match(/[.\-_@]/g)?.length??0;return Math.max(1,t+n+r)}const x=16e3,S=8*x;function C(e){let t=Math.min(e.length,S),n=new Uint8Array(44+t*2),r=new DataView(n.buffer);w(n,0,`RIFF`),r.setUint32(4,36+t*2,!0),w(n,8,`WAVEfmt `),r.setUint32(16,16,!0),r.setUint16(20,1,!0),r.setUint16(22,1,!0),r.setUint32(24,x,!0),r.setUint32(28,x*2,!0),r.setUint16(32,2,!0),r.setUint16(34,16,!0),w(n,36,`data`),r.setUint32(40,t*2,!0);for(let n=0;n<t;n+=1){let t=Math.max(-1,Math.min(1,e[n]??0));r.setInt16(44+n*2,Math.round(t<0?t*32768:t*32767),!0)}return n}function w(e,t,n){for(let r=0;r<n.length;r+=1)e[t+r]=n.charCodeAt(r)}let T={kind:`unread`};function E(e){return typeof e==`string`&&e.length>0&&e!==`default`&&e!==`communications`}async function D(){if(T.kind!==`unread`)return T;let e={kind:`system_default`};try{let t=await r().microphonePreference?.read();if(typeof t==`object`&&t&&`kind`in t&&t.kind===`device`){let n=`deviceId`in t?t.deviceId:null;E(n)&&(e={kind:`device`,deviceId:n})}}catch{}return T.kind===`unread`&&(T=e),T}function O(e){let t={channelCount:1,echoCancellation:!0,noiseSuppression:!0,autoGainControl:!0};return e.kind===`device`&&(t.deviceId={exact:e.deviceId}),{audio:t,video:!1}}async function k(e){if(E(e.deviceId))return{kind:`device`,deviceId:e.deviceId};if(e.groupId)try{let t=await navigator.mediaDevices.enumerateDevices();for(let n of t)if(n.kind===`audioinput`&&n.groupId===e.groupId&&E(n.deviceId))return{kind:`device`,deviceId:n.deviceId}}catch{}return{kind:`system_default`}}async function A(e){e.throwIfAborted();let t=await D();e.throwIfAborted();let n,r=t.kind===`device`?`preferred`:`default`;try{n=await navigator.mediaDevices.getUserMedia(O(t))}catch(i){if(e.throwIfAborted(),t.kind!==`device`||!(i instanceof DOMException)||i.name!==`OverconstrainedError`&&i.name!==`NotFoundError`)throw i;n=await navigator.mediaDevices.getUserMedia(O({kind:`system_default`})),r=`replacement`}let i=j.bind(null,n);e.addEventListener(`abort`,i,{once:!0});try{e.throwIfAborted();let t=n.getAudioTracks()[0];if(!t||t.readyState!==`live`)throw new F;let i=t.getSettings(),a=await k(i);if(e.throwIfAborted(),t.readyState!==`live`)throw new F;return{stream:n,preference:a,selection:r,label:t.label||`Default microphone`,processing:{noiseSuppression:i.noiseSuppression??null,echoCancellation:i.echoCancellation??null,autoGainControl:i.autoGainControl??null,sampleRate:i.sampleRate??null}}}catch(e){throw i(),e}finally{e.removeEventListener(`abort`,i)}}function j(e){for(let t of e.getTracks())t.stop()}async function M(e){e.preference.kind!==`device`||e.selection===`replacement`||await P(e.preference)}async function N(){await P({kind:`system_default`})}async function P(e){T=e;try{await r().microphonePreference?.save(e)}catch{}}var F=class extends Error{constructor(){super(`The microphone disconnected or stopped. Check your microphone and try again.`),this.name=`VoiceMicrophoneInterruptedError`}};function I(e){return e.noise===`high`?`Background noise is high. Move closer to the microphone and try again.`:`Audio too brief or quiet: ${(e.voicedMs/1e3).toFixed(2)} of ${(e.requiredVoicedMs/1e3).toFixed(2)} seconds detected.`}var L=class extends Error{constructor(e){super(I(e)),this.quality=e,this.name=`InsufficientVoiceAudioError`}};const R=.006,z=8e3;function B(){if(V(),!navigator.userActivation.isActive){let{label:e,place:t}=r();throw Error(`Open ${t} and select Record to use ${e}.`)}}function V(){if(document.visibilityState!==`visible`||!document.hasFocus()){let{label:e,place:t}=r();throw Error(`Keep ${t} open and focused to use ${e}.`)}}var H=class extends Error{constructor(){super(`Select Record to use ${r().label}.`),this.name=`VoiceUserActionNeededError`}};function U(e){return new Promise(t=>setTimeout(t,e))}function W(){return Error(`${r().label} recording was cancelled.`)}function G(e){return e.detector===`level`?async()=>g:h}async function K(e,t,n={}){let r=n.expectedSpeechMs??null,i=G(n);if(e.aborted)throw W();let a=crypto.randomUUID();t({kind:`opening`});let o=await A(e),{stream:s}=o;try{if(e.aborted)throw W();let n=await new q(s,e,t,r,i).result.finally(t.bind(null,{kind:`processing`}));if(e.aborted)throw W();let c=await new OfflineAudioContext(1,x,x).decodeAudioData(n.bytes),l=new OfflineAudioContext(1,Math.ceil(c.duration*x),x),u=l.createBufferSource();u.buffer=c,u.connect(l.destination),u.start();try{let r=await l.startRendering();try{if(e.aborted)throw W();let s=r.getChannelData(0),c=await J(s,e,await i()),l={attemptId:a,capturedAtMs:Date.now(),durationMs:r.duration*1e3,recordingMs:n.recordingMs,endedBy:n.endedBy,voicedMs:c.voicedMs,peakFrameRms:c.peakFrameRms,levelThreshold:R,requiredVoicedMs:600,microphone:o.label,microphoneSelection:o.selection,processing:o.processing,backgroundRms:c.backgroundRms,speechToBackgroundDb:c.speechToBackgroundDb,noise:c.noise};if(t({kind:`measured`,quality:l}),c.kind===`insufficient_audio`)return{kind:`insufficient_audio`,quality:l};let u=s.slice(Math.max(0,c.first-3200),Math.min(s.length,c.last+3200));return await M(o),e.aborted&&(u.fill(0),e.throwIfAborted()),{kind:`captured`,samples:u,quality:l,lastSoundAtMs:n.lastSoundAtMs}}finally{Y(r)}}finally{Y(c)}}finally{for(let e of s.getTracks())e.stop()}}var q=class{recorder;chunks=[];timer=null;completion=Z();startedAtMs=performance.now();ended=null;activity={kind:`waiting`,activeMs:0};audioContext=null;worklet=null;detector=null;pending=Promise.resolve();queuedFrames=0;closed=!1;result=this.completion.promise;constructor(e,t,n,r,i){this.stream=e,this.signal=t,this.onCapture=n,this.expectedSpeechMs=r,this.createDetector=i,this.recorder=new MediaRecorder(e),this.recorder.addEventListener(`dataavailable`,this.data),this.recorder.addEventListener(`stop`,this.stopped),this.recorder.addEventListener(`error`,this.failed);for(let t of e.getAudioTracks())t.addEventListener(`ended`,this.interrupted),t.addEventListener(`mute`,this.interrupted);t.addEventListener(`abort`,this.cancelled,{once:!0}),this.timer=setTimeout(this.startupFailed,z),this.start()}async start(){try{if(await this.startPauseDetection(),this.signal.throwIfAborted(),this.startedAtMs=performance.now(),this.closed)return;this.recorder.start(),this.timer!==null&&clearTimeout(this.timer),this.timer=setTimeout(this.stop.bind(this,`limit`),z),this.onCapture({kind:`listening`})}catch(e){this.cleanup(),this.completion.reject(e instanceof Error?e:Error(`Speech detection could not start.`))}}data=e=>{this.chunks.push(e.data)};stop=e=>{this.recorder.state!==`inactive`&&(this.ended={endedBy:e,recordingMs:performance.now()-this.startedAtMs,lastSoundAtMs:this.activity.kind===`speaking`?this.activity.lastSoundAtMs:null},this.recorder.stop(),this.stopTracks())};cancelled=()=>{if(this.recorder.state===`inactive`){this.cleanup(),this.completion.reject(W());return}this.stop(`stream_ended`)};startupFailed=()=>{this.cleanup(),this.completion.reject(Error(`Speech detection startup timed out. Please try again.`))};detectorFailed=e=>{this.cleanup(),this.completion.reject(e instanceof Error?e:Error(`Speech detection failed.`))};failed=()=>{this.cleanup(),this.completion.reject(Error(`${r().label} could not record this microphone.`))};interrupted=()=>{this.cleanup(),this.completion.reject(new F)};stopped=()=>{let e=this.ended;if(this.cleanup(),this.signal.aborted){this.completion.reject(W());return}if(e===null){this.completion.reject(new F);return}new Blob(this.chunks,{type:this.recorder.mimeType}).arrayBuffer().then(this.complete.bind(this,e),this.completion.reject.bind(this.completion))};complete=(e,t)=>{if(this.signal.aborted){this.completion.reject(W());return}this.completion.resolve({bytes:t,...e})};async startPauseDetection(){let e=new AudioContext({sampleRate:x});this.audioContext=e;let t=e.resume();if(this.detector=await this.createDetector(),this.signal.throwIfAborted(),e.sampleRate!==16e3)throw Error(`Speech detection requires 16 kHz audio.`);await e.audioWorklet.addModule(a(`voice-activity-worklet.js`)),this.signal.throwIfAborted();let n=new AudioWorkletNode(e,`voice-activity-frames`);if(this.worklet=n,n.port.onmessage=this.receiveFrame,e.createMediaStreamSource(this.stream).connect(n),n.connect(e.destination),await Promise.race([t.catch(()=>{}),U(1e3)]),e.state!==`running`)throw new H}receiveFrame=e=>{if(!(e.data instanceof Float32Array)||e.data.length!==512){this.failed();return}if(this.closed||this.recorder.state!==`recording`){e.data.fill(0);return}if(++this.queuedFrames>32){e.data.fill(0),this.failed();return}this.pending=this.pending.then(this.processFrame.bind(this,e.data)).catch(this.detectorFailed)};async processFrame(e){let t=this.detector;try{if(this.closed||!t)return;let n=await t.probability(e);if(this.closed){t.clear();return}let r=0;for(let t of e)r+=t*t;let i=Math.sqrt(r/e.length),a=n>=.5&&i>=R;this.updateActivity(a),this.onCapture({kind:`activity`,rms:i,speech:a})}finally{e.fill(0),--this.queuedFrames}}updateActivity(e){let t=performance.now();switch(this.activity.kind){case`waiting`:{let n=e?this.activity.activeMs+32:0;this.activity=n>=100?{kind:`speaking`,lastSoundAtMs:t,voicedMs:n}:{kind:`waiting`,activeMs:n};return}case`speaking`:{let{lastSoundAtMs:n,voicedMs:r}=this.activity;if(e){this.activity={kind:`speaking`,lastSoundAtMs:t,voicedMs:r+32};return}t-n>=v(r,this.expectedSpeechMs)&&this.stop(`pause`);return}default:return Q(this.activity)}}stopPauseDetection=()=>{this.closed=!0,this.worklet&&(this.worklet.port.onmessage=null,this.worklet.port.close(),this.worklet.disconnect()),this.worklet=null,this.detector?.clear(),this.detector=null;let e=this.audioContext;this.audioContext=null,e&&e.close().catch(X)};stopTracks(){for(let e of this.stream.getTracks())e.stop()}cleanup(){for(let e of this.stream.getAudioTracks())e.removeEventListener(`ended`,this.interrupted),e.removeEventListener(`mute`,this.interrupted);this.stopPauseDetection(),this.stopTracks(),this.timer!==null&&clearTimeout(this.timer),this.signal.removeEventListener(`abort`,this.cancelled),this.recorder.removeEventListener(`dataavailable`,this.data),this.recorder.removeEventListener(`stop`,this.stopped),this.recorder.removeEventListener(`error`,this.failed)}};async function J(e,t,n){let r=-1,i=0,a=0,o=0,s=0,c=0,l=0,u=new Float32Array(512);try{for(let d=0;d<e.length;d+=u.length){t.throwIfAborted(),u.fill(0);let f=Math.min(u.length,e.length-d);u.set(e.subarray(d,d+f));let p=0;for(let e of u)p+=e*e;let m=Math.sqrt(p/f);if(o=Math.max(o,m),!(await n.probability(u)>=.5&&m>=R)){s+=p,c+=f;continue}r<0&&(r=d),i=d+f,a+=f,l+=p}}finally{u.fill(0),n.clear()}let d=Math.sqrt(s/Math.max(1,c)),f=Math.sqrt(l/Math.max(1,a)),p=a&&c?20*Math.log10(Math.max(f,1e-8)/Math.max(d,1e-8)):null,m={backgroundRms:d,speechToBackgroundDb:p,noise:d>=R&&(p===null||p<10)?`high`:c?`normal`:`unknown`},h=a/x*1e3;return r<0||h<600?{kind:`insufficient_audio`,voicedMs:h,peakFrameRms:o,...m}:{kind:`captured`,first:r,last:i,voicedMs:h,peakFrameRms:o,...m}}function Y(e){for(let t=0;t<e.numberOfChannels;t+=1)e.getChannelData(t).fill(0)}function X(){}function Z(){let e,t;return{promise:new Promise((n,r)=>{e=n,t=r}),resolve:e,reject:t}}function Q(e){throw Error(`Unexpected speech activity: ${String(e)}`)}export{n as C,r as S,g as _,B as a,s as b,A as c,S as d,x as f,h as g,y as h,V as i,M as l,v as m,H as n,I as o,C as p,K as r,F as s,L as t,N as u,m as v,i as w,f as x,p as y};
|
package/dist/browser/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{C as e,_ as t,a as n,b as r,c as i,d as a,f as o,g as s,h as c,i as l,l as u,m as d,n as f,o as p,p as m,r as h,s as g,t as _,u as v,v as y,x as b,y as x}from"./chunks/microphone-BQVF_tVV.js";export{x as BrowserInferenceModel,_ as InsufficientVoiceAudioError,a as MAX_SAMPLES,o as SAMPLE_RATE,r as VoiceAssetLoadError,g as VoiceMicrophoneInterruptedError,f as VoiceUserActionNeededError,e as configureVoiceCapture,s as createVoiceActivity,d as endPauseMs,c as expectedSpeechMs,t as levelDetector,i as openVoiceMicrophone,m as pcm16Wav,h as recordVoice,u as rememberVoiceMicrophone,l as requireVoiceForeground,n as requireVoiceUserAction,v as useSystemVoiceMicrophone,y as voiceActivityModel,b as voiceInferenceEngine,p as voiceQualityMessage};
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
+
import { ROW_EASE } from "./rowMotion.js";
|
|
1
2
|
//#region src/card/phraseCard.ts
|
|
2
3
|
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
3
4
|
const WIDTH = 308;
|
|
4
5
|
const MID = 32;
|
|
5
6
|
const SEGMENTS = 6;
|
|
6
7
|
const GAP = 8;
|
|
7
|
-
const FADE_OUT_MS = 150;
|
|
8
|
-
const FADE_IN_MS = 300;
|
|
9
|
-
const FADE_IN_DELAY_MS = 60;
|
|
10
|
-
const ROW_STAGGER_MS = 40;
|
|
11
8
|
const RIBBON_MOVE_MS = 180;
|
|
12
9
|
const FLATTEN_MS = 200;
|
|
13
10
|
const OPEN_MS = 200;
|
|
@@ -217,7 +214,7 @@ var PhraseCard = class {
|
|
|
217
214
|
["action", prompt.actionRow]
|
|
218
215
|
].filter(([key]) => this.rows[key] !== next[key] || key === "head" && this.rows.headline !== next.headline).map(([, element]) => element);
|
|
219
216
|
prompt.phrase.classList.toggle("seams-voiceid-phrase-dim", next.dim);
|
|
220
|
-
if (changed.length > 0 && !this.motion.matches) await Promise.all(changed.map((element) => fade(element,
|
|
217
|
+
if (changed.length > 0 && !this.motion.matches) await Promise.all(changed.map((element) => fade(element, 150)));
|
|
221
218
|
if (this.stale(version)) return;
|
|
222
219
|
const ribbonTop = prompt.ribbon.getBoundingClientRect().top;
|
|
223
220
|
const heightBefore = prompt.element.getBoundingClientRect().height;
|
|
@@ -232,7 +229,7 @@ var PhraseCard = class {
|
|
|
232
229
|
duration: RIBBON_MOVE_MS,
|
|
233
230
|
easing: EASE_MOVE
|
|
234
231
|
});
|
|
235
|
-
for (const [index, element] of changed.entries()) fadeIn(element, this.motion.matches,
|
|
232
|
+
for (const [index, element] of changed.entries()) fadeIn(element, this.motion.matches, 20 + index * 30);
|
|
236
233
|
}
|
|
237
234
|
markedText(text) {
|
|
238
235
|
if (typeof text === "string") return [text];
|
|
@@ -447,8 +444,8 @@ function fadeIn(element, reduced, delay) {
|
|
|
447
444
|
transform: "none"
|
|
448
445
|
}], {
|
|
449
446
|
delay: reduced ? 0 : delay,
|
|
450
|
-
duration: reduced ? 150 :
|
|
451
|
-
easing:
|
|
447
|
+
duration: reduced ? 150 : 200,
|
|
448
|
+
easing: ROW_EASE,
|
|
452
449
|
fill: "both"
|
|
453
450
|
});
|
|
454
451
|
}
|
package/dist/esm/card/prompt.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { styleNonce } from "../config.js";
|
|
2
2
|
import { useSystemVoiceMicrophone } from "../microphoneInput.js";
|
|
3
|
+
import { ROW_EASE } from "./rowMotion.js";
|
|
3
4
|
import { t as VoiceVisual } from "../chunks/visual-Di3ojTED.js";
|
|
4
5
|
//#region src/card/prompt.ts
|
|
5
6
|
const CSS = `
|
|
@@ -60,7 +61,7 @@ const CSS = `
|
|
|
60
61
|
.seams-voiceid-prompt > .seams-voiceid-status { grid-area: 4 / 1; }
|
|
61
62
|
.seams-voiceid-progress-slot { grid-row: 5; grid-column: 1; min-width: 0; align-self: center; }
|
|
62
63
|
.seams-voiceid-progress-slot:empty { visibility: hidden; }
|
|
63
|
-
.seams-voiceid-activation-surface .seams-voiceid-record-slot { transition: opacity
|
|
64
|
+
.seams-voiceid-activation-surface .seams-voiceid-record-slot { transition: opacity 200ms cubic-bezier(0.2, 0, 0, 1) 80ms, visibility 0s linear 0s; }
|
|
64
65
|
.seams-voiceid-activation-surface[data-preparation="loading"] .seams-voiceid-record-slot { opacity: 0; visibility: hidden; transition: none; }
|
|
65
66
|
@media (prefers-reduced-motion: reduce) {
|
|
66
67
|
.seams-voiceid-prompt { transform: none; transition: opacity 150ms ease; }
|
|
@@ -316,8 +317,9 @@ const CSS = `
|
|
|
316
317
|
font-weight: 500;
|
|
317
318
|
border: 0;
|
|
318
319
|
border-radius: var(--seams-shape-control, 10px);
|
|
319
|
-
|
|
320
|
-
|
|
320
|
+
/* The theme's main button. A page that sets no button colours keeps the ink button. */
|
|
321
|
+
background: var(--seams-colors-buttonBackground, var(--seams-colors-textPrimary, #eff2f3));
|
|
322
|
+
color: var(--seams-colors-textButton, var(--seams-colors-colorBackground, #192024));
|
|
321
323
|
}
|
|
322
324
|
.seams-voiceid-prompt .seams-voiceid-primary {
|
|
323
325
|
transition: opacity 180ms ease, visibility 0s linear 0s;
|
|
@@ -576,23 +578,7 @@ var VoicePrompt = class {
|
|
|
576
578
|
this.record.type = "button";
|
|
577
579
|
this.record.className = "seams-voiceid-primary";
|
|
578
580
|
this.record.textContent = "Record";
|
|
579
|
-
this.cancel = document
|
|
580
|
-
this.cancel.type = "button";
|
|
581
|
-
this.cancel.className = "seams-voiceid-close";
|
|
582
|
-
const closeIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
583
|
-
closeIcon.setAttribute("viewBox", "0 0 24 24");
|
|
584
|
-
closeIcon.setAttribute("width", "20");
|
|
585
|
-
closeIcon.setAttribute("height", "20");
|
|
586
|
-
closeIcon.setAttribute("aria-hidden", "true");
|
|
587
|
-
const closePath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
588
|
-
closePath.setAttribute("d", "M6 6l12 12M18 6L6 18");
|
|
589
|
-
closePath.setAttribute("fill", "none");
|
|
590
|
-
closePath.setAttribute("stroke", "currentColor");
|
|
591
|
-
closePath.setAttribute("stroke-width", "1.75");
|
|
592
|
-
closePath.setAttribute("stroke-linecap", "round");
|
|
593
|
-
closeIcon.append(closePath);
|
|
594
|
-
this.cancel.append(closeIcon);
|
|
595
|
-
this.cancel.setAttribute("aria-label", "Cancel");
|
|
581
|
+
this.cancel = cornerButton(document, "Cancel", CLOSE_PATH);
|
|
596
582
|
this.progress = document.createElement("div");
|
|
597
583
|
this.progress.className = "seams-voiceid-progress-slot";
|
|
598
584
|
const slot = document.createElement("div");
|
|
@@ -610,13 +596,22 @@ var VoicePrompt = class {
|
|
|
610
596
|
this.sizes.observe(this.element);
|
|
611
597
|
}
|
|
612
598
|
/**
|
|
613
|
-
* In an app's dialog with a step before this card, the corner
|
|
614
|
-
*
|
|
599
|
+
* In an app's dialog with a step before this card, Back in the leading corner returns to that
|
|
600
|
+
* step. With `close`, Close stays in the trailing corner and ends the whole dialog; without it,
|
|
601
|
+
* Back takes Close's place. Each one ends the card as cancelled. The result is the Back control.
|
|
615
602
|
*/
|
|
616
|
-
showBack() {
|
|
603
|
+
showBack(close) {
|
|
604
|
+
if (close) {
|
|
605
|
+
this.cancel.setAttribute("aria-label", "Close");
|
|
606
|
+
const back = cornerButton(this.element.ownerDocument, "Back", BACK_PATH);
|
|
607
|
+
back.classList.add("seams-voiceid-back");
|
|
608
|
+
this.cancel.before(back);
|
|
609
|
+
return back;
|
|
610
|
+
}
|
|
617
611
|
this.cancel.classList.add("seams-voiceid-back");
|
|
618
612
|
this.cancel.setAttribute("aria-label", "Back");
|
|
619
|
-
this.cancel.querySelector("path")?.setAttribute("d",
|
|
613
|
+
this.cancel.querySelector("path")?.setAttribute("d", BACK_PATH);
|
|
614
|
+
return this.cancel;
|
|
620
615
|
}
|
|
621
616
|
/**
|
|
622
617
|
* Moves the rows to a new state. The rows whose content changes fade out together, take their
|
|
@@ -973,11 +968,6 @@ function fadeIn(element, reduced, delay) {
|
|
|
973
968
|
function assertNever(value) {
|
|
974
969
|
throw new Error(`Unexpected capture event: ${String(value)}`);
|
|
975
970
|
}
|
|
976
|
-
const ROW_OUT_MS = 150;
|
|
977
|
-
const ROW_IN_MS = 300;
|
|
978
|
-
const ROW_IN_DELAY_MS = 60;
|
|
979
|
-
const ROW_STAGGER_MS = 40;
|
|
980
|
-
const ROW_EASE = "cubic-bezier(0.2, 0, 0, 1)";
|
|
981
971
|
const ROW_MOTION = "seams-voiceid-row";
|
|
982
972
|
/** A frame that never reaches the card's size stops being followed after this time. */
|
|
983
973
|
const FRAME_FILL_LIMIT_MS = 800;
|
|
@@ -1008,7 +998,7 @@ function fadeRowOut(row) {
|
|
|
1008
998
|
const from = Number(getComputedStyle(row).opacity);
|
|
1009
999
|
cancelRowMotion(row);
|
|
1010
1000
|
return row.animate([{ opacity: from }, { opacity: 0 }], {
|
|
1011
|
-
duration:
|
|
1001
|
+
duration: 150 * from,
|
|
1012
1002
|
easing: "ease-in",
|
|
1013
1003
|
fill: "forwards",
|
|
1014
1004
|
id: ROW_MOTION
|
|
@@ -1028,8 +1018,8 @@ function fadeRowsIn(rows) {
|
|
|
1028
1018
|
opacity: to,
|
|
1029
1019
|
transform: "none"
|
|
1030
1020
|
}], {
|
|
1031
|
-
delay: reduced ? 0 :
|
|
1032
|
-
duration: reduced ? 150 :
|
|
1021
|
+
delay: reduced ? 0 : 20 + index * 30,
|
|
1022
|
+
duration: reduced ? 150 : 200,
|
|
1033
1023
|
easing: ROW_EASE,
|
|
1034
1024
|
fill: "backwards",
|
|
1035
1025
|
id: ROW_MOTION
|
|
@@ -1039,7 +1029,7 @@ function fadeRowsIn(rows) {
|
|
|
1039
1029
|
function ignoreCancel() {}
|
|
1040
1030
|
function continueFade({ row, opacity }) {
|
|
1041
1031
|
row.animate([{ opacity }, { opacity: 1 }], {
|
|
1042
|
-
duration:
|
|
1032
|
+
duration: 200 * (1 - opacity),
|
|
1043
1033
|
easing: ROW_EASE,
|
|
1044
1034
|
id: ROW_MOTION
|
|
1045
1035
|
});
|
|
@@ -1065,10 +1055,33 @@ function leaveSurface(element) {
|
|
|
1065
1055
|
element.style.pointerEvents = "none";
|
|
1066
1056
|
const from = Number(getComputedStyle(element).opacity);
|
|
1067
1057
|
element.animate([{ opacity: from }, { opacity: 0 }], {
|
|
1068
|
-
duration:
|
|
1058
|
+
duration: 150 * from,
|
|
1069
1059
|
easing: "ease-in",
|
|
1070
1060
|
fill: "forwards"
|
|
1071
1061
|
}).finished.catch(ignoreCancel).finally(() => element.remove());
|
|
1072
1062
|
}
|
|
1063
|
+
const CLOSE_PATH = "M6 6l12 12M18 6L6 18";
|
|
1064
|
+
const BACK_PATH = "m15 18-6-6 6-6";
|
|
1065
|
+
/** A control in a corner of the card, with an icon and no text. */
|
|
1066
|
+
function cornerButton(document, label, path) {
|
|
1067
|
+
const button = document.createElement("button");
|
|
1068
|
+
button.type = "button";
|
|
1069
|
+
button.className = "seams-voiceid-close";
|
|
1070
|
+
button.setAttribute("aria-label", label);
|
|
1071
|
+
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1072
|
+
icon.setAttribute("viewBox", "0 0 24 24");
|
|
1073
|
+
icon.setAttribute("width", "20");
|
|
1074
|
+
icon.setAttribute("height", "20");
|
|
1075
|
+
icon.setAttribute("aria-hidden", "true");
|
|
1076
|
+
const line = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1077
|
+
line.setAttribute("d", path);
|
|
1078
|
+
line.setAttribute("fill", "none");
|
|
1079
|
+
line.setAttribute("stroke", "currentColor");
|
|
1080
|
+
line.setAttribute("stroke-width", "1.75");
|
|
1081
|
+
line.setAttribute("stroke-linecap", "round");
|
|
1082
|
+
icon.append(line);
|
|
1083
|
+
button.append(icon);
|
|
1084
|
+
return button;
|
|
1085
|
+
}
|
|
1073
1086
|
//#endregion
|
|
1074
1087
|
export { VoicePrompt, leaveSurface };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/card/rowMotion.ts
|
|
2
|
+
const ROW_OUT_MS = 150;
|
|
3
|
+
const ROW_IN_MS = 200;
|
|
4
|
+
const ROW_IN_DELAY_MS = 20;
|
|
5
|
+
const ROW_STAGGER_MS = 30;
|
|
6
|
+
const ROW_EASE = "cubic-bezier(0.2, 0, 0, 1)";
|
|
7
|
+
//#endregion
|
|
8
|
+
export { ROW_EASE, ROW_IN_DELAY_MS, ROW_IN_MS, ROW_OUT_MS, ROW_STAGGER_MS };
|
package/dist/esm/card/surface.js
CHANGED
|
@@ -2,7 +2,7 @@ import { captureConfig } from "../config.js";
|
|
|
2
2
|
import { voiceActivityModel } from "../voiceActivity.js";
|
|
3
3
|
import { expectedSpeechMs } from "../speechPause.js";
|
|
4
4
|
import { VoiceMicrophoneInterruptedError } from "../microphoneInput.js";
|
|
5
|
-
import { recordVoice, voiceQualityMessage } from "../microphone.js";
|
|
5
|
+
import { VoiceUserActionNeededError, recordVoice, voiceQualityMessage } from "../microphone.js";
|
|
6
6
|
import { PhraseCard } from "./phraseCard.js";
|
|
7
7
|
import { VoicePrompt, leaveSurface } from "./prompt.js";
|
|
8
8
|
//#region src/card/surface.ts
|
|
@@ -24,12 +24,17 @@ var CardSurface = class {
|
|
|
24
24
|
unmounted;
|
|
25
25
|
outer;
|
|
26
26
|
detector;
|
|
27
|
+
pressed;
|
|
28
|
+
/** Back, when the card shows it apart from Close. */
|
|
29
|
+
back;
|
|
27
30
|
recorder = null;
|
|
28
31
|
recording = null;
|
|
29
32
|
choice = null;
|
|
30
33
|
view = null;
|
|
31
34
|
started = false;
|
|
32
35
|
closed = false;
|
|
36
|
+
/** The next recording starts with no press. A card that continues another never does. */
|
|
37
|
+
autoStart;
|
|
33
38
|
/** The voiced time of the whole sentence, after which a shorter pause ends a recording. */
|
|
34
39
|
expectedSpeechMs;
|
|
35
40
|
/** The speech detector has started; before that, a recording waits for it to load. */
|
|
@@ -39,6 +44,7 @@ var CardSurface = class {
|
|
|
39
44
|
const outer = input.signal;
|
|
40
45
|
this.outer = outer;
|
|
41
46
|
this.detector = input.detector;
|
|
47
|
+
this.autoStart = input.autoStart === true;
|
|
42
48
|
this.expectedSpeechMs = expectedSpeechMs(sentence);
|
|
43
49
|
const handedOver = document.querySelector(".seams-voiceid-activation-surface[data-seams-voiceid-handoff]");
|
|
44
50
|
if (input.detector === "level") this.speechDetectorReady = true;
|
|
@@ -56,12 +62,16 @@ var CardSurface = class {
|
|
|
56
62
|
delete this.prompt.phrase.dataset.voiceidPhrase;
|
|
57
63
|
this.prompt.status.textContent = PROMPT_MESSAGE;
|
|
58
64
|
this.prompt.record.textContent = "Record";
|
|
59
|
-
|
|
65
|
+
const controls = frame.controls ?? "close";
|
|
66
|
+
this.pressed = frame.pressed;
|
|
67
|
+
const back = controls === "close" ? null : this.prompt.showBack(controls === "back_and_close");
|
|
68
|
+
this.back = back === this.prompt.cancel ? null : back;
|
|
60
69
|
const handedOverPrompt = handedOver?.querySelector(":scope > .seams-voiceid-prompt") ?? null;
|
|
61
70
|
const rows = handedOverPrompt ? handedOverRows(handedOverPrompt) : null;
|
|
62
71
|
if (handedOverPrompt) {
|
|
63
72
|
this.prompt.continueFrom(handedOverPrompt);
|
|
64
73
|
this.prompt.record.hidden = true;
|
|
74
|
+
this.autoStart = false;
|
|
65
75
|
}
|
|
66
76
|
this.card = new PhraseCard(this.prompt, {
|
|
67
77
|
label: PROMPT_LABEL,
|
|
@@ -71,7 +81,9 @@ var CardSurface = class {
|
|
|
71
81
|
}, rows);
|
|
72
82
|
this.element.append(this.prompt.element);
|
|
73
83
|
this.prompt.record.addEventListener("click", this.press);
|
|
74
|
-
|
|
84
|
+
const corner = controls === "back" ? this.pressBack : this.pressClose;
|
|
85
|
+
this.prompt.cancel.addEventListener("click", corner);
|
|
86
|
+
this.back?.addEventListener("click", this.pressBack);
|
|
75
87
|
this.element.addEventListener("cancel", this.cancel);
|
|
76
88
|
outer.addEventListener("abort", this.cancel);
|
|
77
89
|
if (handedOver) handedOver.replaceWith(this.element);
|
|
@@ -91,7 +103,8 @@ var CardSurface = class {
|
|
|
91
103
|
if (this.view) {
|
|
92
104
|
this.view = null;
|
|
93
105
|
this.card.showPrompt();
|
|
94
|
-
}
|
|
106
|
+
} else if (this.autoStart) this.begin(recording);
|
|
107
|
+
this.autoStart = false;
|
|
95
108
|
return recording.promise;
|
|
96
109
|
}
|
|
97
110
|
show(view) {
|
|
@@ -113,7 +126,9 @@ var CardSurface = class {
|
|
|
113
126
|
this.outer.removeEventListener("abort", this.cancel);
|
|
114
127
|
this.element.removeEventListener("cancel", this.cancel);
|
|
115
128
|
this.prompt.record.removeEventListener("click", this.press);
|
|
116
|
-
this.prompt.cancel.removeEventListener("click", this.
|
|
129
|
+
this.prompt.cancel.removeEventListener("click", this.pressBack);
|
|
130
|
+
this.prompt.cancel.removeEventListener("click", this.pressClose);
|
|
131
|
+
this.back?.removeEventListener("click", this.pressBack);
|
|
117
132
|
this.card.dispose();
|
|
118
133
|
this.prompt.dispose();
|
|
119
134
|
leaveSurface(this.element);
|
|
@@ -125,12 +140,15 @@ var CardSurface = class {
|
|
|
125
140
|
return;
|
|
126
141
|
}
|
|
127
142
|
if (!this.recording || this.started) return;
|
|
143
|
+
this.begin(this.recording);
|
|
144
|
+
};
|
|
145
|
+
begin(recording) {
|
|
128
146
|
this.started = true;
|
|
129
147
|
this.prompt.record.disabled = true;
|
|
130
148
|
this.prompt.status.textContent = this.speechDetectorReady ? "Opening microphone…" : `Loading ${captureConfig().label}…`;
|
|
131
|
-
this.attempt(this.
|
|
132
|
-
}
|
|
133
|
-
async attempt(recording) {
|
|
149
|
+
this.attempt(recording, this.autoStart);
|
|
150
|
+
}
|
|
151
|
+
async attempt(recording, unpressed = false) {
|
|
134
152
|
const recorder = new AbortController();
|
|
135
153
|
this.recorder = recorder;
|
|
136
154
|
try {
|
|
@@ -160,6 +178,10 @@ var CardSurface = class {
|
|
|
160
178
|
this.retry(error.message);
|
|
161
179
|
return;
|
|
162
180
|
}
|
|
181
|
+
if (unpressed && error instanceof VoiceUserActionNeededError) {
|
|
182
|
+
this.retry(PROMPT_MESSAGE, "Record");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
163
185
|
this.started = false;
|
|
164
186
|
this.recording = null;
|
|
165
187
|
recording.reject(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -173,9 +195,9 @@ var CardSurface = class {
|
|
|
173
195
|
if (event.kind === "processing") this.card.beginCheck();
|
|
174
196
|
else this.prompt.capture(event);
|
|
175
197
|
};
|
|
176
|
-
retry(message) {
|
|
198
|
+
retry(message, action = "Try again") {
|
|
177
199
|
this.started = false;
|
|
178
|
-
this.prompt.record.textContent =
|
|
200
|
+
this.prompt.record.textContent = action;
|
|
179
201
|
this.prompt.ready(message);
|
|
180
202
|
this.prompt.record.focus();
|
|
181
203
|
}
|
|
@@ -185,6 +207,13 @@ var CardSurface = class {
|
|
|
185
207
|
settle?.(choice);
|
|
186
208
|
}
|
|
187
209
|
/** A close during a check is not possible: the button is hidden while the caller works. */
|
|
210
|
+
pressBack = () => this.pressControl("back");
|
|
211
|
+
pressClose = () => this.pressControl("close");
|
|
212
|
+
pressControl(control) {
|
|
213
|
+
if (this.view?.kind === "check") return;
|
|
214
|
+
this.pressed?.(control);
|
|
215
|
+
this.cancel();
|
|
216
|
+
}
|
|
188
217
|
cancel = () => {
|
|
189
218
|
if (this.view?.kind === "check" && !this.outer.aborted) return;
|
|
190
219
|
if (this.recording) {
|
package/dist/esm/index.js
CHANGED
|
@@ -4,5 +4,5 @@ import { createVoiceActivity, levelDetector, voiceActivityModel } from "./voiceA
|
|
|
4
4
|
import { endPauseMs, expectedSpeechMs } from "./speechPause.js";
|
|
5
5
|
import { MAX_SAMPLES, SAMPLE_RATE, pcm16Wav } from "./wav.js";
|
|
6
6
|
import { VoiceMicrophoneInterruptedError, openVoiceMicrophone, rememberVoiceMicrophone, useSystemVoiceMicrophone } from "./microphoneInput.js";
|
|
7
|
-
import { InsufficientVoiceAudioError, recordVoice, requireVoiceForeground, requireVoiceUserAction, voiceQualityMessage } from "./microphone.js";
|
|
8
|
-
export { BrowserInferenceModel, InsufficientVoiceAudioError, MAX_SAMPLES, SAMPLE_RATE, VoiceAssetLoadError, VoiceMicrophoneInterruptedError, configureVoiceCapture, createVoiceActivity, endPauseMs, expectedSpeechMs, levelDetector, openVoiceMicrophone, pcm16Wav, recordVoice, rememberVoiceMicrophone, requireVoiceForeground, requireVoiceUserAction, useSystemVoiceMicrophone, voiceActivityModel, voiceInferenceEngine, voiceQualityMessage };
|
|
7
|
+
import { InsufficientVoiceAudioError, VoiceUserActionNeededError, recordVoice, requireVoiceForeground, requireVoiceUserAction, voiceQualityMessage } from "./microphone.js";
|
|
8
|
+
export { BrowserInferenceModel, InsufficientVoiceAudioError, MAX_SAMPLES, SAMPLE_RATE, VoiceAssetLoadError, VoiceMicrophoneInterruptedError, VoiceUserActionNeededError, configureVoiceCapture, createVoiceActivity, endPauseMs, expectedSpeechMs, levelDetector, openVoiceMicrophone, pcm16Wav, recordVoice, rememberVoiceMicrophone, requireVoiceForeground, requireVoiceUserAction, useSystemVoiceMicrophone, voiceActivityModel, voiceInferenceEngine, voiceQualityMessage };
|
package/dist/esm/microphone.js
CHANGED
|
@@ -36,6 +36,21 @@ function requireVoiceForeground() {
|
|
|
36
36
|
throw new Error(`Keep ${place} open and focused to use ${label}.`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* The browser starts audio only after the person acts on the page (a click or a key). A
|
|
41
|
+
* recording that starts by itself gets this error, and the caller then shows its Record control.
|
|
42
|
+
*/
|
|
43
|
+
var VoiceUserActionNeededError = class extends Error {
|
|
44
|
+
constructor() {
|
|
45
|
+
super(`Select Record to use ${captureConfig().label}.`);
|
|
46
|
+
this.name = "VoiceUserActionNeededError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/** The time that a permitted audio context needs at most to start, after its setup. */
|
|
50
|
+
const AUDIO_START_MS = 1e3;
|
|
51
|
+
function elapsed(ms) {
|
|
52
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
53
|
+
}
|
|
39
54
|
function cancelled() {
|
|
40
55
|
return /* @__PURE__ */ new Error(`${captureConfig().label} recording was cancelled.`);
|
|
41
56
|
}
|
|
@@ -243,7 +258,8 @@ var VoiceRecording = class {
|
|
|
243
258
|
worklet.port.onmessage = this.receiveFrame;
|
|
244
259
|
context.createMediaStreamSource(this.stream).connect(worklet);
|
|
245
260
|
worklet.connect(context.destination);
|
|
246
|
-
await resumed;
|
|
261
|
+
await Promise.race([resumed.catch(() => {}), elapsed(AUDIO_START_MS)]);
|
|
262
|
+
if (context.state !== "running") throw new VoiceUserActionNeededError();
|
|
247
263
|
}
|
|
248
264
|
receiveFrame = (event) => {
|
|
249
265
|
if (!(event.data instanceof Float32Array) || event.data.length !== 512) {
|
|
@@ -425,4 +441,4 @@ function assertNeverActivity(activity) {
|
|
|
425
441
|
throw new Error(`Unexpected speech activity: ${String(activity)}`);
|
|
426
442
|
}
|
|
427
443
|
//#endregion
|
|
428
|
-
export { InsufficientVoiceAudioError, recordVoice, requireVoiceForeground, requireVoiceUserAction, voiceQualityMessage };
|
|
444
|
+
export { InsufficientVoiceAudioError, VoiceUserActionNeededError, recordVoice, requireVoiceForeground, requireVoiceUserAction, voiceQualityMessage };
|
package/dist/example/example.js
CHANGED
|
@@ -9,7 +9,7 @@ configureVoiceCapture({ assetBase: '../assets/' });
|
|
|
9
9
|
const ENROLLMENT_PHRASES = [
|
|
10
10
|
'My voice is my signature for this wallet.',
|
|
11
11
|
'I send money only when I say so.',
|
|
12
|
-
'
|
|
12
|
+
'Please learn how I sound today.',
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
const log = document.querySelector('#log');
|
|
@@ -18,7 +18,13 @@ const detector = () => (document.querySelector('#level').checked ? 'level' : 'mo
|
|
|
18
18
|
|
|
19
19
|
/** Shows one sentence, records it and returns its WAV, or null when the person closed the card. */
|
|
20
20
|
async function recordSentence(sentence, signal) {
|
|
21
|
-
const surface = openPhraseCard({
|
|
21
|
+
const surface = openPhraseCard({
|
|
22
|
+
sentence,
|
|
23
|
+
signal,
|
|
24
|
+
detector: detector(),
|
|
25
|
+
autoStart: document.querySelector('#auto').checked,
|
|
26
|
+
frame: { parent },
|
|
27
|
+
});
|
|
22
28
|
try {
|
|
23
29
|
const recording = await surface.record();
|
|
24
30
|
if (recording.kind === 'cancelled') return null;
|
package/dist/example/index.html
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
<h1>voice-capture</h1>
|
|
18
18
|
<p>This page uses only <code>@seams/voice-capture</code>. The audio stays in the page.</p>
|
|
19
19
|
<label><input type="checkbox" id="level" /> Record with no model download (the detector <code>level</code>)</label>
|
|
20
|
+
<label><input type="checkbox" id="auto" /> Start the recording with no press on Record (<code>autoStart</code>)</label>
|
|
20
21
|
<div class="row">
|
|
21
22
|
<button class="start" id="sentence">Record one sentence</button>
|
|
22
23
|
<button class="start" id="enroll">Record three enrollment phrases</button>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { openPhraseCard, type PhraseCardFrame, type PhraseCardInput } from './surface';
|
|
1
|
+
export { openPhraseCard, type PhraseCardControl, type PhraseCardControls, type PhraseCardFrame, type PhraseCardInput, } from './surface';
|
|
2
2
|
export type { PhraseCheckView, PhraseChoice, PhraseRecording, PhraseResultView, PhraseSegmentState, PhraseSurface, PhraseText, PhraseView, } from './types';
|
|
3
3
|
export { leaveSurface, VoicePrompt, type PhraseDisplay, type PromptChange } from './prompt';
|
|
4
4
|
export { PhraseCard, type HandedOverRows, type PromptCopy } from './phraseCard';
|
|
@@ -46,10 +46,11 @@ export declare class VoicePrompt {
|
|
|
46
46
|
/** The text to show for a phrase, such as the phrase with its commas. */
|
|
47
47
|
display?: PhraseDisplay);
|
|
48
48
|
/**
|
|
49
|
-
* In an app's dialog with a step before this card, the corner
|
|
50
|
-
*
|
|
49
|
+
* In an app's dialog with a step before this card, Back in the leading corner returns to that
|
|
50
|
+
* step. With `close`, Close stays in the trailing corner and ends the whole dialog; without it,
|
|
51
|
+
* Back takes Close's place. Each one ends the card as cancelled. The result is the Back control.
|
|
51
52
|
*/
|
|
52
|
-
showBack():
|
|
53
|
+
showBack(close: boolean): HTMLButtonElement;
|
|
53
54
|
/**
|
|
54
55
|
* Moves the rows to a new state. The rows whose content changes fade out together, take their
|
|
55
56
|
* new content and fade in one after another; the other rows stay still. A change that comes
|
|
@@ -6,19 +6,35 @@ export type PhraseCardFrame = Readonly<{
|
|
|
6
6
|
parent?: HTMLElement;
|
|
7
7
|
/** More class names for the surface element, such as the host's theme class. */
|
|
8
8
|
className?: string;
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The corner controls of the card. `close` (default): Close in the trailing corner. `back`: Back
|
|
11
|
+
* in the leading corner, in place of Close. `back_and_close`: both. Each one, and Escape, ends
|
|
12
|
+
* the card as cancelled.
|
|
13
|
+
*/
|
|
14
|
+
controls?: PhraseCardControls;
|
|
15
|
+
/** Runs when the person presses Back or Close, before the card ends. Escape is not a press. */
|
|
16
|
+
pressed?(control: PhraseCardControl): void;
|
|
11
17
|
/** The host sizes its frame to the surface, so the card follows the frame. */
|
|
12
18
|
hugged?: boolean;
|
|
13
19
|
/** Runs when the surface is in the document. Its return value runs when the surface closes. */
|
|
14
20
|
mounted?(element: HTMLElement): (() => void) | void;
|
|
15
21
|
}>;
|
|
22
|
+
/** The corner controls of a phrase card. */
|
|
23
|
+
export type PhraseCardControls = 'close' | 'back' | 'back_and_close';
|
|
24
|
+
/** One corner control of a phrase card. */
|
|
25
|
+
export type PhraseCardControl = 'back' | 'close';
|
|
16
26
|
export type PhraseCardInput = Readonly<{
|
|
17
27
|
sentence: string;
|
|
18
28
|
signal: AbortSignal;
|
|
19
29
|
frame?: PhraseCardFrame;
|
|
20
30
|
/** The speech detector of the recordings. Default: `model`. */
|
|
21
31
|
detector?: VoiceRecordingOptions['detector'];
|
|
32
|
+
/**
|
|
33
|
+
* The first recording starts when the caller asks for it, with no press on Record. Use it
|
|
34
|
+
* when the person just asked for this card, with a click or a key. If the browser does not
|
|
35
|
+
* start audio without a press, the card stays with its Record button.
|
|
36
|
+
*/
|
|
37
|
+
autoStart?: boolean;
|
|
22
38
|
}>;
|
|
23
39
|
/**
|
|
24
40
|
* Shows the given sentence in the prompt. The surface records clips with the quality check, and
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { configureVoiceCapture, type VoiceCaptureConfig, type VoiceMicrophonePreference, type VoiceMicrophonePreferenceStore, } from './config';
|
|
2
|
-
export { recordVoice, requireVoiceForeground, requireVoiceUserAction, voiceQualityMessage, InsufficientVoiceAudioError, type VoiceActivityEvent, type VoiceCaptureEvent, type VoiceCaptureObserver, type VoiceCaptureQuality, type VoiceRecordingOptions, type VoiceRecordingResult, } from './microphone';
|
|
2
|
+
export { recordVoice, requireVoiceForeground, requireVoiceUserAction, voiceQualityMessage, InsufficientVoiceAudioError, VoiceUserActionNeededError, type VoiceActivityEvent, type VoiceCaptureEvent, type VoiceCaptureObserver, type VoiceCaptureQuality, type VoiceRecordingOptions, type VoiceRecordingResult, } from './microphone';
|
|
3
3
|
export { openVoiceMicrophone, rememberVoiceMicrophone, useSystemVoiceMicrophone, VoiceMicrophoneInterruptedError, type VoiceMicrophone, } from './microphoneInput';
|
|
4
4
|
export { endPauseMs, expectedSpeechMs } from './speechPause';
|
|
5
5
|
export { createVoiceActivity, levelDetector, voiceActivityModel, type SpeechDetector, } from './voiceActivity';
|
|
@@ -73,6 +73,13 @@ export type VoiceRecordingOptions = Readonly<{
|
|
|
73
73
|
export declare function requireVoiceUserAction(): void;
|
|
74
74
|
/** Throws unless the page is visible and has the focus. */
|
|
75
75
|
export declare function requireVoiceForeground(): void;
|
|
76
|
+
/**
|
|
77
|
+
* The browser starts audio only after the person acts on the page (a click or a key). A
|
|
78
|
+
* recording that starts by itself gets this error, and the caller then shows its Record control.
|
|
79
|
+
*/
|
|
80
|
+
export declare class VoiceUserActionNeededError extends Error {
|
|
81
|
+
constructor();
|
|
82
|
+
}
|
|
76
83
|
/**
|
|
77
84
|
* Records one utterance at 16 kHz. The recording ends after a pause, or after 8 s. A recording
|
|
78
85
|
* with less than 0.6 s of speech is `insufficient_audio`. The samples hold the speech with 0.2 s
|
package/package.json
CHANGED