dsh-xrxs-lingting 0.4.0-dev.50 → 0.4.0-dev.51
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 +2 -0
- package/package.json +1 -1
- package/plugin-dist/client.js +147 -6
package/README.md
CHANGED
|
@@ -110,3 +110,5 @@ npm run smoke:client -- /path/to/cinlyn-desktop
|
|
|
110
110
|
该检查不代替安装后对授权、录音、转写和回放的人工验证。
|
|
111
111
|
|
|
112
112
|
布局回归验证:`node scripts/smoke-layout-browser.mjs <playwright/index.mjs 路径>`,使用 Chrome 无头浏览器运行真实插件产物,验证页面边界、弹窗居中、转写面板层级、窗口缩放与卸载后聊天恢复。使用合成录音状态,不访问业务数据、不采集声音。
|
|
113
|
+
|
|
114
|
+
设备切换:支持的桌面原生模块会在默认输出变化时重新连接系统采集,插件重新采样并替换默认麦克风输入,保留同一录音与转写连接。操作系统切换设备期间可能存在短暂音频缺口;持续无法恢复才报错暂停。
|
package/package.json
CHANGED
package/plugin-dist/client.js
CHANGED
|
@@ -1606,6 +1606,117 @@ function toSegment(segment) {
|
|
|
1606
1606
|
return { id: segment.id, speaker: speakerLabel(segment.speakerId), text: segment.text, final: segment.final };
|
|
1607
1607
|
}
|
|
1608
1608
|
|
|
1609
|
+
// src/audio/microphone-switch.ts
|
|
1610
|
+
function followDefaultMicrophone(options) {
|
|
1611
|
+
let current = options.initial;
|
|
1612
|
+
let stopped = false;
|
|
1613
|
+
let running = false;
|
|
1614
|
+
let queued = false;
|
|
1615
|
+
let cancelAcquire;
|
|
1616
|
+
let cancelDelay;
|
|
1617
|
+
const delay = () => new Promise((resolve) => {
|
|
1618
|
+
const timer = setTimeout(() => {
|
|
1619
|
+
cancelDelay = void 0;
|
|
1620
|
+
resolve();
|
|
1621
|
+
}, 250);
|
|
1622
|
+
cancelDelay = () => {
|
|
1623
|
+
clearTimeout(timer);
|
|
1624
|
+
cancelDelay = void 0;
|
|
1625
|
+
resolve();
|
|
1626
|
+
};
|
|
1627
|
+
});
|
|
1628
|
+
const watch = (stream) => {
|
|
1629
|
+
stream.getAudioTracks().forEach((track) => {
|
|
1630
|
+
track.onended = () => {
|
|
1631
|
+
void reconnect();
|
|
1632
|
+
};
|
|
1633
|
+
});
|
|
1634
|
+
};
|
|
1635
|
+
const reconnect = async () => {
|
|
1636
|
+
if (stopped) return;
|
|
1637
|
+
if (running) {
|
|
1638
|
+
queued = true;
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
running = true;
|
|
1642
|
+
const began = Date.now();
|
|
1643
|
+
try {
|
|
1644
|
+
do {
|
|
1645
|
+
queued = false;
|
|
1646
|
+
try {
|
|
1647
|
+
const next = await new Promise((resolve, reject) => {
|
|
1648
|
+
let settled = false;
|
|
1649
|
+
const finishError = (error) => {
|
|
1650
|
+
if (settled) return;
|
|
1651
|
+
settled = true;
|
|
1652
|
+
clearTimeout(timer);
|
|
1653
|
+
cancelAcquire = void 0;
|
|
1654
|
+
reject(error);
|
|
1655
|
+
};
|
|
1656
|
+
const timer = setTimeout(() => finishError(new Error("Microphone recovery timed out")), Math.max(0, 8e3 - (Date.now() - began)));
|
|
1657
|
+
cancelAcquire = () => finishError(new Error("Recording stopped"));
|
|
1658
|
+
Promise.resolve().then(options.acquire).then((stream) => {
|
|
1659
|
+
if (settled || stopped) {
|
|
1660
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
settled = true;
|
|
1664
|
+
clearTimeout(timer);
|
|
1665
|
+
cancelAcquire = void 0;
|
|
1666
|
+
resolve(stream);
|
|
1667
|
+
}, finishError);
|
|
1668
|
+
});
|
|
1669
|
+
if (stopped) {
|
|
1670
|
+
next.getTracks().forEach((track) => track.stop());
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
if (!next.getAudioTracks().some((track) => track.readyState === "live")) {
|
|
1674
|
+
next.getTracks().forEach((track) => track.stop());
|
|
1675
|
+
throw new Error("Microphone is not live");
|
|
1676
|
+
}
|
|
1677
|
+
const previous = current;
|
|
1678
|
+
try {
|
|
1679
|
+
options.replace(next, previous);
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
next.getTracks().forEach((track) => track.stop());
|
|
1682
|
+
throw error;
|
|
1683
|
+
}
|
|
1684
|
+
current = next;
|
|
1685
|
+
previous.getTracks().forEach((track) => {
|
|
1686
|
+
track.onended = null;
|
|
1687
|
+
track.stop();
|
|
1688
|
+
});
|
|
1689
|
+
watch(current);
|
|
1690
|
+
} catch {
|
|
1691
|
+
if (stopped) return;
|
|
1692
|
+
if (Date.now() - began >= 8e3) {
|
|
1693
|
+
if (!current.getAudioTracks().some((track) => track.readyState === "live")) options.failed();
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
await delay();
|
|
1697
|
+
queued = true;
|
|
1698
|
+
}
|
|
1699
|
+
} while (queued && !stopped);
|
|
1700
|
+
} finally {
|
|
1701
|
+
running = false;
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
const changed = () => {
|
|
1705
|
+
void reconnect();
|
|
1706
|
+
};
|
|
1707
|
+
watch(current);
|
|
1708
|
+
options.devices.addEventListener?.("devicechange", changed);
|
|
1709
|
+
return () => {
|
|
1710
|
+
stopped = true;
|
|
1711
|
+
cancelDelay?.();
|
|
1712
|
+
cancelAcquire?.();
|
|
1713
|
+
options.devices.removeEventListener?.("devicechange", changed);
|
|
1714
|
+
current.getTracks().forEach((track) => {
|
|
1715
|
+
track.onended = null;
|
|
1716
|
+
});
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1609
1720
|
// src/audio/worklet-source.ts
|
|
1610
1721
|
var captureWorkletSource = `
|
|
1611
1722
|
class LingtingCaptureProcessor extends AudioWorkletProcessor {
|
|
@@ -1691,7 +1802,7 @@ class NativeSystemSource extends AudioWorkletProcessor {
|
|
|
1691
1802
|
accept(data) {
|
|
1692
1803
|
if (this.failed) return;
|
|
1693
1804
|
if (!data || !(data.samples instanceof Float32Array) || !Number.isFinite(data.sampleRate) || data.sampleRate < 8000 || data.sampleRate > 192000) return;
|
|
1694
|
-
if (this.rate && this.rate !== data.sampleRate) { this.
|
|
1805
|
+
if (this.rate && this.rate !== data.sampleRate) { this.read = 0; this.write = 0; this.started = false; this.buffer.fill(0); }
|
|
1695
1806
|
this.rate = data.sampleRate;
|
|
1696
1807
|
// Bound latency, not just memory. After a render stall, retain the newest
|
|
1697
1808
|
// 100 ms instead of spending minutes playing stale audio or killing capture.
|
|
@@ -1784,6 +1895,7 @@ var MeetingCapture = class {
|
|
|
1784
1895
|
emitting = false;
|
|
1785
1896
|
stopSystem = null;
|
|
1786
1897
|
systemNode = null;
|
|
1898
|
+
stopMicrophoneSwitch = null;
|
|
1787
1899
|
async start(onChunk, onError) {
|
|
1788
1900
|
if (this.context || this.starting) throw new Error("\u91C7\u96C6\u5DF2\u5728\u8FDB\u884C\u4E2D\u3002");
|
|
1789
1901
|
this.starting = true;
|
|
@@ -1822,7 +1934,6 @@ var MeetingCapture = class {
|
|
|
1822
1934
|
for (const stream of this.streams) {
|
|
1823
1935
|
for (const track of stream.getAudioTracks()) {
|
|
1824
1936
|
if (track.readyState !== "live") throw new Error("\u97F3\u9891\u8BBE\u5907\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB\u5F55\u97F3\u3002");
|
|
1825
|
-
track.onended = () => fail(stream === microphone ? "\u9EA6\u514B\u98CE\u5DF2\u65AD\u5F00\uFF0C\u5F55\u97F3\u5DF2\u6682\u505C\uFF0C\u8BF7\u68C0\u67E5\u8BBE\u5907\u540E\u7EE7\u7EED\u3002" : "\u7CFB\u7EDF\u58F0\u97F3\u91C7\u96C6\u5DF2\u505C\u6B62\uFF0C\u5F55\u97F3\u5DF2\u6682\u505C\uFF0C\u8BF7\u91CD\u65B0\u6388\u6743\u540E\u7EE7\u7EED\u3002");
|
|
1826
1937
|
}
|
|
1827
1938
|
}
|
|
1828
1939
|
const context = new AudioContext();
|
|
@@ -1836,7 +1947,7 @@ var MeetingCapture = class {
|
|
|
1836
1947
|
return;
|
|
1837
1948
|
}
|
|
1838
1949
|
stalledSince ??= Date.now();
|
|
1839
|
-
if (Date.now() - stalledSince >=
|
|
1950
|
+
if (Date.now() - stalledSince >= 8e3) {
|
|
1840
1951
|
fail("\u97F3\u9891\u5904\u7406\u6062\u590D\u8D85\u65F6\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB\u5F55\u97F3\u3002");
|
|
1841
1952
|
return;
|
|
1842
1953
|
}
|
|
@@ -1846,7 +1957,8 @@ var MeetingCapture = class {
|
|
|
1846
1957
|
return;
|
|
1847
1958
|
}
|
|
1848
1959
|
recovering = true;
|
|
1849
|
-
void context.resume().catch(() =>
|
|
1960
|
+
void context.resume().catch(() => {
|
|
1961
|
+
}).finally(() => {
|
|
1850
1962
|
recovering = false;
|
|
1851
1963
|
});
|
|
1852
1964
|
};
|
|
@@ -1924,6 +2036,24 @@ var MeetingCapture = class {
|
|
|
1924
2036
|
this.nodes.push(source, analyser, gain);
|
|
1925
2037
|
analysers.push(analyser);
|
|
1926
2038
|
}
|
|
2039
|
+
this.stopMicrophoneSwitch = followDefaultMicrophone({
|
|
2040
|
+
devices,
|
|
2041
|
+
initial: microphone,
|
|
2042
|
+
acquire: () => devices.getUserMedia({ audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true, autoGainControl: true } }),
|
|
2043
|
+
replace: (next, previous) => {
|
|
2044
|
+
if (generation !== this.generation) throw new Error("\u5F55\u97F3\u5DF2\u53D6\u6D88\u3002");
|
|
2045
|
+
const replacement = context.createMediaStreamSource(next);
|
|
2046
|
+
replacement.connect(analysers[0]);
|
|
2047
|
+
const oldSource = sources[0];
|
|
2048
|
+
oldSource.disconnect();
|
|
2049
|
+
sources[0] = replacement;
|
|
2050
|
+
this.nodes = this.nodes.filter((item) => item !== oldSource);
|
|
2051
|
+
this.nodes.push(replacement);
|
|
2052
|
+
this.streams = this.streams.filter((item) => item !== previous);
|
|
2053
|
+
this.streams.push(next);
|
|
2054
|
+
},
|
|
2055
|
+
failed: () => fail("\u9EA6\u514B\u98CE\u5207\u6362\u540E\u672A\u80FD\u6062\u590D\uFF0C\u5F55\u97F3\u5DF2\u6682\u505C\uFF0C\u8BF7\u68C0\u67E5\u8BBE\u5907\u8FDE\u63A5\u3002")
|
|
2056
|
+
});
|
|
1927
2057
|
const mute = context.createGain();
|
|
1928
2058
|
mute.gain.value = 0;
|
|
1929
2059
|
node.connect(mute);
|
|
@@ -1970,6 +2100,8 @@ var MeetingCapture = class {
|
|
|
1970
2100
|
}
|
|
1971
2101
|
async stop() {
|
|
1972
2102
|
++this.generation;
|
|
2103
|
+
this.stopMicrophoneSwitch?.();
|
|
2104
|
+
this.stopMicrophoneSwitch = null;
|
|
1973
2105
|
this.emitting = false;
|
|
1974
2106
|
if (this.timer) clearInterval(this.timer);
|
|
1975
2107
|
this.timer = null;
|
|
@@ -5450,7 +5582,7 @@ function mountBusinessSurface(doc) {
|
|
|
5450
5582
|
else center.style.removeProperty("position");
|
|
5451
5583
|
} };
|
|
5452
5584
|
}
|
|
5453
|
-
function BusinessSurface({ children }) {
|
|
5585
|
+
function BusinessSurface({ children, onClose }) {
|
|
5454
5586
|
const [target, setTarget] = (0, import_react22.useState)(null);
|
|
5455
5587
|
(0, import_react22.useLayoutEffect)(() => {
|
|
5456
5588
|
let mounted = null;
|
|
@@ -5461,9 +5593,18 @@ function BusinessSurface({ children }) {
|
|
|
5461
5593
|
setTarget(mounted?.target ?? null);
|
|
5462
5594
|
};
|
|
5463
5595
|
attach();
|
|
5596
|
+
const onSidebarClick = (event) => {
|
|
5597
|
+
const element = event.target instanceof Element ? event.target : null;
|
|
5598
|
+
const button = element?.closest("button");
|
|
5599
|
+
if (!button?.closest('[data-slot="sidebar"]')) return;
|
|
5600
|
+
const label = button.getAttribute("aria-label")?.trim();
|
|
5601
|
+
if (label && /^(新会话|新建会话|New session)$/i.test(label)) onClose();
|
|
5602
|
+
};
|
|
5603
|
+
document.addEventListener("click", onSidebarClick, true);
|
|
5464
5604
|
const observer = new MutationObserver(attach);
|
|
5465
5605
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
5466
5606
|
return () => {
|
|
5607
|
+
document.removeEventListener("click", onSidebarClick, true);
|
|
5467
5608
|
observer.disconnect();
|
|
5468
5609
|
mounted?.dispose();
|
|
5469
5610
|
};
|
|
@@ -5537,7 +5678,7 @@ async function apply(ctx) {
|
|
|
5537
5678
|
function Overlay() {
|
|
5538
5679
|
const page = (0, import_react23.useSyncExternalStore)(business.subscribe, business.getSnapshot, business.getSnapshot);
|
|
5539
5680
|
return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
|
|
5540
|
-
!hostBusiness && page.activeId === ID && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(BusinessSurface, { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Page, { activeId: page.activeId }) }),
|
|
5681
|
+
!hostBusiness && page.activeId === ID && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(BusinessSurface, { onClose: () => business.close(), children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Page, { activeId: page.activeId }) }),
|
|
5541
5682
|
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PanelBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(XinHostNotice, { store: hostState }) }),
|
|
5542
5683
|
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PanelBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(LingtingSidebar, { service, controller, onOpenPage: () => business.open(ID) }) }),
|
|
5543
5684
|
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PanelBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(MeetingPrompt, { service }) })
|