@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.
- package/README.md +25 -2
- package/dist/browser/card.js +49 -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/index.js +1 -1
- package/dist/esm/card/phraseCard.js +5 -8
- package/dist/esm/card/prompt.js +64 -35
- package/dist/esm/card/ribbon.js +14 -3
- package/dist/esm/card/rowMotion.js +8 -0
- package/dist/esm/card/surface.js +47 -11
- package/dist/esm/card/visual.js +2 -2
- package/dist/esm/chunks/{visual-Di3ojTED.js → visual-CeEQ8QkM.js} +31 -3
- 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/ribbon.d.ts +2 -0
- package/dist/types/card/rowMotion.d.ts +5 -0
- package/dist/types/card/surface.d.ts +18 -2
- package/dist/types/card/visual.d.ts +3 -0
- 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};
|
package/dist/esm/card/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PhraseCard } from "./phraseCard.js";
|
|
2
|
-
import { t as VoiceVisual } from "../chunks/visual-
|
|
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,
|
|
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,6 +1,7 @@
|
|
|
1
1
|
import { styleNonce } from "../config.js";
|
|
2
2
|
import { useSystemVoiceMicrophone } from "../microphoneInput.js";
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
320
|
-
|
|
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
|
|
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
|
|
614
|
-
*
|
|
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",
|
|
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:
|
|
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 :
|
|
1032
|
-
duration: reduced ? 150 :
|
|
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:
|
|
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:
|
|
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 };
|
package/dist/esm/card/ribbon.js
CHANGED
|
@@ -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)
|
|
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",
|
|
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(
|
|
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 };
|
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,
|
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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) {
|
package/dist/esm/card/visual.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as VoiceVisual } from "../chunks/visual-
|
|
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:
|
|
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:
|
|
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 };
|
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 };
|