@deepseek-ai/dsh-experimental-client-ui-voice-input 0.2.0-rc.2 → 0.2.1-alpha.2

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.i18n.yaml CHANGED
@@ -15,11 +15,11 @@
15
15
  en: d152484eb41ac6b4
16
16
  zh: 364c80ee00a374ff
17
17
  /deepseek-ai-dsh-experimental-client-ui-voice-input/use-this-package:
18
- en: 44f25ad14ceda123
19
- zh: 43c4680be42af1cf
18
+ en: 4db1c67430cfa691
19
+ zh: 484808eeff0d7692
20
20
  /deepseek-ai-dsh-experimental-client-ui-voice-input/understand-the-implementation:
21
- en: 18807008bccfdb6e
22
- zh: bb5a82c92d4aa056
21
+ en: 8fe96eeef9afe7d7
22
+ zh: 11936f647ded39eb
23
23
  /deepseek-ai-dsh-experimental-client-ui-voice-input/further-exploration:
24
24
  en: 5c64426add9d79d6
25
25
  zh: 4e06aa2ead8d2ccd
package/README.md CHANGED
@@ -27,6 +27,8 @@ This optional browser plugin adds an outline microphone icon between the model s
27
27
 
28
28
  Enable the [voice input bundle](../voice-input-bundle/README.md) from Plugins. After cache inspection, missing models trigger a setup prompt with Go to setup and Later actions. Go to setup opens bundle details; Download and prepare starts installation there. Intact cached models need no prompt or repeated installation. Before recognition is ready, guidance appears only on click, with no microphone tooltip. Clicking the microphone opens the same guidance dialog; its action opens the voice bundle details without recording or downloading. Cache inspection, preparation in progress, failures and disconnected services instead offer navigation to status, progress or error details. The dialog moves focus to its navigation action and keeps Tab traversal inside it; Later or Escape restores the previous focus without changing or submitting the draft. Before preparation, local providers can show their estimated disk, memory and setup-time requirements. The preparation summary starts collapsed; expand it to inspect completed, current and pending steps. Once ready, click the microphone, allow access and click Stop to transcribe. Cancel or Escape discards capture. Recognition feedback stays inside the toolbar. Provider and language preferences are saved from bundle details through the Settings service. Browser microphone access requires HTTPS or loopback and operating-system permission.
29
29
 
30
+ Use the native **Input device** selector in bundle details to follow the system default or select a browser microphone. System default shows the current device name when available. While bundle details are active, the selected input's live level remains visible at the right of the field; selecting another input restarts the preview for that device. Hiding or leaving the page, or disabling the plugin, releases the preview, including late permission grants. The browser remembers the device for subsequent recordings independently of Host recognition settings. Device insertion and removal refresh the options without interrupting an available selected input. A disconnected selection retains its name and position as a disabled option with an Unavailable label. Reconnecting while the page is active restores its preview; choosing another input removes the unavailable option. Interrupted recordings require an explicit restart. Permission denial keeps other choices accessible; an explicitly selected device never silently falls back to another input.
31
+
30
32
  Before downloading or retrying, **Model download source** offers Automatic and the origins advertised by the Host. SenseVoice normally offers Hugging Face and HF-Mirror; a fixed private deployment exposes only its configured origin. Manual selection uses only that source, while Automatic preserves the provider’s comparison and fallback policy. The choice is retained for retries in the current card, without changing deployment configuration or recognition preferences. Source controls are unavailable during preparation and hidden when resources are ready.
31
33
 
32
34
  -----
@@ -37,7 +39,7 @@ Before downloading or retrying, **Model download source** offers Automatic and t
37
39
  <details>
38
40
  <summary>Maintainer details — click to expand</summary>
39
41
 
40
- Native MediaRecorder captures audio and Web Audio converts it to the Host PCM format after flushing the final recorder chunks. Window blur during the microphone permission request does not cancel capture; blur during recording does. Capture failures immediately end the recording UI and offer retry inside the toolbar. Cancellation, failure and plugin disposal share one resource-release promise; the plugin retains ownership until AudioContext closure settles. The original editor selection carries a draft revision: changed drafts retain the transcript for explicit insertion or discard beside the microphone. Session changes, hiding the page during capture and plugin disposal invalidate late results and release tracks. One plugin-owned readiness subscription serves the composer, installation prompt and details. These views use the existing Slot lifecycle and own no preparation tasks. No runtime invariant companion is published because readiness comes from one Host subscription and recording state belongs to one capture operation.
42
+ Native MediaRecorder captures audio and Web Audio converts it to the Host PCM format after flushing the final recorder chunks. Input-level previews use Web Audio without creating a MediaRecorder. Window blur during the microphone permission request does not cancel capture; blur during recording does. Capture failures immediately end the recording UI and offer retry inside the toolbar. Cancellation, failure and plugin disposal share one resource-release promise; the plugin retains ownership until AudioContext closure settles. The original editor selection carries a draft revision: changed drafts retain the transcript for explicit insertion or discard beside the microphone. Session changes, hiding the page during capture and plugin disposal invalidate late results and release tracks. One plugin-owned readiness subscription serves the composer, installation prompt and details. These views use the existing Slot lifecycle and own no preparation tasks.
41
43
 
42
44
  </details>
43
45
 
package/README.zh.md CHANGED
@@ -27,6 +27,8 @@ kind: "package-reference"
27
27
 
28
28
  在插件管理页启用[语音输入 Bundle](../voice-input-bundle/README.zh.md)。缓存检查后,缺少模型时弹出安装引导,可选择“前往安装”或“稍后”。“前往安装”打开 Bundle 详情,再点击“下载并准备”才开始安装。缓存完整时不弹引导,也无需重复安装。识别未就绪时不显示麦克风悬停提示,点击后才打开同一个引导弹窗;点击引导按钮进入语音 Bundle 详情,不会录音或下载。缓存检查、正在准备、准备失败或服务断连时,弹窗改为引导查看状态、进度或错误详情。弹窗将焦点移到导航按钮,并将 Tab 切换约束在弹窗内;选择“稍后”或按 Escape 关闭时恢复此前焦点,不改变或提交草稿。准备前,本地 Provider 可展示磁盘、内存与安装时间的估算。准备摘要默认折叠;展开后可查看已完成、当前和未开始的步骤。就绪后点击麦克风,允许访问,再点击停止以转写。取消或 Escape 丢弃录音。识别反馈留在工具栏内部。Provider 和语言偏好在 Bundle 详情中通过 Settings 服务保存。浏览器麦克风访问需要 HTTPS 或环回地址,以及操作系统权限。
29
29
 
30
+ 在 Bundle 详情中使用原生**输入设备**选择器,跟随系统默认设备或选择浏览器可用的麦克风。系统默认在名称可用时显示当前设备名。Bundle 详情处于活动状态时,已选输入的实时电平持续显示在字段右侧;切换设备会为该设备重新启动预览。隐藏或离开页面、禁用插件时会释放预览,包括随后才批准的麦克风授权。浏览器会记住设备选择并用于后续录音,与 Host 的识别设置分别保存。插入和拔出设备会刷新选项,不打断仍然可用的已选输入。已选设备断开后会作为禁用选项保留名称和位置,并显示“不可用”。页面处于活动状态时,重新连接会恢复预览;改选其他输入后移除不可用选项。录音中断后需要手动重新开始。权限被拒绝时仍可选择其他设备;明确指定的设备不会静默回退到其他输入。
31
+
30
32
  下载或重试前,**模型下载源**提供自动选择及 Host 公布的源。SenseVoice 默认提供 Hugging Face 和 HF-Mirror;固定私有部署仅显示配置的源。手动选择只使用指定源,自动选择沿用提供方的比较与回退策略。当前卡片中的重试会保留选择,不修改部署配置或识别偏好。准备期间不能改源,资源就绪后隐藏源选择器。
31
33
 
32
34
  -----
@@ -37,7 +39,7 @@ kind: "package-reference"
37
39
  <details>
38
40
  <summary>维护者信息 — 点击展开</summary>
39
41
 
40
- 原生 MediaRecorder 捕获音频,等待最后一块数据后由 Web Audio 转换为 Host 的 PCM 格式。等待麦克风授权时窗口失焦不会取消采集;录音期间失焦会取消。采集失败立即结束录音界面,并在活动栏内提供重试。取消、失败和插件释放共享同一个资源释放 Promise;插件保留所有权,直到 AudioContext 关闭任务结束。原编辑器选区携带草稿版本:草稿变化时,转写文字保留在麦克风旁,供显式插入或丢弃。切换 Session、录音期间隐藏页面和释放插件会使迟到结果失效并释放轨道。插件拥有的一份准备状态订阅服务于输入框、安装引导和详情。这些视图使用既有 Slot 生命周期,不拥有准备任务。不发布运行时不变量伴随模块,因为准备状态来自一份 Host 订阅,录音状态属于一个捕获操作。
42
+ 原生 MediaRecorder 捕获音频,等待最后一块数据后由 Web Audio 转换为 Host 的 PCM 格式。输入电平预览使用 Web Audio,不创建 MediaRecorder。等待麦克风授权时窗口失焦不会取消采集;录音期间失焦会取消。采集失败立即结束录音界面,并在活动栏内提供重试。取消、失败和插件释放共享同一个资源释放 Promise;插件保留所有权,直到 AudioContext 关闭任务结束。原编辑器选区携带草稿版本:草稿变化时,转写文字保留在麦克风旁,供显式插入或丢弃。切换 Session、录音期间隐藏页面和释放插件会使迟到结果失效并释放轨道。插件拥有的一份准备状态订阅服务于输入框、安装引导和详情。这些视图使用既有 Slot 生命周期,不拥有准备任务。
41
43
 
42
44
  </details>
43
45
 
package/lib/client.js CHANGED
@@ -7,8 +7,8 @@ window.__ModuleLoader__.load({
7
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
8
  let react = require("react");
9
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
- let _deepseek_ai_dsh_api_gateway_client = require("@deepseek-ai/dsh-api-gateway/client");
11
10
  let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
11
+ let _deepseek_ai_dsh_api_gateway_client = require("@deepseek-ai/dsh-api-gateway/client");
12
12
  //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
13
13
  var _a$1;
14
14
  function $constructor(name, initializer, params) {
@@ -4654,6 +4654,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4654
4654
  /** One microphone acquisition, including a permission prompt that may settle after cancellation. */
4655
4655
  var Recording = class {
4656
4656
  onDispose;
4657
+ deviceId;
4657
4658
  stream;
4658
4659
  recorder;
4659
4660
  context;
@@ -4662,28 +4663,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4662
4663
  chunks = [];
4663
4664
  lifetime = new AbortController();
4664
4665
  disposal;
4665
- constructor(onDispose) {
4666
+ constructor(onDispose, deviceId = "") {
4666
4667
  this.onDispose = onDispose;
4668
+ this.deviceId = deviceId;
4667
4669
  }
4668
4670
  /**
4669
- * Acquire the microphone for this recording.
4670
- * @param onError - receives failures during capture, before asynchronous resource release finishes.
4671
- * @returns after capture starts; a cancelled permission grant immediately releases its tracks.
4671
+ * Acquire the selected microphone without recording or playing audio.
4672
+ * @param onError - receives device interruptions before resource release completes.
4673
+ * @returns after the level analyser starts; cancelled grants release their tracks immediately.
4672
4674
  */
4673
- async start(onError) {
4675
+ async preview(onError) {
4674
4676
  const devices = navigator.mediaDevices;
4675
- if (!devices || typeof MediaRecorder === "undefined") throw new RecordingError("unavailable");
4677
+ if (!devices) throw new RecordingError("unavailable");
4676
4678
  let stream;
4677
4679
  try {
4678
4680
  stream = await devices.getUserMedia({
4679
4681
  audio: {
4680
4682
  echoCancellation: true,
4681
- noiseSuppression: true
4683
+ noiseSuppression: true,
4684
+ ...this.deviceId === "" ? {} : { deviceId: { exact: this.deviceId } }
4682
4685
  },
4683
4686
  video: false
4684
4687
  });
4685
4688
  } catch (error) {
4686
- if (error instanceof DOMException && error.name === "NotAllowedError") throw new RecordingError("permission");
4689
+ if (error instanceof DOMException) {
4690
+ if (error.name === "NotAllowedError") throw new RecordingError("permission");
4691
+ if (error.name === "NotFoundError" || error.name === "OverconstrainedError") throw new RecordingError("deviceMissing");
4692
+ }
4687
4693
  throw error;
4688
4694
  }
4689
4695
  if (this.lifetime.signal.aborted) {
@@ -4698,18 +4704,30 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4698
4704
  this.analyser = this.context.createAnalyser();
4699
4705
  this.analyser.fftSize = this.samples.length;
4700
4706
  this.context.createMediaStreamSource(stream).connect(this.analyser);
4701
- this.recorder = new MediaRecorder(stream);
4707
+ for (const track of stream.getTracks()) track.addEventListener("ended", () => {
4708
+ this.interrupted(onError);
4709
+ }, { signal: this.lifetime.signal });
4710
+ } catch (error) {
4711
+ await this.dispose();
4712
+ throw error;
4713
+ }
4714
+ }
4715
+ /**
4716
+ * Acquire the selected microphone and record audio for transcription.
4717
+ * @param onError - receives failures during capture, before asynchronous resource release finishes.
4718
+ * @returns after capture starts; a cancelled permission grant immediately releases its tracks.
4719
+ */
4720
+ async start(onError) {
4721
+ if (typeof MediaRecorder === "undefined") throw new RecordingError("unavailable");
4722
+ await this.preview(onError);
4723
+ if (this.lifetime.signal.aborted) throw new RecordingError("cancelled");
4724
+ try {
4725
+ this.recorder = new MediaRecorder(this.stream);
4702
4726
  this.recorder.ondataavailable = (event) => {
4703
4727
  if (!this.lifetime.signal.aborted && event.data.size > 0) this.chunks.push(event.data);
4704
4728
  };
4705
4729
  this.recorder.onerror = () => {
4706
- if (this.lifetime.signal.aborted) return;
4707
- this.dispose().catch(() => void 0);
4708
- try {
4709
- onError?.(new RecordingError("interrupted"));
4710
- } catch (error) {
4711
- console.error("Speech recording error handler failed", error);
4712
- }
4730
+ this.interrupted(onError);
4713
4731
  };
4714
4732
  this.recorder.start();
4715
4733
  } catch (error) {
@@ -4717,6 +4735,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4717
4735
  throw error;
4718
4736
  }
4719
4737
  }
4738
+ interrupted(onError) {
4739
+ if (this.lifetime.signal.aborted) return;
4740
+ this.dispose().catch(() => void 0);
4741
+ try {
4742
+ onError?.(new RecordingError("interrupted"));
4743
+ } catch (error) {
4744
+ console.error("Speech recording error handler failed", error);
4745
+ }
4746
+ }
4720
4747
  /**
4721
4748
  * Read the live microphone signal.
4722
4749
  * @returns the measured RMS level, or zero outside capture.
@@ -4729,6 +4756,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4729
4756
  return Math.sqrt(sum / this.samples.length);
4730
4757
  }
4731
4758
  /**
4759
+ * Identify the input actually acquired by this operation.
4760
+ * @returns the audio track's name and device group, or undefined outside capture.
4761
+ */
4762
+ deviceInfo() {
4763
+ const track = this.stream?.getAudioTracks()[0];
4764
+ return track ? {
4765
+ label: track.label,
4766
+ groupId: track.getSettings().groupId ?? ""
4767
+ } : void 0;
4768
+ }
4769
+ /**
4732
4770
  * Finish capture and resample the recording.
4733
4771
  * @param maxDurationSeconds - truncate timer overshoot to the Host limit.
4734
4772
  * @returns one recording after the final MediaRecorder chunk arrives.
@@ -4801,8 +4839,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4801
4839
  }
4802
4840
  };
4803
4841
  //#endregion
4842
+ //#region lib/types/client/failure-text.js
4843
+ /**
4844
+ * Translate a capture or transcription failure.
4845
+ * @param failure - the rejected value.
4846
+ * @param t - the active voice-input translator.
4847
+ * @returns localized failure text.
4848
+ */
4849
+ function failureText(failure, t) {
4850
+ return failure instanceof RecordingError ? t(failure.kind) : t("failed", { message: failure instanceof Error ? failure.message : String(failure) });
4851
+ }
4852
+ //#endregion
4804
4853
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/experimental/client-ui-voice-input/src/client/VoiceInput.module.css.mjs
4805
- const css = ".ddpbLW_trigger{flex:none;width:28px;padding:0}.ddpbLW_triggerAnchor{flex:none;display:inline-flex}.ddpbLW_captureRow{align-items:center;gap:12px;width:100%;min-width:0;min-height:34px;display:flex}.ddpbLW_roundButton{corner-shape:round;background:var(--dsw-specific-selector);border-radius:50%;flex:none;width:32px;height:32px;padding:0}.ddpbLW_roundButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.ddpbLW_waveform{width:0;min-width:24px;height:24px;color:var(--dsw-alias-label-secondary);flex:1;display:block}.ddpbLW_activityMessage{min-width:0;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;flex:1;align-items:center;gap:8px;font-size:12px;display:flex;overflow:hidden}.ddpbLW_inlineAction{flex:none}.ddpbLW_preparation{border:.5px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;gap:10px;margin:12px 0;padding:14px;font-size:13px;display:flex}.ddpbLW_preparation p{margin:0}.ddpbLW_preparationActions{gap:8px;display:flex}.ddpbLW_preparationActions:empty{display:none}.ddpbLW_preparationError{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;font-size:12px}.ddpbLW_downloadFailure{overflow-wrap:anywhere;gap:6px;display:grid}.ddpbLW_summaryRow{flex-wrap:wrap;gap:4px;height:auto;min-height:28px}.ddpbLW_summaryTitle{flex:1;min-width:0}.ddpbLW_metric{color:var(--dsw-alias-label-tertiary);font-size:12px}.ddpbLW_progress{width:100%;height:6px;accent-color:var(--dsw-alias-state-business-primary)}.ddpbLW_steps{flex-direction:column;gap:14px;margin:0;padding:16px 0 2px;list-style:none;display:flex}.ddpbLW_steps li{align-items:flex-start;gap:10px;line-height:18px;display:flex}.ddpbLW_steps li>:first-child{margin-top:1px}.ddpbLW_steps li[data-step-state=pending]{color:var(--dsw-alias-label-tertiary)}.ddpbLW_stepBody{flex:1;min-width:0}.ddpbLW_stepProgress{flex-direction:column;gap:6px;margin-top:6px;display:flex}.ddpbLW_stepProgress small{color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere;font-size:11px}.ddpbLW_preferences{gap:12px;margin:18px 0;font-size:13px;display:grid}.ddpbLW_preferences label,.ddpbLW_sourceChoice label{grid-template-columns:100px minmax(120px,280px);align-items:center;gap:12px;display:grid}.ddpbLW_preferences select,.ddpbLW_sourceChoice select{border:.5px solid var(--dsw-alias-border-l1);color:inherit;font:inherit;background:0 0;border-radius:6px;padding:6px 10px}.ddpbLW_sourceChoice{gap:6px;display:grid}.ddpbLW_preferences p{margin:0}.ddpbLW_settingsCard{border:.5px solid var(--dsw-alias-border-l1);border-radius:10px;padding:16px;list-style:none}.ddpbLW_settingsCard>p{color:var(--dsw-alias-label-secondary);margin:8px 0 0;font-size:13px}.ddpbLW_setupEstimate{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);border-radius:8px;gap:10px;padding:12px;line-height:1.6;display:grid}.ddpbLW_setupEstimate dl{gap:6px;margin:0;display:grid}.ddpbLW_setupEstimate dl>div{grid-template-columns:72px minmax(0,1fr);gap:10px;display:grid}.ddpbLW_setupEstimate dt{color:var(--dsw-alias-label-tertiary)}.ddpbLW_setupEstimate dd{margin:0}.ddpbLW_setupEstimate small{color:var(--dsw-alias-label-tertiary);font-size:12px}";
4854
+ const css = ".ddpbLW_trigger{flex:none;width:28px;padding:0}.ddpbLW_triggerAnchor{flex:none;display:inline-flex}.ddpbLW_captureRow{align-items:center;gap:12px;width:100%;min-width:0;min-height:34px;display:flex}.ddpbLW_roundButton{corner-shape:round;background:var(--dsw-specific-selector);border-radius:50%;flex:none;width:32px;height:32px;padding:0}.ddpbLW_roundButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.ddpbLW_waveform{width:0;min-width:24px;height:24px;color:var(--dsw-alias-label-secondary);flex:1;display:block}.ddpbLW_activityMessage{min-width:0;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;flex:1;align-items:center;gap:8px;font-size:12px;display:flex;overflow:hidden}.ddpbLW_inlineAction{flex:none}.ddpbLW_preparation{border:.5px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;gap:10px;margin:12px 0;padding:14px;font-size:13px;display:flex}.ddpbLW_preparation p{margin:0}.ddpbLW_preparationActions{gap:8px;display:flex}.ddpbLW_preparationActions:empty{display:none}.ddpbLW_preparationError{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;font-size:12px}.ddpbLW_downloadFailure{overflow-wrap:anywhere;gap:6px;display:grid}.ddpbLW_summaryRow{flex-wrap:wrap;gap:4px;height:auto;min-height:28px}.ddpbLW_summaryTitle{flex:1;min-width:0}.ddpbLW_metric{color:var(--dsw-alias-label-tertiary);font-size:12px}.ddpbLW_progress{width:100%;height:6px;accent-color:var(--dsw-alias-state-business-primary)}.ddpbLW_steps{flex-direction:column;gap:14px;margin:0;padding:16px 0 2px;list-style:none;display:flex}.ddpbLW_steps li{align-items:flex-start;gap:10px;line-height:18px;display:flex}.ddpbLW_steps li>:first-child{margin-top:1px}.ddpbLW_steps li[data-step-state=pending]{color:var(--dsw-alias-label-tertiary)}.ddpbLW_stepBody{flex:1;min-width:0}.ddpbLW_stepProgress{flex-direction:column;gap:6px;margin-top:6px;display:flex}.ddpbLW_stepProgress small{color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere;font-size:11px}.ddpbLW_preferences{gap:12px;margin:18px 0;font-size:13px;display:grid}.ddpbLW_deviceRow,.ddpbLW_preferences label,.ddpbLW_sourceChoice label{grid-template-columns:100px minmax(120px,280px);align-items:center;gap:12px;display:grid}.ddpbLW_preferences select,.ddpbLW_sourceChoice select{border:.5px solid var(--dsw-alias-border-l1);color:inherit;font:inherit;background:0 0;border-radius:6px;padding:6px 10px}.ddpbLW_sourceChoice{gap:6px;display:grid}.ddpbLW_preferences p{margin:0}.ddpbLW_settingsCard{border:.5px solid var(--dsw-alias-border-l1);border-radius:10px;padding:16px;list-style:none}.ddpbLW_settingsCard>p{color:var(--dsw-alias-label-secondary);margin:8px 0 0;font-size:13px}.ddpbLW_setupEstimate{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);border-radius:8px;gap:10px;padding:12px;line-height:1.6;display:grid}.ddpbLW_setupEstimate dl{gap:6px;margin:0;display:grid}.ddpbLW_setupEstimate dl>div{grid-template-columns:72px minmax(0,1fr);gap:10px;display:grid}.ddpbLW_setupEstimate dt{color:var(--dsw-alias-label-tertiary)}.ddpbLW_setupEstimate dd{margin:0}.ddpbLW_setupEstimate small{color:var(--dsw-alias-label-tertiary);font-size:12px}.ddpbLW_deviceField{min-width:0;display:block;position:relative}.ddpbLW_deviceField select{width:100%}.ddpbLW_deviceField select.ddpbLW_deviceSelectMeter{padding-right:72px}.ddpbLW_deviceMeter{pointer-events:none;display:inline-flex;position:absolute;top:50%;right:24px;transform:translateY(-50%)}.ddpbLW_deviceError{min-width:0;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;grid-column:2;font-size:12px}.ddpbLW_inputLevel{flex:none;align-items:center;gap:3px;display:inline-flex}.ddpbLW_inputLevel>span{background:var(--dsw-alias-border-l1);border-radius:1px;width:3px;height:12px}.ddpbLW_inputLevel>span[data-active=true]{background:var(--dsw-alias-state-success-primary)}@media (width<=480px){.ddpbLW_deviceRow,.ddpbLW_preferences label,.ddpbLW_sourceChoice label{grid-template-columns:minmax(0,1fr);gap:6px}.ddpbLW_deviceError{grid-column:1}}";
4806
4855
  const tagId = "@deepseek-ai/dsh-experimental-client-ui-voice-input/VoiceInput.module.css";
4807
4856
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
4808
4857
  const tag = document.createElement("style");
@@ -4814,8 +4863,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4814
4863
  var VoiceInput_module_css_default = {
4815
4864
  "activityMessage": "ddpbLW_activityMessage",
4816
4865
  "captureRow": "ddpbLW_captureRow",
4866
+ "deviceError": "ddpbLW_deviceError",
4867
+ "deviceField": "ddpbLW_deviceField",
4868
+ "deviceMeter": "ddpbLW_deviceMeter",
4869
+ "deviceRow": "ddpbLW_deviceRow",
4870
+ "deviceSelectMeter": "ddpbLW_deviceSelectMeter",
4817
4871
  "downloadFailure": "ddpbLW_downloadFailure",
4818
4872
  "inlineAction": "ddpbLW_inlineAction",
4873
+ "inputLevel": "ddpbLW_inputLevel",
4819
4874
  "metric": "ddpbLW_metric",
4820
4875
  "preferences": "ddpbLW_preferences",
4821
4876
  "preparation": "ddpbLW_preparation",
@@ -4991,7 +5046,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4991
5046
  setMessage(text);
4992
5047
  setPhase("feedback");
4993
5048
  };
4994
- const failureText = (failure) => failure instanceof RecordingError ? t(failure.kind) : t("failed", { message: failure instanceof Error ? failure.message : String(failure) });
4995
5049
  const finish = async () => {
4996
5050
  const active = current.current;
4997
5051
  if (!active || active.phase !== "recording") return;
@@ -5027,7 +5081,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5027
5081
  setPhase("idle");
5028
5082
  } catch (failure) {
5029
5083
  await disposeRecording(active.capture);
5030
- if (run === generation.current) feedback(failureText(failure));
5084
+ if (run === generation.current) feedback(failureText(failure, t));
5031
5085
  } finally {
5032
5086
  if (run === generation.current) current.current = void 0;
5033
5087
  }
@@ -5054,7 +5108,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5054
5108
  current.current = void 0;
5055
5109
  clearTimeout(active.timer);
5056
5110
  active.abort.abort();
5057
- feedback(failureText(failure));
5111
+ feedback(failureText(failure, t));
5058
5112
  });
5059
5113
  if (run !== generation.current || current.current !== active) return;
5060
5114
  active.phase = "recording";
@@ -5066,7 +5120,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5066
5120
  await disposeRecording(active.capture);
5067
5121
  if (run === generation.current) {
5068
5122
  current.current = void 0;
5069
- feedback(failureText(failure));
5123
+ feedback(failureText(failure, t));
5070
5124
  }
5071
5125
  }
5072
5126
  };
@@ -5162,6 +5216,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5162
5216
  });
5163
5217
  }
5164
5218
  //#endregion
5219
+ //#region lib/types/client/microphone-device.js
5220
+ /** Browser-local microphone preference shared by settings previews and dictation. */
5221
+ /**
5222
+ * Restore the browser preference independently of Host recognition settings.
5223
+ * @returns validated device selection, persisted through the shared Client store.
5224
+ */
5225
+ function createMicrophoneDeviceStore() {
5226
+ const store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
5227
+ id: "",
5228
+ label: ""
5229
+ }, { persist: { name: "dsh.voice-input.microphone" } });
5230
+ const saved = store.getSnapshot();
5231
+ if (!saved || typeof saved !== "object" || !("id" in saved) || typeof saved.id !== "string" || !("label" in saved) || typeof saved.label !== "string") store.set({
5232
+ id: "",
5233
+ label: ""
5234
+ });
5235
+ return store;
5236
+ }
5237
+ //#endregion
5165
5238
  //#region lib/types/client/locales.js
5166
5239
  /** Dictionary namespace for every voice control. */
5167
5240
  const NS = "voice-input";
@@ -5245,6 +5318,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5245
5318
  wakingShort: "唤醒中…",
5246
5319
  provider: "识别服务",
5247
5320
  language: "识别语言",
5321
+ inputDevice: "输入设备",
5322
+ systemMicrophone: "系统默认",
5323
+ systemMicrophoneNamed: "系统默认({name})",
5324
+ unnamedSelectedMicrophone: "麦克风",
5325
+ unnamedMicrophone: "麦克风 {number}",
5326
+ inputLevel: "麦克风输入电平",
5327
+ deviceUnavailable: "不可用",
5328
+ deviceMissing: "麦克风不可用,请选择其他输入设备",
5248
5329
  auto: "自动识别",
5249
5330
  zh: "中文",
5250
5331
  en: "英语",
@@ -5349,6 +5430,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5349
5430
  wakingShort: "Waking…",
5350
5431
  provider: "Recognition service",
5351
5432
  language: "Recognition language",
5433
+ inputDevice: "Input device",
5434
+ systemMicrophone: "System default",
5435
+ systemMicrophoneNamed: "System default ({name})",
5436
+ unnamedSelectedMicrophone: "Microphone",
5437
+ unnamedMicrophone: "Microphone {number}",
5438
+ inputLevel: "Microphone input level",
5439
+ deviceUnavailable: "Unavailable",
5440
+ deviceMissing: "Microphone unavailable. Choose another input device.",
5352
5441
  auto: "Detect automatically",
5353
5442
  zh: "Chinese",
5354
5443
  en: "English",
@@ -5425,6 +5514,183 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5425
5514
  };
5426
5515
  }
5427
5516
  //#endregion
5517
+ //#region lib/types/client/MicrophonePicker.js
5518
+ /** Device selection owns microphone previews while its page is active. */
5519
+ function InputLevel({ recording, label }) {
5520
+ const meter = (0, react.useRef)(null);
5521
+ (0, react.useEffect)(() => {
5522
+ const bars = Array.from(meter.current.children);
5523
+ let frame;
5524
+ const draw = () => {
5525
+ const decibels = 20 * Math.log10(Math.max(recording.amplitude(), 1e-5));
5526
+ const count = Math.ceil(Math.max(0, Math.min(1, (decibels + 60) / 60)) * bars.length);
5527
+ for (const [index, bar] of bars.entries()) bar.setAttribute("data-active", String(index < count));
5528
+ frame = requestAnimationFrame(draw);
5529
+ };
5530
+ draw();
5531
+ return () => {
5532
+ cancelAnimationFrame(frame);
5533
+ };
5534
+ }, [recording]);
5535
+ return (0, react_jsx_runtime.jsx)("span", {
5536
+ ref: meter,
5537
+ className: VoiceInput_module_css_default.inputLevel,
5538
+ role: "img",
5539
+ "aria-label": label,
5540
+ children: Array.from({ length: 8 }, (_, index) => (0, react_jsx_runtime.jsx)("span", {}, index))
5541
+ });
5542
+ }
5543
+ /** Render the selected input and preview its measured level without recording audio. */
5544
+ function MicrophonePicker({ useMicrophoneDevice, selectMicrophone, createRecording, t }) {
5545
+ const selected = useMicrophoneDevice((value) => value);
5546
+ const [devices, setDevices] = (0, react.useState)([{
5547
+ id: selected.id,
5548
+ label: selected.id === "" ? "" : selected.label
5549
+ }]);
5550
+ const [preview, setPreview] = (0, react.useState)(), [error, setError] = (0, react.useState)();
5551
+ (0, react.useEffect)(() => {
5552
+ const media = navigator.mediaDevices;
5553
+ const lifetime = new AbortController();
5554
+ let capture, enumeration = 0;
5555
+ const release = () => {
5556
+ if (capture) capture.dispose().catch(() => void 0);
5557
+ capture = void 0;
5558
+ };
5559
+ setPreview(void 0);
5560
+ setError(void 0);
5561
+ const fail = (failure) => {
5562
+ if (lifetime.signal.aborted) return;
5563
+ setError({ failure });
5564
+ };
5565
+ const acquire = async () => {
5566
+ const recording = createRecording();
5567
+ capture = recording;
5568
+ const interrupted = (failure) => {
5569
+ if (capture !== recording || lifetime.signal.aborted) return;
5570
+ release();
5571
+ setPreview(void 0);
5572
+ fail(failure);
5573
+ refresh();
5574
+ };
5575
+ try {
5576
+ await recording.preview(interrupted);
5577
+ if (capture !== recording || lifetime.signal.aborted) return;
5578
+ setPreview(recording);
5579
+ setError(void 0);
5580
+ refresh();
5581
+ } catch (failure) {
5582
+ if (capture !== recording || lifetime.signal.aborted) return;
5583
+ release();
5584
+ setPreview(void 0);
5585
+ fail(failure);
5586
+ }
5587
+ };
5588
+ const refresh = async () => {
5589
+ const request = ++enumeration;
5590
+ try {
5591
+ if (!media) throw new RecordingError("unavailable");
5592
+ const inputs = (await media.enumerateDevices()).filter((device) => device.kind === "audioinput");
5593
+ if (lifetime.signal.aborted || request !== enumeration) return;
5594
+ const known = inputs.length === 0 || inputs.some((device) => device.label !== "");
5595
+ const missing = selected.id === "" ? inputs.length === 0 : known && !inputs.some((device) => device.deviceId === selected.id);
5596
+ const system = inputs.find((device) => device.deviceId === "default");
5597
+ const activeDefault = selected.id === "" ? capture?.deviceInfo() : void 0;
5598
+ const groupId = activeDefault?.groupId || system?.groupId;
5599
+ const choices = [{
5600
+ id: "",
5601
+ label: (groupId ? inputs.find((device) => device.groupId === groupId && device.deviceId !== "default" && device.deviceId !== "communications") : void 0)?.label || activeDefault?.label || system?.label || "",
5602
+ unavailable: inputs.length === 0
5603
+ }, ...inputs.filter((device) => device.deviceId !== "" && device.deviceId !== "default").map((device) => ({
5604
+ id: device.deviceId,
5605
+ label: device.label
5606
+ }))];
5607
+ setDevices((previous) => {
5608
+ const next = [...choices];
5609
+ if (selected.id !== "" && !next.some((device) => device.id === selected.id)) {
5610
+ const knownDevice = previous.find((device) => device.id === selected.id);
5611
+ next.splice(Math.max(1, previous.findIndex((device) => device.id === selected.id)), 0, {
5612
+ id: selected.id,
5613
+ label: knownDevice?.label ?? "",
5614
+ unavailable: missing
5615
+ });
5616
+ }
5617
+ return next;
5618
+ });
5619
+ if (missing) {
5620
+ release();
5621
+ setPreview(void 0);
5622
+ setError(void 0);
5623
+ } else if (!document.hidden && !capture) acquire();
5624
+ } catch (failure) {
5625
+ if (request === enumeration) fail(failure);
5626
+ }
5627
+ };
5628
+ const changed = () => {
5629
+ refresh();
5630
+ };
5631
+ const hidden = () => {
5632
+ if (document.hidden) {
5633
+ release();
5634
+ setPreview(void 0);
5635
+ } else changed();
5636
+ };
5637
+ media?.addEventListener("devicechange", changed, { signal: lifetime.signal });
5638
+ document.addEventListener("visibilitychange", hidden, { signal: lifetime.signal });
5639
+ changed();
5640
+ return () => {
5641
+ lifetime.abort();
5642
+ release();
5643
+ };
5644
+ }, [selected.id, createRecording]);
5645
+ const choices = devices.filter((device) => !device.unavailable || device.id === selected.id || device.id === "");
5646
+ const missing = choices.find((device) => device.id === selected.id)?.unavailable;
5647
+ const deviceName = (device, index) => {
5648
+ if (device.id === "") return device.label ? t("systemMicrophoneNamed", { name: device.label }) : t("systemMicrophone");
5649
+ return device.label || (device.id === selected.id ? selected.label : "") || (index > 0 ? t("unnamedMicrophone", { number: String(index) }) : t("unnamedSelectedMicrophone"));
5650
+ };
5651
+ return (0, react_jsx_runtime.jsxs)("div", {
5652
+ className: VoiceInput_module_css_default.deviceRow,
5653
+ "data-voice-input-device": true,
5654
+ children: [
5655
+ (0, react_jsx_runtime.jsx)("label", {
5656
+ htmlFor: "voice-input-device",
5657
+ children: t("inputDevice")
5658
+ }),
5659
+ (0, react_jsx_runtime.jsxs)("span", {
5660
+ className: VoiceInput_module_css_default.deviceField,
5661
+ children: [(0, react_jsx_runtime.jsx)("select", {
5662
+ id: "voice-input-device",
5663
+ value: selected.id,
5664
+ className: preview ? VoiceInput_module_css_default.deviceSelectMeter : void 0,
5665
+ onChange: (event) => {
5666
+ const device = choices.find((item) => item.id === event.target.value);
5667
+ selectMicrophone({
5668
+ id: device.id,
5669
+ label: device.id === "" ? "" : device.label
5670
+ });
5671
+ },
5672
+ children: choices.map((device, index) => (0, react_jsx_runtime.jsxs)("option", {
5673
+ value: device.id,
5674
+ disabled: device.unavailable,
5675
+ children: [deviceName(device, index), device.unavailable ? ` — ${t("deviceUnavailable")}` : ""]
5676
+ }, device.id))
5677
+ }), preview && !missing && (0, react_jsx_runtime.jsx)("span", {
5678
+ className: VoiceInput_module_css_default.deviceMeter,
5679
+ children: (0, react_jsx_runtime.jsx)(InputLevel, {
5680
+ recording: preview,
5681
+ label: t("inputLevel")
5682
+ })
5683
+ })]
5684
+ }),
5685
+ error && (0, react_jsx_runtime.jsx)("span", {
5686
+ className: VoiceInput_module_css_default.deviceError,
5687
+ role: "alert",
5688
+ children: failureText(error.failure, t)
5689
+ })
5690
+ ]
5691
+ });
5692
+ }
5693
+ //#endregion
5428
5694
  //#region lib/types/client/PreparationCard.js
5429
5695
  /** Provider-owned preparation steps and persisted recognition preferences. */
5430
5696
  function preparationTone(state) {
@@ -5673,6 +5939,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5673
5939
  catalog && (0, react_jsx_runtime.jsxs)("div", {
5674
5940
  className: VoiceInput_module_css_default.preferences,
5675
5941
  children: [
5942
+ (0, react_jsx_runtime.jsx)(MicrophonePicker, { ...props }),
5676
5943
  (0, react_jsx_runtime.jsxs)("label", { children: [props.t("provider"), (0, react_jsx_runtime.jsx)("select", {
5677
5944
  value: catalog.selection.providerId,
5678
5945
  disabled: !readiness.connected || saving,
@@ -5759,6 +6026,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5759
6026
  en
5760
6027
  }));
5761
6028
  const recordings = /* @__PURE__ */ new Set();
6029
+ const microphone = createMicrophoneDeviceStore();
5762
6030
  const readiness = observeReadiness(ctx);
5763
6031
  ctx.effect(() => readiness.dispose);
5764
6032
  ctx.effect(() => async () => {
@@ -5768,11 +6036,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5768
6036
  openSettings: () => {
5769
6037
  ctx.pluginNavigation.openBundle("@deepseek-ai/dsh-experimental-voice-input-bundle");
5770
6038
  },
5771
- hooks: { speechReadiness: readiness.state },
6039
+ hooks: {
6040
+ speechReadiness: readiness.state,
6041
+ microphoneDevice: microphone
6042
+ },
6043
+ selectMicrophone: (device) => {
6044
+ microphone.set(device);
6045
+ },
5772
6046
  createRecording: () => {
5773
6047
  const recording = new Recording(() => {
5774
6048
  recordings.delete(recording);
5775
- });
6049
+ }, microphone.getSnapshot().id);
5776
6050
  recordings.add(recording);
5777
6051
  return recording;
5778
6052
  },
@@ -0,0 +1,6 @@
1
+ import type { InjectFace, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { VoiceInputInjected } from './VoiceInput.tsx';
3
+ import { NS } from './locales.ts';
4
+ /** Render the selected input and preview its measured level without recording audio. */
5
+ export declare function MicrophonePicker({ useMicrophoneDevice, selectMicrophone, createRecording, t }: Pick<InjectFace<VoiceInputInjected>, 'useMicrophoneDevice' | 'selectMicrophone' | 'createRecording'> & PropsLocale<typeof NS>): import("react").JSX.Element;
6
+ //# sourceMappingURL=MicrophonePicker.d.ts.map
@@ -10,5 +10,5 @@ export type PreparationCardProps = Pick<InjectFace<VoiceInputInjected>, 'prepare
10
10
  /** Render a collapsed current-step summary or all Host-owned preparation steps. */
11
11
  export declare function PreparationCard({ provider, connected, prepare, cancelPreparation, t }: PreparationCardProps): import("react").JSX.Element;
12
12
  /** Recognition preferences and preparation cards shared by plugin details and Settings. */
13
- export declare function VoicePreparation({ useSpeechReadiness, ...props }: Pick<InjectFace<VoiceInputInjected>, 'useSpeechReadiness' | 'configure' | 'prepare' | 'cancelPreparation'> & PropsLocale<typeof NS>): import("react").JSX.Element;
13
+ export declare function VoicePreparation({ useSpeechReadiness, ...props }: Pick<InjectFace<VoiceInputInjected>, 'useSpeechReadiness' | 'configure' | 'prepare' | 'cancelPreparation' | 'createRecording' | 'useMicrophoneDevice' | 'selectMicrophone'> & PropsLocale<typeof NS>): import("react").JSX.Element;
14
14
  //# sourceMappingURL=PreparationCard.d.ts.map
@@ -3,6 +3,7 @@ import type { TranscriptionRequest } from '@deepseek-ai/dsh-experimental-api-spe
3
3
  import type { SpeechPreparationOptions, SpeechProviderId, SpeechSelectionPatch, Transcript } from '@deepseek-ai/dsh-experimental-speech-to-text/types';
4
4
  import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
5
5
  import { type Recording } from './audio.ts';
6
+ import type { MicrophoneDevice } from './microphone-device.ts';
6
7
  import type { SpeechReadiness } from './readiness.ts';
7
8
  import { NS } from './locales.ts';
8
9
  /** Host calls injected without exposing a Cordis Context to React. */
@@ -11,6 +12,8 @@ export interface VoiceInputActions {
11
12
  openSettings: () => void;
12
13
  /** @returns one microphone operation owned by the plugin lifecycle. */
13
14
  createRecording: () => Recording;
15
+ /** Remember the browser microphone used by previews and subsequent recordings. */
16
+ selectMicrophone: (device: MicrophoneDevice) => void;
14
17
  transcribe: (request: TranscriptionRequest, signal: AbortSignal) => Promise<RemoteResult<Transcript>>;
15
18
  prepare: (providerId: SpeechProviderId, options?: SpeechPreparationOptions) => Promise<void>;
16
19
  cancelPreparation: (providerId: SpeechProviderId) => Promise<void>;
@@ -20,6 +23,7 @@ export interface VoiceInputActions {
20
23
  export interface VoiceInputInjected extends VoiceInputActions {
21
24
  hooks: {
22
25
  speechReadiness: HostObservable<SpeechReadiness>;
26
+ microphoneDevice: HostObservable<MicrophoneDevice>;
23
27
  };
24
28
  }
25
29
  /** Composer-owned expansion and draft actions; preferences stay in plugin settings. */
@@ -1,8 +1,8 @@
1
1
  /** Browser-owned microphone capture and native Web Audio resampling. */
2
2
  /** Capture failure whose message is localized by the caller. */
3
3
  export declare class RecordingError extends Error {
4
- readonly kind: 'unavailable' | 'permission' | 'empty' | 'cancelled' | 'interrupted';
5
- constructor(kind: 'unavailable' | 'permission' | 'empty' | 'cancelled' | 'interrupted');
4
+ readonly kind: 'unavailable' | 'permission' | 'empty' | 'cancelled' | 'interrupted' | 'deviceMissing';
5
+ constructor(kind: 'unavailable' | 'permission' | 'empty' | 'cancelled' | 'interrupted' | 'deviceMissing');
6
6
  }
7
7
  /**
8
8
  * Encode mono floating-point samples as the canonical PCM16 WAV accepted by the Host.
@@ -19,6 +19,7 @@ export declare function audioBase64(bytes: Uint8Array): string;
19
19
  /** One microphone acquisition, including a permission prompt that may settle after cancellation. */
20
20
  export declare class Recording {
21
21
  private readonly onDispose;
22
+ private readonly deviceId;
22
23
  private stream;
23
24
  private recorder;
24
25
  private context;
@@ -27,18 +28,30 @@ export declare class Recording {
27
28
  private chunks;
28
29
  private readonly lifetime;
29
30
  private disposal;
30
- constructor(onDispose: () => void);
31
+ constructor(onDispose: () => void, deviceId?: string);
31
32
  /**
32
- * Acquire the microphone for this recording.
33
+ * Acquire the selected microphone without recording or playing audio.
34
+ * @param onError - receives device interruptions before resource release completes.
35
+ * @returns after the level analyser starts; cancelled grants release their tracks immediately.
36
+ */
37
+ preview(onError?: (error: RecordingError) => void): Promise<void>;
38
+ /**
39
+ * Acquire the selected microphone and record audio for transcription.
33
40
  * @param onError - receives failures during capture, before asynchronous resource release finishes.
34
41
  * @returns after capture starts; a cancelled permission grant immediately releases its tracks.
35
42
  */
36
43
  start(onError?: (error: RecordingError) => void): Promise<void>;
44
+ private interrupted;
37
45
  /**
38
46
  * Read the live microphone signal.
39
47
  * @returns the measured RMS level, or zero outside capture.
40
48
  */
41
49
  amplitude(): number;
50
+ /**
51
+ * Identify the input actually acquired by this operation.
52
+ * @returns the audio track's name and device group, or undefined outside capture.
53
+ */
54
+ deviceInfo(): Pick<MediaDeviceInfo, 'label' | 'groupId'> | undefined;
42
55
  /**
43
56
  * Finish capture and resample the recording.
44
57
  * @param maxDurationSeconds - truncate timer overshoot to the Host limit.
@@ -0,0 +1,11 @@
1
+ /** Locale-owned messages shared by microphone preview and dictation. */
2
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
3
+ import type { NS } from './locales.ts';
4
+ /**
5
+ * Translate a capture or transcription failure.
6
+ * @param failure - the rejected value.
7
+ * @param t - the active voice-input translator.
8
+ * @returns localized failure text.
9
+ */
10
+ export declare function failureText(failure: unknown, t: PropsLocale<typeof NS>['t']): string;
11
+ //# sourceMappingURL=failure-text.d.ts.map
@@ -80,6 +80,14 @@ export declare const zh: {
80
80
  readonly wakingShort: "唤醒中…";
81
81
  readonly provider: "识别服务";
82
82
  readonly language: "识别语言";
83
+ readonly inputDevice: "输入设备";
84
+ readonly systemMicrophone: "系统默认";
85
+ readonly systemMicrophoneNamed: "系统默认({name})";
86
+ readonly unnamedSelectedMicrophone: "麦克风";
87
+ readonly unnamedMicrophone: "麦克风 {number}";
88
+ readonly inputLevel: "麦克风输入电平";
89
+ readonly deviceUnavailable: "不可用";
90
+ readonly deviceMissing: "麦克风不可用,请选择其他输入设备";
83
91
  readonly auto: "自动识别";
84
92
  readonly zh: "中文";
85
93
  readonly en: "英语";
@@ -0,0 +1,13 @@
1
+ /** Browser-local microphone preference shared by settings previews and dictation. */
2
+ import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
3
+ /** An empty id follows the system input; labels store browser names without localized placeholders. */
4
+ export interface MicrophoneDevice {
5
+ readonly id: string;
6
+ readonly label: string;
7
+ }
8
+ /**
9
+ * Restore the browser preference independently of Host recognition settings.
10
+ * @returns validated device selection, persisted through the shared Client store.
11
+ */
12
+ export declare function createMicrophoneDeviceStore(): SnapshotStore<MicrophoneDevice>;
13
+ //# sourceMappingURL=microphone-device.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-experimental-client-ui-voice-input",
3
3
  "description": "Record speech and insert editable text into the conversation draft",
4
- "version": "0.2.0-rc.2",
4
+ "version": "0.2.1-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -33,45 +33,45 @@
33
33
  "license": "MIT",
34
34
  "dependencies": {},
35
35
  "peerDependencies": {
36
- "@deepseek-ai/cordis": "~4.0.4",
37
- "@deepseek-ai/dsh-api-remotes": "0.2.0-rc.2",
38
- "@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.2",
39
- "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
40
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2",
41
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
42
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
43
- "@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.2",
44
- "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
45
- "@deepseek-ai/dsh-experimental-api-speech-to-text": "0.2.0-rc.2",
46
- "@deepseek-ai/dsh-experimental-speech-to-text": "0.2.0-rc.2",
47
- "@deepseek-ai/dsh-api-gateway": "0.2.0-rc.2",
48
- "@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
49
- "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
50
- "@deepseek-ai/dsh-session": "0.2.0-rc.2",
51
- "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.0-rc.2"
36
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1",
37
+ "@deepseek-ai/dsh-api-remotes": "0.2.1-alpha.2",
38
+ "@deepseek-ai/dsh-api-session-controller": "0.2.1-alpha.2",
39
+ "@deepseek-ai/dsh-client-locale": "0.2.1-alpha.2",
40
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-alpha.2",
41
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-alpha.2",
42
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-alpha.2",
43
+ "@deepseek-ai/dsh-client-ui-session": "0.2.1-alpha.2",
44
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.2",
45
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
46
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2",
47
+ "@deepseek-ai/dsh-experimental-api-speech-to-text": "0.2.1-alpha.2",
48
+ "@deepseek-ai/dsh-experimental-speech-to-text": "0.2.1-alpha.2",
49
+ "@deepseek-ai/dsh-api-gateway": "0.2.1-alpha.2",
50
+ "@deepseek-ai/dsh-client-store": "0.2.1-alpha.2",
51
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-alpha.2"
52
52
  },
53
53
  "devDependencies": {
54
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1",
55
+ "@deepseek-ai/dsh-api-remotes": "0.2.1-alpha.2",
56
+ "@deepseek-ai/dsh-api-session-controller": "0.2.1-alpha.2",
57
+ "@deepseek-ai/dsh-client-locale": "0.2.1-alpha.2",
58
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-alpha.2",
59
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-alpha.2",
60
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-alpha.2",
61
+ "@deepseek-ai/dsh-client-ui-session": "0.2.1-alpha.2",
62
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.2",
63
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
64
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2",
65
+ "@deepseek-ai/dsh-experimental-api-speech-to-text": "0.2.1-alpha.2",
66
+ "@deepseek-ai/dsh-experimental-speech-to-text": "0.2.1-alpha.2",
67
+ "@deepseek-ai/dsh-client-test-runtime": "0.2.1-alpha.2",
54
68
  "@types/react": "~18.3.1",
55
69
  "react": "^18.2.0",
56
70
  "react-dom": "^18.2.0",
57
- "@testing-library/react": "^16.1.0",
58
- "@deepseek-ai/cordis": "~4.0.4",
59
- "@deepseek-ai/dsh-api-remotes": "0.2.0-rc.2",
60
- "@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.2",
61
- "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
62
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2",
63
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
64
- "@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.2",
65
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
66
- "@deepseek-ai/dsh-session": "0.2.0-rc.2",
67
- "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
68
- "@deepseek-ai/dsh-experimental-api-speech-to-text": "0.2.0-rc.2",
69
- "@deepseek-ai/dsh-experimental-speech-to-text": "0.2.0-rc.2",
70
- "@deepseek-ai/dsh-client-test-runtime": "0.2.0-rc.2",
71
- "@deepseek-ai/dsh-api-gateway": "0.2.0-rc.2",
72
- "@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
73
- "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.0-rc.2",
74
- "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2"
71
+ "@testing-library/react": "^16.3.3",
72
+ "@deepseek-ai/dsh-api-gateway": "0.2.1-alpha.2",
73
+ "@deepseek-ai/dsh-client-store": "0.2.1-alpha.2",
74
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-alpha.2"
75
75
  },
76
76
  "dsh": {
77
77
  "client": {