@seams/voice-capture 0.67.1 → 0.68.1

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.
@@ -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};
@@ -1 +1 @@
1
- import{S 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,y as b}from"./chunks/microphone-CU5gQG6w.js";export{y as BrowserInferenceModel,_ as InsufficientVoiceAudioError,v as MAX_SAMPLES,a as SAMPLE_RATE,b as VoiceAssetLoadError,p as VoiceMicrophoneInterruptedError,e as configureVoiceCapture,c as createVoiceActivity,m as endPauseMs,d as expectedSpeechMs,s as levelDetector,g as openVoiceMicrophone,o as pcm16Wav,f as recordVoice,i as rememberVoiceMicrophone,h as requireVoiceForeground,l as requireVoiceUserAction,u as useSystemVoiceMicrophone,t as voiceActivityModel,r as voiceInferenceEngine,n as voiceQualityMessage};
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,5 +1,5 @@
1
1
  import { PhraseCard } from "./phraseCard.js";
2
- import { t as VoiceVisual } from "../chunks/visual-Di3ojTED.js";
2
+ import { t as VoiceVisual } from "../chunks/visual-CeEQ8QkM.js";
3
3
  import { VoiceRibbon } from "./ribbon.js";
4
4
  import { VoicePrompt, leaveSurface } from "./prompt.js";
5
5
  import { openPhraseCard } from "./surface.js";
@@ -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, FADE_OUT_MS)));
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, FADE_IN_DELAY_MS + index * ROW_STAGGER_MS);
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 : FADE_IN_MS,
451
- easing: EASE_IN_OUT,
447
+ duration: reduced ? 150 : 200,
448
+ easing: ROW_EASE,
452
449
  fill: "both"
453
450
  });
454
451
  }
@@ -1,6 +1,7 @@
1
1
  import { styleNonce } from "../config.js";
2
2
  import { useSystemVoiceMicrophone } from "../microphoneInput.js";
3
- import { t as VoiceVisual } from "../chunks/visual-Di3ojTED.js";
3
+ import { ROW_EASE } from "./rowMotion.js";
4
+ import { t as VoiceVisual } from "../chunks/visual-CeEQ8QkM.js";
4
5
  //#region src/card/prompt.ts
5
6
  const CSS = `
6
7
  .seams-voiceid-activation-surface { width: min(100%, 26.25rem); margin: 0 auto; background: transparent; }
@@ -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 300ms cubic-bezier(0.2, 0, 0, 1) 140ms, visibility 0s linear 0s; }
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; }
@@ -254,6 +255,17 @@ const CSS = `
254
255
  overflow: visible;
255
256
  }
256
257
  .seams-voiceid-ribbon .seams-voiceid-ribbon-check { stroke: var(--seams-colors-colorBackground, #192024); }
258
+ /* The theme can colour the voice line (with its ring and its seal) and its three echo lines,
259
+ and set their widths. With none of these values, the line has the card's text colour. */
260
+ .seams-voiceid-ribbon { color: var(--seams-colors-voiceLine, currentColor); }
261
+ .seams-voiceid-ribbon .seams-voiceid-ribbon-line { stroke-width: var(--seams-shape-voiceLineWidth, 1px); }
262
+ .seams-voiceid-ribbon .seams-voiceid-ribbon-echo {
263
+ stroke-width: var(--seams-shape-voiceEchoWidth, 0.65px);
264
+ color: var(--seams-colors-voiceEcho, var(--seams-colors-voiceLine, currentColor));
265
+ }
266
+ .seams-voiceid-ribbon .seams-voiceid-ribbon-echo[data-layer="2"] {
267
+ color: var(--seams-colors-voiceEchoDeep, var(--seams-colors-voiceEcho, var(--seams-colors-voiceLine, currentColor)));
268
+ }
257
269
  .seams-voiceid-prompt .seams-voiceid-phrase.seams-voiceid-success-headline {
258
270
  display: grid;
259
271
  align-content: end;
@@ -316,8 +328,9 @@ const CSS = `
316
328
  font-weight: 500;
317
329
  border: 0;
318
330
  border-radius: var(--seams-shape-control, 10px);
319
- background: var(--seams-colors-textPrimary, #eff2f3);
320
- color: var(--seams-colors-colorBackground, #192024);
331
+ /* The theme's main button. A page that sets no button colours keeps the ink button. */
332
+ background: var(--seams-colors-buttonBackground, var(--seams-colors-textPrimary, #eff2f3));
333
+ color: var(--seams-colors-textButton, var(--seams-colors-colorBackground, #192024));
321
334
  }
322
335
  .seams-voiceid-prompt .seams-voiceid-primary {
323
336
  transition: opacity 180ms ease, visibility 0s linear 0s;
@@ -464,6 +477,11 @@ const CSS = `
464
477
  grid-template-rows: auto minmax(6rem, auto) 5rem minmax(3.375rem, auto) 0;
465
478
  }
466
479
  .seams-voiceid-prompt[data-phrase-view="check"] .seams-voiceid-record-slot { display: none; }
480
+ /* A card that records by itself (autoStart) has no Record row: its rows sit in the middle. */
481
+ .seams-voiceid-prompt[data-auto-start] {
482
+ grid-template-rows: auto minmax(6rem, auto) 5rem minmax(3.375rem, auto) 0;
483
+ }
484
+ .seams-voiceid-prompt[data-auto-start] > .seams-voiceid-record-slot { display: none; }
467
485
  .seams-voiceid-prompt[data-phrase-view] .seams-voiceid-microphone { display: none; }
468
486
  .seams-voiceid-prompt .seams-voiceid-close { transition: opacity 150ms ease, visibility 0s linear 0s; }
469
487
  .seams-voiceid-prompt[data-phrase-view="check"] .seams-voiceid-close {
@@ -576,23 +594,7 @@ var VoicePrompt = class {
576
594
  this.record.type = "button";
577
595
  this.record.className = "seams-voiceid-primary";
578
596
  this.record.textContent = "Record";
579
- this.cancel = document.createElement("button");
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");
597
+ this.cancel = cornerButton(document, "Cancel", CLOSE_PATH);
596
598
  this.progress = document.createElement("div");
597
599
  this.progress.className = "seams-voiceid-progress-slot";
598
600
  const slot = document.createElement("div");
@@ -610,13 +612,22 @@ var VoicePrompt = class {
610
612
  this.sizes.observe(this.element);
611
613
  }
612
614
  /**
613
- * In an app's dialog with a step before this card, the corner control returns to that step.
614
- * It still ends the card as cancelled; the Wallet tells the app that the person went back.
615
+ * In an app's dialog with a step before this card, Back in the leading corner returns to that
616
+ * step. With `close`, Close stays in the trailing corner and ends the whole dialog; without it,
617
+ * Back takes Close's place. Each one ends the card as cancelled. The result is the Back control.
615
618
  */
616
- showBack() {
619
+ showBack(close) {
620
+ if (close) {
621
+ this.cancel.setAttribute("aria-label", "Close");
622
+ const back = cornerButton(this.element.ownerDocument, "Back", BACK_PATH);
623
+ back.classList.add("seams-voiceid-back");
624
+ this.cancel.before(back);
625
+ return back;
626
+ }
617
627
  this.cancel.classList.add("seams-voiceid-back");
618
628
  this.cancel.setAttribute("aria-label", "Back");
619
- this.cancel.querySelector("path")?.setAttribute("d", "m15 18-6-6 6-6");
629
+ this.cancel.querySelector("path")?.setAttribute("d", BACK_PATH);
630
+ return this.cancel;
620
631
  }
621
632
  /**
622
633
  * Moves the rows to a new state. The rows whose content changes fade out together, take their
@@ -973,11 +984,6 @@ function fadeIn(element, reduced, delay) {
973
984
  function assertNever(value) {
974
985
  throw new Error(`Unexpected capture event: ${String(value)}`);
975
986
  }
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
987
  const ROW_MOTION = "seams-voiceid-row";
982
988
  /** A frame that never reaches the card's size stops being followed after this time. */
983
989
  const FRAME_FILL_LIMIT_MS = 800;
@@ -1008,7 +1014,7 @@ function fadeRowOut(row) {
1008
1014
  const from = Number(getComputedStyle(row).opacity);
1009
1015
  cancelRowMotion(row);
1010
1016
  return row.animate([{ opacity: from }, { opacity: 0 }], {
1011
- duration: ROW_OUT_MS * from,
1017
+ duration: 150 * from,
1012
1018
  easing: "ease-in",
1013
1019
  fill: "forwards",
1014
1020
  id: ROW_MOTION
@@ -1028,8 +1034,8 @@ function fadeRowsIn(rows) {
1028
1034
  opacity: to,
1029
1035
  transform: "none"
1030
1036
  }], {
1031
- delay: reduced ? 0 : ROW_IN_DELAY_MS + index * ROW_STAGGER_MS,
1032
- duration: reduced ? 150 : ROW_IN_MS,
1037
+ delay: reduced ? 0 : 20 + index * 30,
1038
+ duration: reduced ? 150 : 200,
1033
1039
  easing: ROW_EASE,
1034
1040
  fill: "backwards",
1035
1041
  id: ROW_MOTION
@@ -1039,7 +1045,7 @@ function fadeRowsIn(rows) {
1039
1045
  function ignoreCancel() {}
1040
1046
  function continueFade({ row, opacity }) {
1041
1047
  row.animate([{ opacity }, { opacity: 1 }], {
1042
- duration: ROW_IN_MS * (1 - opacity),
1048
+ duration: 200 * (1 - opacity),
1043
1049
  easing: ROW_EASE,
1044
1050
  id: ROW_MOTION
1045
1051
  });
@@ -1065,10 +1071,33 @@ function leaveSurface(element) {
1065
1071
  element.style.pointerEvents = "none";
1066
1072
  const from = Number(getComputedStyle(element).opacity);
1067
1073
  element.animate([{ opacity: from }, { opacity: 0 }], {
1068
- duration: ROW_OUT_MS * from,
1074
+ duration: 150 * from,
1069
1075
  easing: "ease-in",
1070
1076
  fill: "forwards"
1071
1077
  }).finished.catch(ignoreCancel).finally(() => element.remove());
1072
1078
  }
1079
+ const CLOSE_PATH = "M6 6l12 12M18 6L6 18";
1080
+ const BACK_PATH = "m15 18-6-6 6-6";
1081
+ /** A control in a corner of the card, with an icon and no text. */
1082
+ function cornerButton(document, label, path) {
1083
+ const button = document.createElement("button");
1084
+ button.type = "button";
1085
+ button.className = "seams-voiceid-close";
1086
+ button.setAttribute("aria-label", label);
1087
+ const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1088
+ icon.setAttribute("viewBox", "0 0 24 24");
1089
+ icon.setAttribute("width", "20");
1090
+ icon.setAttribute("height", "20");
1091
+ icon.setAttribute("aria-hidden", "true");
1092
+ const line = document.createElementNS("http://www.w3.org/2000/svg", "path");
1093
+ line.setAttribute("d", path);
1094
+ line.setAttribute("fill", "none");
1095
+ line.setAttribute("stroke", "currentColor");
1096
+ line.setAttribute("stroke-width", "1.75");
1097
+ line.setAttribute("stroke-linecap", "round");
1098
+ icon.append(line);
1099
+ button.append(icon);
1100
+ return button;
1101
+ }
1073
1102
  //#endregion
1074
1103
  export { VoicePrompt, leaveSurface };
@@ -94,10 +94,21 @@ var VoiceRibbon = class {
94
94
  this.bottom
95
95
  ].entries()) segment.dataset.seamsProgressSegment = String(index);
96
96
  }
97
- for (let layer = 3; layer >= 0; layer -= 1) this.lines.push(this.append(document, "path", layer === 0 ? "1" : "0.65"));
97
+ for (let layer = 3; layer >= 0; layer -= 1) {
98
+ const line = this.append(document, "path", layer === 0 ? "1" : "0.65");
99
+ line.classList.add(layer === 0 ? "seams-voiceid-ribbon-line" : "seams-voiceid-ribbon-echo");
100
+ if (layer > 0) line.dataset.layer = String(layer);
101
+ this.lines.push(line);
102
+ }
98
103
  this.paint(performance.now());
99
104
  }
100
105
  readLevel = () => this.level;
106
+ /** The voice line's width now: 1, or the theme's `voiceLineWidth`. */
107
+ lineWidth() {
108
+ const line = this.lines[this.lines.length - 1];
109
+ const width = line ? Number.parseFloat(getComputedStyle(line).strokeWidth) : NaN;
110
+ return Number.isFinite(width) && width > 0 ? width : 1;
111
+ }
101
112
  setPhase(phase) {
102
113
  if (phase === "processing" && this.stage.kind === "line") {
103
114
  const amplitude = Math.max(this.level, this.idleAmplitude(performance.now()));
@@ -242,7 +253,7 @@ var VoiceRibbon = class {
242
253
  ].entries()) {
243
254
  const start = index * (width + gap);
244
255
  segment.setAttribute("d", `M${start.toFixed(2)} ${MID}H${(start + width).toFixed(2)}`);
245
- segment.setAttribute("stroke-width", "1");
256
+ segment.setAttribute("stroke-width", String(this.lineWidth()));
246
257
  segment.setAttribute("opacity", this.segmentOpacity(split, index));
247
258
  }
248
259
  }
@@ -268,7 +279,7 @@ var VoiceRibbon = class {
268
279
  const lineOpacity = LINE_REST_OPACITY + amplitude * .4;
269
280
  for (const [half, turned] of [[this.top, false], [this.bottom, true]]) {
270
281
  half.setAttribute("d", this.halfPath(tail, head, turned, amplitude, waveScale));
271
- half.setAttribute("stroke-width", lerp(1, 2, settled).toFixed(2));
282
+ half.setAttribute("stroke-width", lerp(this.lineWidth(), Math.max(2, this.lineWidth()), settled).toFixed(2));
272
283
  half.setAttribute("opacity", lerp(lineOpacity, 1, settled).toFixed(3));
273
284
  }
274
285
  this.middle.setAttribute("d", this.linePath(0, amplitude, BEND_START, WIDTH - BEND_START, waveScale));
@@ -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 };
@@ -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
- if (frame.back) this.prompt.showBack();
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,
@@ -69,16 +79,23 @@ var CardSurface = class {
69
79
  message: PROMPT_MESSAGE,
70
80
  action: "Record"
71
81
  }, rows);
82
+ if (this.autoStart) {
83
+ this.prompt.record.hidden = true;
84
+ this.prompt.element.dataset.autoStart = "";
85
+ this.prompt.element.tabIndex = -1;
86
+ }
72
87
  this.element.append(this.prompt.element);
73
88
  this.prompt.record.addEventListener("click", this.press);
74
- this.prompt.cancel.addEventListener("click", this.cancel);
89
+ const corner = controls === "back" ? this.pressBack : this.pressClose;
90
+ this.prompt.cancel.addEventListener("click", corner);
91
+ this.back?.addEventListener("click", this.pressBack);
75
92
  this.element.addEventListener("cancel", this.cancel);
76
93
  outer.addEventListener("abort", this.cancel);
77
94
  if (handedOver) handedOver.replaceWith(this.element);
78
95
  else parent.appendChild(this.element);
79
96
  if (frame.hugged) this.element.classList.add("seams-voiceid-hugged");
80
97
  this.unmounted = frame.mounted?.(this.element);
81
- if (rows) this.prompt.element.focus({ preventScroll: true });
98
+ if (rows || this.autoStart) this.prompt.element.focus({ preventScroll: true });
82
99
  else this.prompt.record.focus();
83
100
  if (outer.aborted) this.cancel();
84
101
  }
@@ -91,10 +108,12 @@ var CardSurface = class {
91
108
  if (this.view) {
92
109
  this.view = null;
93
110
  this.card.showPrompt();
94
- }
111
+ } else if (this.autoStart) this.begin(recording);
112
+ this.autoStart = false;
95
113
  return recording.promise;
96
114
  }
97
115
  show(view) {
116
+ delete this.prompt.element.dataset.autoStart;
98
117
  if (this.closed) return Promise.resolve("closed");
99
118
  this.settleChoice("replaced");
100
119
  this.view = view;
@@ -113,7 +132,9 @@ var CardSurface = class {
113
132
  this.outer.removeEventListener("abort", this.cancel);
114
133
  this.element.removeEventListener("cancel", this.cancel);
115
134
  this.prompt.record.removeEventListener("click", this.press);
116
- this.prompt.cancel.removeEventListener("click", this.cancel);
135
+ this.prompt.cancel.removeEventListener("click", this.pressBack);
136
+ this.prompt.cancel.removeEventListener("click", this.pressClose);
137
+ this.back?.removeEventListener("click", this.pressBack);
117
138
  this.card.dispose();
118
139
  this.prompt.dispose();
119
140
  leaveSurface(this.element);
@@ -125,12 +146,15 @@ var CardSurface = class {
125
146
  return;
126
147
  }
127
148
  if (!this.recording || this.started) return;
149
+ this.begin(this.recording);
150
+ };
151
+ begin(recording) {
128
152
  this.started = true;
129
153
  this.prompt.record.disabled = true;
130
154
  this.prompt.status.textContent = this.speechDetectorReady ? "Opening microphone…" : `Loading ${captureConfig().label}…`;
131
- this.attempt(this.recording);
132
- };
133
- async attempt(recording) {
155
+ this.attempt(recording, this.autoStart);
156
+ }
157
+ async attempt(recording, unpressed = false) {
134
158
  const recorder = new AbortController();
135
159
  this.recorder = recorder;
136
160
  try {
@@ -160,6 +184,10 @@ var CardSurface = class {
160
184
  this.retry(error.message);
161
185
  return;
162
186
  }
187
+ if (unpressed && error instanceof VoiceUserActionNeededError) {
188
+ this.retry(PROMPT_MESSAGE, "Record");
189
+ return;
190
+ }
163
191
  this.started = false;
164
192
  this.recording = null;
165
193
  recording.reject(error instanceof Error ? error : new Error(String(error)));
@@ -173,9 +201,10 @@ var CardSurface = class {
173
201
  if (event.kind === "processing") this.card.beginCheck();
174
202
  else this.prompt.capture(event);
175
203
  };
176
- retry(message) {
204
+ retry(message, action = "Try again") {
177
205
  this.started = false;
178
- this.prompt.record.textContent = "Try again";
206
+ delete this.prompt.element.dataset.autoStart;
207
+ this.prompt.record.textContent = action;
179
208
  this.prompt.ready(message);
180
209
  this.prompt.record.focus();
181
210
  }
@@ -185,6 +214,13 @@ var CardSurface = class {
185
214
  settle?.(choice);
186
215
  }
187
216
  /** A close during a check is not possible: the button is hidden while the caller works. */
217
+ pressBack = () => this.pressControl("back");
218
+ pressClose = () => this.pressControl("close");
219
+ pressControl(control) {
220
+ if (this.view?.kind === "check") return;
221
+ this.pressed?.(control);
222
+ this.cancel();
223
+ }
188
224
  cancel = () => {
189
225
  if (this.view?.kind === "check" && !this.outer.aborted) return;
190
226
  if (this.recording) {
@@ -1,2 +1,2 @@
1
- import { t as VoiceVisual } from "../chunks/visual-Di3ojTED.js";
2
- export { VoiceVisual };
1
+ import { n as beamPalette, t as VoiceVisual } from "../chunks/visual-CeEQ8QkM.js";
2
+ export { VoiceVisual, beamPalette };
@@ -1430,6 +1430,31 @@ ${Le.split("{id}").join(de)}` : ca }), /* @__PURE__ */ jsxs("div", {
1430
1430
  ]
1431
1431
  })] });
1432
1432
  });
1433
+ /** The Beam's palette without a theme value. */
1434
+ const DEFAULT_BEAM = "ocean";
1435
+ /**
1436
+ * The band of the Beam in greys, for the `mono` palette: its own band has colour fringes in
1437
+ * every palette, pink on light and red and blue on dark.
1438
+ */
1439
+ const GREY_BAND = {
1440
+ light: {
1441
+ core: "#8b98a5",
1442
+ above: "#cfd9de",
1443
+ mid: "#aab8c2",
1444
+ below: "#657786"
1445
+ },
1446
+ dark: {
1447
+ core: "#ffffff",
1448
+ above: "#8b98a5",
1449
+ mid: "#cfd9de",
1450
+ below: "#536471"
1451
+ }
1452
+ };
1453
+ /** The theme's `voiceBeam`, the name of a Beam palette, or the default one. */
1454
+ function beamPalette(value) {
1455
+ const name = value.trim();
1456
+ return Object.hasOwn(Va, name) ? name : DEFAULT_BEAM;
1457
+ }
1433
1458
  /** Shared by enrollment, unlock and signing. The recorder remains the only audio owner. */
1434
1459
  var VoiceVisual = class {
1435
1460
  element;
@@ -1503,18 +1528,21 @@ var VoiceVisual = class {
1503
1528
  }
1504
1529
  render = () => {
1505
1530
  if (this.disposed) return;
1531
+ const mode = (this.prompt.closest("[data-theme]")?.getAttribute("data-theme") ?? this.prompt.ownerDocument.documentElement.getAttribute("data-theme")) === "light" ? "light" : "dark";
1532
+ const palette = beamPalette(getComputedStyle(this.prompt).getPropertyValue("--seams-colors-voiceBeam"));
1506
1533
  render(createElement(VoiceBeamView, {
1507
1534
  className: "seams-voiceid-beam",
1508
- theme: (this.prompt.closest("[data-theme]")?.getAttribute("data-theme") ?? this.prompt.ownerDocument.documentElement.getAttribute("data-theme")) === "light" ? "light" : "dark",
1535
+ theme: mode,
1536
+ ...palette === "mono" ? { bandColors: GREY_BAND[mode] } : {},
1509
1537
  level: this.ribbon.readLevel,
1510
1538
  processing: this.phase === "processing",
1511
1539
  active: this.phase !== "idle",
1512
1540
  staticColors: true,
1513
- colorVariant: "ocean",
1541
+ colorVariant: palette,
1514
1542
  borderRadius: Number.parseFloat(getComputedStyle(this.prompt).borderRadius) || 16,
1515
1543
  children: null
1516
1544
  }), this.glow);
1517
1545
  };
1518
1546
  };
1519
1547
  //#endregion
1520
- export { VoiceVisual as t };
1548
+ export { beamPalette as n, VoiceVisual as t };
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 };
@@ -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 };