@olares/lares-core 0.15.14 → 0.15.16

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/i18n/voice.js CHANGED
@@ -1,4 +1,12 @@
1
1
  export const ZH = {
2
+ "pc.conversation": "开始语音对话",
3
+ "pc.pause": "暂停语音对话",
4
+ "pc.resume": "继续语音对话",
5
+ "pc.finish": "完成听写",
6
+ "pc.retry": "重试",
7
+ "pc.paused": "语音已暂停",
8
+ "pc.speaking": "正在朗读",
9
+ "pc.finishFirst": "请先完成或取消听写",
2
10
  "input.keyboard": "键盘",
3
11
  "input.stopSpeaking": "停止朗读",
4
12
  "gesture.cancelRelease": "松开取消",
@@ -24,6 +32,7 @@ export const ZH = {
24
32
  "dictation.discard": "放弃",
25
33
  "dictation.original": "查看原文",
26
34
  "dictation.processing": "正在整理文字…",
35
+ "dictation.processingFailed": "文字整理失败,原始转写已保留",
27
36
  "dictation.preview": "预览并编辑",
28
37
  "dictation.retry": "重试",
29
38
  "dictation.useOriginal": "使用原文",
@@ -233,6 +242,14 @@ export const ZH = {
233
242
  };
234
243
 
235
244
  export const EN = {
245
+ "pc.conversation": "Start voice conversation",
246
+ "pc.pause": "Pause voice conversation",
247
+ "pc.resume": "Resume voice conversation",
248
+ "pc.finish": "Finish dictation",
249
+ "pc.retry": "Retry",
250
+ "pc.paused": "Voice paused",
251
+ "pc.speaking": "Speaking",
252
+ "pc.finishFirst": "Finish or discard dictation first",
236
253
  "input.keyboard": "Keyboard",
237
254
  "input.stopSpeaking": "Stop speaking",
238
255
  "gesture.cancelRelease": "Release to cancel",
@@ -258,6 +275,7 @@ export const EN = {
258
275
  "dictation.discard": "Discard",
259
276
  "dictation.original": "View original",
260
277
  "dictation.processing": "Refining text…",
278
+ "dictation.processingFailed": "Text processing failed. Original transcript preserved.",
261
279
  "dictation.preview": "Preview and edit",
262
280
  "dictation.retry": "Retry",
263
281
  "dictation.useOriginal": "Use original",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olares/lares-core",
3
- "version": "0.15.14",
3
+ "version": "0.15.16",
4
4
  "description": "Lares 共享业务核心:Router、workspace、文件/媒体、Olares 身份(无 UI、无平台壳)",
5
5
  "type": "module",
6
6
  "repository": {
package/voice/panel.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { DICTATION_PRESETS } from "./dictation.js";
2
2
 
3
3
  /** The panel owns drafts and turn policy; adapters only supply settings, audio and insertion ports. */
4
- export function createVoicePanel({ create, complete, loadConfig = async () => ({}), process = async ({ text }) => text, onSession = () => {} }) {
4
+ export function createVoicePanel({ create, complete, loadConfig = async () => ({}), process = async ({ text }) => text, onSession = () => {}, manualTurns = true, inlineDictation = false }) {
5
5
  let state = { visible: false, inputActive: false, previewVisible: false, mode: "single", sessionId: null, raw: "", draft: "", edited: false, preset: "clean", stage: "idle", snapshot: null, busy: false, error: null, device: "", levels: [], startedAt: null, elapsedMs: 0, holding: false, cancelArmed: false };
6
6
  let call = null, config = null, generation = 0, intent = 0, processing = null;
7
7
  let unsubscribe = () => {};
8
8
  let serial = Promise.resolve();
9
9
  const drafts = new Map(), listeners = new Set();
10
- const emit = (patch) => { state = { ...state, ...patch }; state.previewVisible = state.visible && state.mode === "single" && (state.stage === "preview" || state.stage === "processing" && state.previewVisible); state.inputActive = state.visible && !state.previewVisible; for (const listener of listeners) listener(); };
10
+ const emit = (patch) => { state = { ...state, ...patch }; state.previewVisible = !inlineDictation && state.visible && state.mode === "single" && (state.stage === "preview" || state.stage === "processing" && state.previewVisible); state.inputActive = state.visible && !state.previewVisible; for (const listener of listeners) listener(); };
11
11
  const save = () => state.mode === "single" && drafts.set(state.sessionId, { raw: state.raw, draft: state.draft, edited: state.edited, preset: state.preset, config });
12
12
  const cancelProcessing = () => { processing?.abort(); processing = null; };
13
13
  const enqueue = (operation) => {
@@ -72,13 +72,19 @@ export function createVoicePanel({ create, complete, loadConfig = async () => ({
72
72
  emit({ snapshot, ...(snapshot.inputLevel == null ? {} : { levels: [...state.levels.slice(-19), snapshot.inputLevel] }),
73
73
  ...(snapshot.capture === "recording" && state.snapshot?.capture !== "recording" ? { startedAt: Date.now(), elapsedMs: 0 } : snapshot.capture !== "recording" && state.startedAt ? { startedAt: null, elapsedMs: Date.now() - state.startedAt } : {}) });
74
74
  });
75
- await call.start({ sessionId: target, continuous: state.mode === "continuous", manual: state.mode === "continuous", shouldStart: () => token === intent && state.visible });
75
+ await call.start({ sessionId: target, continuous: state.mode === "continuous", manual: state.mode === "continuous" && manualTurns, shouldStart: () => token === intent && state.visible });
76
76
  if (token !== intent || !state.visible) await call.controller.cancelRecording();
77
77
  };
78
78
  const pause = async () => {
79
79
  try { if (state.mode === "single") { await finishCapture(); renderDraft(); } else await call?.controller.cancelRecording(); }
80
80
  finally { if (call?.controller.getSnapshot().connection === "open") await call.pause(); }
81
81
  };
82
+ const insertDraft = async () => {
83
+ if (!state.draft || state.stage !== "preview") return;
84
+ await complete({ sessionId: state.sessionId, text: state.draft });
85
+ drafts.delete(state.sessionId); config = null;
86
+ emit({ raw: "", draft: "", edited: false, stage: "idle", visible: false }); await end();
87
+ };
82
88
  const panel = {
83
89
  getSnapshot: () => state,
84
90
  subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
@@ -105,7 +111,16 @@ export function createVoicePanel({ create, complete, loadConfig = async () => ({
105
111
  close() { intent++; cancelProcessing(); emit({ visible: false, holding: false, cancelArmed: false }); if (state.mode === "continuous") void call?.controller.cancelRecording(); return enqueue(pause); },
106
112
  pause() { intent++; cancelProcessing(); emit({ holding: false, cancelArmed: false }); if (state.mode === "continuous") void call?.controller.cancelRecording(); return enqueue(pause); },
107
113
  start() { const token = ++intent; return enqueue(() => prepare(token)); },
108
- finish: () => state.busy ? Promise.resolve() : enqueue(async () => { await finishCapture(); if (state.raw) await transform(state.preset); else renderDraft(); }),
114
+ finish() {
115
+ if (state.busy) return Promise.resolve();
116
+ const token = intent;
117
+ return enqueue(async () => {
118
+ await finishCapture();
119
+ if (token !== intent) return;
120
+ if (state.raw) await transform(state.preset); else renderDraft();
121
+ if (inlineDictation && token === intent && state.visible && state.draft && !state.error) await insertDraft();
122
+ });
123
+ },
109
124
  edit(text) { cancelProcessing(); emit({ draft: text, edited: true, stage: "preview" }); save(); },
110
125
  selectPreset(preset, replaceEdited = false) {
111
126
  if (state.edited && !replaceEdited) return Promise.resolve(false);
@@ -127,12 +142,8 @@ export function createVoicePanel({ create, complete, loadConfig = async () => ({
127
142
  if (!call) return Promise.resolve();
128
143
  return Promise.resolve(cancel || !call.controller.getSnapshot().inputReady ? call.controller.cancelRecording() : call.controller.stopRecording());
129
144
  },
130
- complete() { return enqueue(async () => {
131
- if (!state.draft || state.stage !== "preview") return;
132
- await complete({ sessionId: state.sessionId, text: state.draft });
133
- drafts.delete(state.sessionId); config = null; emit({ raw: "", draft: "", edited: false, stage: "idle", visible: false }); await end();
134
- }); },
135
- discard() { intent++; cancelProcessing(); return enqueue(async () => { await end(); drafts.delete(state.sessionId); config = null; emit({ raw: "", draft: "", edited: false, stage: "idle", visible: false }); }); },
145
+ complete() { return enqueue(insertDraft); },
146
+ discard() { intent++; cancelProcessing(); void call?.controller.cancelRecording().catch(() => {}); return enqueue(async () => { await end(); drafts.delete(state.sessionId); config = null; emit({ raw: "", draft: "", edited: false, stage: "idle", visible: false }); }); },
136
147
  end() { intent++; emit({ holding: false, cancelArmed: false }); return enqueue(async () => { await end(); emit({ visible: false }); }); },
137
148
  dispose() { intent++; cancelProcessing(); return enqueue(async () => { try { await end(); } finally { listeners.clear(); drafts.clear(); } }); },
138
149
  };
@@ -20,3 +20,32 @@ export function voiceInputPresentation(view, now = Date.now()) {
20
20
  const label = ui.pending ? ui.status : view.cancelArmed ? "gesture.cancelRelease" : view.holding ? "gesture.release" : view.mode === "single" ? ui.error ? "dictation.retry" : "dictation.finish" : "dictation.hold";
21
21
  return { ...ui, label, elapsed, disabled: ui.pending && !view.holding, timed: ui.recording || view.holding };
22
22
  }
23
+
24
+ export function voiceDictationPresentation(view, now = Date.now()) {
25
+ const ui = voiceInputPresentation(view, now);
26
+ const processingFailed = Boolean(view.raw && view.error && view.error !== "voice_draft_changed");
27
+ return { ...ui, waveform: ui.phase === "recording" && !ui.error, processingFailed, recoverable: Boolean(view.raw && ui.error) };
28
+ }
29
+
30
+ /** Discovery establishes capability; a transient connection error does not revoke it. */
31
+ export function voiceAvailability(settings) {
32
+ if (!settings) return { dictation: false, conversation: false, reason: "loading" };
33
+ const config = settings.config || {};
34
+ const available = (models, selected) => Array.isArray(models) && models.length > 0 && (!selected || models.includes(selected));
35
+ const dictation = available(settings.sttModels, config.model);
36
+ const chat = config.preset === "direct" || available(settings.chatModels, config.chatModel === "default-chat" ? "" : config.chatModel);
37
+ const pipeline = dictation && available(settings.ttsModels, config.ttsModel) && chat;
38
+ // Automatic turns against native Realtime still require independent acceptance.
39
+ return { dictation, conversation: config.backend !== "realtime" && pipeline, reason: !dictation ? "asr" : !pipeline ? "conversation" : config.backend === "realtime" ? "realtime" : null };
40
+ }
41
+
42
+ export function voicePrimaryAction({ view, available, empty, running, disabled = false, nativeDisabled = false }) {
43
+ if (view.visible && view.mode === "single") return { action: view.error || view.snapshot?.error ? "retry" : "finish", disabled: disabled || view.busy || view.stage === "processing" || view.snapshot?.capture === "transcribing" };
44
+ if (!empty) return { action: "native", disabled: nativeDisabled };
45
+ if (view.visible && view.mode === "continuous") {
46
+ const active = view.busy || view.snapshot?.connection === "open";
47
+ return { action: active ? "pause" : available ? "resume" : "native", disabled: disabled || !active && !available };
48
+ }
49
+ if (running) return { action: "native", disabled: nativeDisabled };
50
+ return { action: available ? "conversation" : "native", disabled: disabled || !available };
51
+ }