dsh-vidi 0.7.0 → 0.7.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/lib/client.js +125 -28
- package/lib/index.mjs +164 -22
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -255,6 +255,23 @@ const Icon = {
|
|
|
255
255
|
strokeWidth: 2.6,
|
|
256
256
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5 12.5l4.5 4.5L19 7.5" })
|
|
257
257
|
}),
|
|
258
|
+
sidePanel: () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
259
|
+
viewBox: "0 0 24 24",
|
|
260
|
+
width: "18",
|
|
261
|
+
height: "18",
|
|
262
|
+
fill: "none",
|
|
263
|
+
stroke: "currentColor",
|
|
264
|
+
strokeWidth: "1.8",
|
|
265
|
+
strokeLinecap: "round",
|
|
266
|
+
strokeLinejoin: "round",
|
|
267
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
268
|
+
x: "3",
|
|
269
|
+
y: "4",
|
|
270
|
+
width: "18",
|
|
271
|
+
height: "16",
|
|
272
|
+
rx: "2.5"
|
|
273
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M15 4v16" })]
|
|
274
|
+
}),
|
|
258
275
|
plus: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
259
276
|
viewBox: "0 0 24 24",
|
|
260
277
|
width: "14",
|
|
@@ -844,7 +861,9 @@ function Pick(props) {
|
|
|
844
861
|
if (!list.current?.contains(e.target) && !btn.current?.contains(e.target)) close(false);
|
|
845
862
|
};
|
|
846
863
|
const gone = (e) => {
|
|
847
|
-
|
|
864
|
+
const t = e.target;
|
|
865
|
+
if (list.current?.contains(t)) return;
|
|
866
|
+
if (e.type === "resize" || t === document || t === document.documentElement || t instanceof Node && btn.current && t.contains(btn.current)) close(false);
|
|
848
867
|
};
|
|
849
868
|
document.addEventListener("pointerdown", away, true);
|
|
850
869
|
window.addEventListener("resize", gone);
|
|
@@ -34066,10 +34085,21 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
|
|
|
34066
34085
|
]
|
|
34067
34086
|
}),
|
|
34068
34087
|
openMenu && (() => {
|
|
34088
|
+
const phone = narrow && window.innerWidth < 700 && !full && !pageFull;
|
|
34089
|
+
const floating = narrow && !phone && !full && !pageFull;
|
|
34090
|
+
const r = floating ? stage.current?.getBoundingClientRect() : void 0;
|
|
34091
|
+
const place = r ? {
|
|
34092
|
+
position: "fixed",
|
|
34093
|
+
right: Math.max(8, window.innerWidth - r.right + 8),
|
|
34094
|
+
bottom: Math.max(8, window.innerHeight - r.bottom + 56),
|
|
34095
|
+
width: Math.min(360, window.innerWidth - 16),
|
|
34096
|
+
maxHeight: Math.max(240, r.bottom - 56 - 12)
|
|
34097
|
+
} : void 0;
|
|
34069
34098
|
const panel = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
34070
|
-
className: `dshl-menu ${
|
|
34099
|
+
className: `dshl-menu ${phone ? "sheet" : ""}`,
|
|
34071
34100
|
role: "dialog",
|
|
34072
34101
|
"aria-label": openMenu.title,
|
|
34102
|
+
style: place,
|
|
34073
34103
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
34074
34104
|
className: "head",
|
|
34075
34105
|
children: [
|
|
@@ -34094,13 +34124,13 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
|
|
|
34094
34124
|
children: openMenu.content
|
|
34095
34125
|
})]
|
|
34096
34126
|
});
|
|
34097
|
-
return
|
|
34127
|
+
return phone ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
34098
34128
|
className: "dshl-sheetwrap",
|
|
34099
34129
|
onMouseDown: (e) => {
|
|
34100
34130
|
if (e.target === e.currentTarget) setMenu(null);
|
|
34101
34131
|
},
|
|
34102
34132
|
children: panel
|
|
34103
|
-
}), document.body) : panel;
|
|
34133
|
+
}), document.body) : floating ? (0, react_dom.createPortal)(panel, document.body) : panel;
|
|
34104
34134
|
})(),
|
|
34105
34135
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
34106
34136
|
className: "dshl-controls",
|
|
@@ -34628,38 +34658,49 @@ function RestreamPanel(props) {
|
|
|
34628
34658
|
]
|
|
34629
34659
|
});
|
|
34630
34660
|
}
|
|
34631
|
-
/**
|
|
34661
|
+
/** Subtitle pages: the overlay for OBS's Browser Source, and the big screen of subtitles only. */
|
|
34632
34662
|
function OverlayLink({ t }) {
|
|
34633
|
-
const [
|
|
34634
|
-
const [copied, setCopied] = (0, react.useState)(
|
|
34663
|
+
const [urls, setUrls] = (0, react.useState)({});
|
|
34664
|
+
const [copied, setCopied] = (0, react.useState)("");
|
|
34635
34665
|
(0, react.useEffect)(() => {
|
|
34636
|
-
fetch("/live/api/overlay").then((r) => r.json()).then(
|
|
34666
|
+
fetch("/live/api/overlay").then((r) => r.json()).then(setUrls).catch(() => {});
|
|
34637
34667
|
}, []);
|
|
34638
|
-
if (!url) return null;
|
|
34639
|
-
|
|
34668
|
+
if (!urls.url) return null;
|
|
34669
|
+
const row = (label, url, note, open) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
34640
34670
|
className: "rs-overlay",
|
|
34641
34671
|
children: [
|
|
34642
34672
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
34643
34673
|
className: "rs-sub",
|
|
34644
|
-
children:
|
|
34674
|
+
children: label
|
|
34645
34675
|
}),
|
|
34646
34676
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
34647
34677
|
className: "rs-url",
|
|
34648
|
-
children: [
|
|
34649
|
-
|
|
34650
|
-
|
|
34651
|
-
|
|
34652
|
-
|
|
34678
|
+
children: [
|
|
34679
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: url }),
|
|
34680
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
34681
|
+
className: "dshl-link",
|
|
34682
|
+
onClick: () => void navigator.clipboard.writeText(url).then(() => {
|
|
34683
|
+
setCopied(url);
|
|
34684
|
+
setTimeout(() => setCopied(""), 1500);
|
|
34685
|
+
}),
|
|
34686
|
+
children: copied === url ? t("tr.copied") : t("rs.copy")
|
|
34653
34687
|
}),
|
|
34654
|
-
|
|
34655
|
-
|
|
34688
|
+
open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
34689
|
+
className: "dshl-link",
|
|
34690
|
+
href: url,
|
|
34691
|
+
target: "_blank",
|
|
34692
|
+
rel: "noreferrer",
|
|
34693
|
+
children: t("rs.openPage")
|
|
34694
|
+
})
|
|
34695
|
+
]
|
|
34656
34696
|
}),
|
|
34657
34697
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
34658
34698
|
className: "rs-hint",
|
|
34659
|
-
children:
|
|
34699
|
+
children: note
|
|
34660
34700
|
})
|
|
34661
34701
|
]
|
|
34662
34702
|
});
|
|
34703
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [row(t("rs.overlay"), urls.url, t("rs.overlay.d")), urls.screen && row(t("rs.screen"), urls.screenLan ?? urls.screen, t("rs.screen.d").replace("{local}", urls.screen), true)] });
|
|
34663
34704
|
}
|
|
34664
34705
|
|
|
34665
34706
|
// src/client/Settings.tsx
|
|
@@ -35217,6 +35258,29 @@ function LiveView(props) {
|
|
|
35217
35258
|
const [subStyle, setSubStyle] = (0, react.useState)(() => loadStyle(lines));
|
|
35218
35259
|
const [delay, setDelay] = (0, react.useState)(8);
|
|
35219
35260
|
const [theater, setTheater] = (0, react.useState)(false);
|
|
35261
|
+
const [sideOpen, setSideOpenRaw] = (0, react.useState)(() => store.get("sideOpen", true));
|
|
35262
|
+
const setSideOpen = (v) => {
|
|
35263
|
+
setSideOpenRaw(v);
|
|
35264
|
+
store.set("sideOpen", v);
|
|
35265
|
+
};
|
|
35266
|
+
const [sideW, setSideWRaw] = (0, react.useState)(() => store.get("sideW", 380));
|
|
35267
|
+
const mainRef = (0, react.useRef)(null);
|
|
35268
|
+
const dragSide = (e) => {
|
|
35269
|
+
e.preventDefault();
|
|
35270
|
+
const box = mainRef.current?.getBoundingClientRect();
|
|
35271
|
+
if (!box) return;
|
|
35272
|
+
const move = (ev) => setSideWRaw(Math.round(Math.min(box.width * .6, Math.max(280, box.right - ev.clientX - 8))));
|
|
35273
|
+
const up = () => {
|
|
35274
|
+
window.removeEventListener("pointermove", move);
|
|
35275
|
+
window.removeEventListener("pointerup", up);
|
|
35276
|
+
setSideWRaw((w) => {
|
|
35277
|
+
store.set("sideW", w);
|
|
35278
|
+
return w;
|
|
35279
|
+
});
|
|
35280
|
+
};
|
|
35281
|
+
window.addEventListener("pointermove", move);
|
|
35282
|
+
window.addEventListener("pointerup", up);
|
|
35283
|
+
};
|
|
35220
35284
|
const [tab, setTab] = (0, react.useState)("transcript");
|
|
35221
35285
|
const [query, setQuery] = (0, react.useState)("");
|
|
35222
35286
|
const [trMode, setTrMode] = (0, react.useState)("lines");
|
|
@@ -35929,7 +35993,8 @@ function LiveView(props) {
|
|
|
35929
35993
|
})]
|
|
35930
35994
|
}) : null;
|
|
35931
35995
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35932
|
-
className: `dshl ${theater && !activeBench ? "theater" : ""} ${pageMode ? "page" : ""} ${activeBench ? "bench" : ""}`,
|
|
35996
|
+
className: `dshl ${theater && !activeBench ? "theater" : ""} ${pageMode ? "page" : ""} ${activeBench ? "bench" : ""} ${pageMode && !sideOpen && !activeBench ? "noside" : ""}`,
|
|
35997
|
+
style: pageMode ? { ["--side-w"]: `${sideW}px` } : void 0,
|
|
35933
35998
|
"data-dsh-own-drop": "dsh-vidi",
|
|
35934
35999
|
onDragEnter: (e) => {
|
|
35935
36000
|
if (e.dataTransfer.types.includes("Files")) e.stopPropagation();
|
|
@@ -36031,6 +36096,13 @@ function LiveView(props) {
|
|
|
36031
36096
|
open: settingsOpen,
|
|
36032
36097
|
onOpen: setSettingsOpen,
|
|
36033
36098
|
t
|
|
36099
|
+
}),
|
|
36100
|
+
pageMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
36101
|
+
className: `dshl-ctl icon ${sideOpen ? "on" : ""}`,
|
|
36102
|
+
title: t(sideOpen ? "side.hide" : "side.show"),
|
|
36103
|
+
"aria-pressed": sideOpen,
|
|
36104
|
+
onClick: () => setSideOpen(!sideOpen),
|
|
36105
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.sidePanel, {})
|
|
36034
36106
|
})
|
|
36035
36107
|
]
|
|
36036
36108
|
})
|
|
@@ -36060,6 +36132,7 @@ function LiveView(props) {
|
|
|
36060
36132
|
children: emptyBox
|
|
36061
36133
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
|
|
36062
36134
|
className: "dshl-main",
|
|
36135
|
+
ref: mainRef,
|
|
36063
36136
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36064
36137
|
className: "dshl-col",
|
|
36065
36138
|
children: !hasMain || !job ? emptyBox : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
@@ -36211,9 +36284,13 @@ function LiveView(props) {
|
|
|
36211
36284
|
children: transcriptBox
|
|
36212
36285
|
})
|
|
36213
36286
|
] })
|
|
36214
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
36287
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
|
|
36215
36288
|
className: "dshl-side",
|
|
36216
|
-
children:
|
|
36289
|
+
children: [pageMode && !activeBench && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36290
|
+
className: "dshl-resize",
|
|
36291
|
+
onPointerDown: dragSide,
|
|
36292
|
+
title: t("side.resize")
|
|
36293
|
+
}), activeBench && view ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36217
36294
|
className: "dshl-benchbox",
|
|
36218
36295
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Guard, {
|
|
36219
36296
|
name: `workbench ${activeBench.id}`,
|
|
@@ -36230,7 +36307,7 @@ function LiveView(props) {
|
|
|
36230
36307
|
hasVideo: !!job,
|
|
36231
36308
|
jobsDir,
|
|
36232
36309
|
t
|
|
36233
|
-
}) : transcriptBox
|
|
36310
|
+
}) : transcriptBox]
|
|
36234
36311
|
})]
|
|
36235
36312
|
}),
|
|
36236
36313
|
dragging && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -36299,7 +36376,7 @@ function SubsStarter(props) {
|
|
|
36299
36376
|
const box = (0, react.useRef)(null);
|
|
36300
36377
|
const pop = (0, react.useRef)(null);
|
|
36301
36378
|
const toggle = () => {
|
|
36302
|
-
setSheet(
|
|
36379
|
+
setSheet(window.innerWidth < 700 && !document.fullscreenElement);
|
|
36303
36380
|
setOpen((v) => !v);
|
|
36304
36381
|
};
|
|
36305
36382
|
(0, react.useEffect)(() => {
|
|
@@ -36801,6 +36878,12 @@ const en = {
|
|
|
36801
36878
|
"rs.overlay": "Subtitles for OBS (Browser Source)",
|
|
36802
36879
|
"rs.overlay.d": "Add a Browser Source in OBS with this address (1920×1080): the newest subtitles on a transparent background, over your camera or any picture. Add ?lines=zh,src&size=4.5 to choose lines and size.",
|
|
36803
36880
|
"rs.copy": "Copy",
|
|
36881
|
+
"side.hide": "Hide the chat",
|
|
36882
|
+
"side.show": "Show the chat",
|
|
36883
|
+
"side.resize": "Drag to change the width",
|
|
36884
|
+
"rs.openPage": "Open",
|
|
36885
|
+
"rs.screen": "Subtitle screen (16:9)",
|
|
36886
|
+
"rs.screen.d": "Only the subtitles, large, for a big screen or projector: open it in any browser on this network and go full screen (on this computer: {local}). Add ?theme=light for a light screen; it can also be an OBS source of its own.",
|
|
36804
36887
|
"rs.content": "Sends",
|
|
36805
36888
|
"rs.c.burn": "Picture + subtitles",
|
|
36806
36889
|
"rs.c.clean": "Picture only",
|
|
@@ -37179,6 +37262,12 @@ const zh = {
|
|
|
37179
37262
|
"rs.overlay": "OBS 字幕叠加(浏览器源)",
|
|
37180
37263
|
"rs.overlay.d": "在 OBS 里加一个「浏览器源」,填这个地址(1920×1080):透明背景上只有最新的字幕,叠在摄像头或任何画面上。地址后加 ?lines=zh,src&size=4.5 可以选行和字号。",
|
|
37181
37264
|
"rs.copy": "复制",
|
|
37265
|
+
"side.hide": "收起对话",
|
|
37266
|
+
"side.show": "展开对话",
|
|
37267
|
+
"side.resize": "拖动调整宽度",
|
|
37268
|
+
"rs.openPage": "打开",
|
|
37269
|
+
"rs.screen": "字幕大屏(16:9)",
|
|
37270
|
+
"rs.screen.d": "只有字幕、字很大,适合大屏和投影:在同一网络里任何浏览器打开、全屏即可(本机用 {local})。地址后加 ?theme=light 换浅色;也可以作为 OBS 的一个独立画面来源。",
|
|
37182
37271
|
"rs.content": "内容",
|
|
37183
37272
|
"rs.c.burn": "画面 + 字幕",
|
|
37184
37273
|
"rs.c.clean": "只要画面",
|
|
@@ -37708,12 +37797,18 @@ html[data-platform='darwin'] .dshl-panelwrap .dshl.page .dshl-top { padding-top:
|
|
|
37708
37797
|
html[data-platform='darwin'] .dshl.page .dshl-top { -webkit-app-region: drag; app-region: drag; }
|
|
37709
37798
|
html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .dshl-select, .dshl-pick) { -webkit-app-region: no-drag; app-region: no-drag; }
|
|
37710
37799
|
.dshl.page { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
|
37711
|
-
.dshl.page .dshl-main { flex: 1; min-height: 0; grid-template-columns: minmax(0, 1fr) clamp(320px, 30vw, 420px); align-items: stretch; padding-bottom: 16px; }
|
|
37800
|
+
.dshl.page .dshl-main { flex: 1; min-height: 0; grid-template-columns: minmax(0, 1fr) var(--side-w, clamp(320px, 30vw, 420px)); align-items: stretch; padding-bottom: 16px; }
|
|
37712
37801
|
/* Video page: player on top, then the status row, then subtitles/glossary filling what is left; chat on the right. */
|
|
37713
37802
|
.dshl.page .dshl-col { display: flex; flex-direction: column; min-height: 0; container-type: size; }
|
|
37714
37803
|
.dshl.page .dshl-col > .dshl-drop { flex: 1; aspect-ratio: auto; min-height: 0; }
|
|
37715
37804
|
.dshl.page .dshl-stage { flex: none; width: 100%; max-height: max(240px, calc(100cqh - 270px)); }
|
|
37716
|
-
.dshl.page .dshl-side { position:
|
|
37805
|
+
.dshl.page .dshl-side { position: relative; top: auto; height: 100%; max-height: none; }
|
|
37806
|
+
.dshl.page.noside .dshl-main { grid-template-columns: minmax(0, 1fr); }
|
|
37807
|
+
.dshl.page.noside .dshl-side { display: none; }
|
|
37808
|
+
/* Drag the chat panel's left edge to change its width; the player and subtitles take the rest. */
|
|
37809
|
+
.dshl-resize { position: absolute; top: 0; bottom: 0; left: 0; z-index: 3; width: 8px; cursor: col-resize; }
|
|
37810
|
+
.dshl-resize::after { content: ''; position: absolute; top: 50%; left: 2px; width: 3px; height: 36px; border-radius: 2px; background: var(--line); transform: translateY(-50%); opacity: 0; transition: opacity .15s; }
|
|
37811
|
+
.dshl-resize:hover::after { opacity: 1; }
|
|
37717
37812
|
/* Workbench open: the video and its subtitles keep a column on the left; the bench takes the rest. */
|
|
37718
37813
|
.dshl.page.bench .dshl-main { grid-template-columns: clamp(340px, 32vw, 520px) minmax(0, 1fr); }
|
|
37719
37814
|
.dshl-lag { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; margin: -2px 0 8px; padding: 7px 12px; border-radius: 8px; background: var(--l1, rgba(0,0,0,.04)); color: var(--muted); font-size: 12.5px; }
|
|
@@ -37734,6 +37829,7 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
|
|
|
37734
37829
|
.dshl.page.theater .dshl-stage { max-height: 100cqh; }
|
|
37735
37830
|
@media (max-width: 1000px) {
|
|
37736
37831
|
.dshl.page { display: block; overflow: auto; }
|
|
37832
|
+
.dshl-resize { display: none; }
|
|
37737
37833
|
.dshl.page.bench .dshl-main { grid-template-columns: minmax(0, 1fr); }
|
|
37738
37834
|
.dshl.page.bench .dshl-side { height: auto; min-height: 0; }
|
|
37739
37835
|
.dshl-benchbox { overflow: visible; }
|
|
@@ -37966,8 +38062,9 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
|
|
|
37966
38062
|
.dshl-dl .dshl-chips .chip { background: transparent; }
|
|
37967
38063
|
|
|
37968
38064
|
/* Menus as a bottom sheet on small players (phones) */
|
|
37969
|
-
.dshl-sheetwrap { position: fixed; inset: 0; z-index: 2147483002; display: flex; align-items: flex-end; background: rgba(0,0,0,.4); }
|
|
37970
|
-
.dshl-menu
|
|
38065
|
+
.dshl-sheetwrap { position: fixed; inset: 0; z-index: 2147483002; display: flex; align-items: flex-end; justify-content: center; background: rgba(0,0,0,.4); }
|
|
38066
|
+
body > .dshl-menu { z-index: 2147483002; }
|
|
38067
|
+
.dshl-menu.sheet { position: relative; right: auto; bottom: auto; width: 100%; max-width: 560px; max-height: 78vh; padding-bottom: env(safe-area-inset-bottom); border: 0; border-radius: 16px 16px 0 0;
|
|
37971
38068
|
font-family: system-ui, -apple-system, "PingFang SC", sans-serif; font-size: 15px; }
|
|
37972
38069
|
.dshl-menu.sheet .head { padding: 14px 12px 8px 18px; font-size: 16px; }
|
|
37973
38070
|
.dshl-menu.sheet .body { padding: 4px 14px 16px; }
|
package/lib/index.mjs
CHANGED
|
@@ -4725,6 +4725,8 @@ var Stream = class {
|
|
|
4725
4725
|
cjk;
|
|
4726
4726
|
open = [];
|
|
4727
4727
|
done = [];
|
|
4728
|
+
/** When the last piece arrived (wall clock, ms). */
|
|
4729
|
+
lastWall = 0;
|
|
4728
4730
|
constructor(cjk) {
|
|
4729
4731
|
this.cjk = cjk;
|
|
4730
4732
|
}
|
|
@@ -4754,18 +4756,51 @@ var Stream = class {
|
|
|
4754
4756
|
this.push(p);
|
|
4755
4757
|
}
|
|
4756
4758
|
push(p) {
|
|
4759
|
+
this.lastWall = Date.now();
|
|
4757
4760
|
this.open.push(p);
|
|
4758
4761
|
const text = this.text.trim();
|
|
4759
4762
|
const len = this.cjk ? Array.from(text).length : text.length;
|
|
4760
4763
|
const soft = this.cjk ? SOFT_CJK : SOFT_LATIN, hard = this.cjk ? HARD_CJK : HARD_LATIN;
|
|
4761
4764
|
if (END.test(text) || len >= soft && /[,,、;;::]\s*$/.test(text) || len >= hard) this.close();
|
|
4762
4765
|
}
|
|
4766
|
+
/** End the sentence at its last comma or full stop (the rest starts the next one); false when it has none. */
|
|
4767
|
+
closeAtPause() {
|
|
4768
|
+
const text = this.text;
|
|
4769
|
+
let cut = -1;
|
|
4770
|
+
for (let i = text.length - 2; i > 0; i--) if (/[,。、;:!?,;:!?]/.test(text[i])) {
|
|
4771
|
+
cut = i + 1;
|
|
4772
|
+
break;
|
|
4773
|
+
}
|
|
4774
|
+
if (cut <= 0) return false;
|
|
4775
|
+
const head = [], tail = [];
|
|
4776
|
+
let n = 0;
|
|
4777
|
+
for (const p of this.open) {
|
|
4778
|
+
if (n + p.text.length <= cut) head.push(p);
|
|
4779
|
+
else if (n >= cut) tail.push(p);
|
|
4780
|
+
else {
|
|
4781
|
+
head.push({
|
|
4782
|
+
t: p.t,
|
|
4783
|
+
text: p.text.slice(0, cut - n)
|
|
4784
|
+
});
|
|
4785
|
+
tail.push({
|
|
4786
|
+
t: p.t,
|
|
4787
|
+
text: p.text.slice(cut - n)
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
n += p.text.length;
|
|
4791
|
+
}
|
|
4792
|
+
this.open = head;
|
|
4793
|
+
this.close();
|
|
4794
|
+
this.open = tail;
|
|
4795
|
+
return true;
|
|
4796
|
+
}
|
|
4763
4797
|
close() {
|
|
4764
4798
|
const text = this.text.trim();
|
|
4765
4799
|
if (text) this.done.push({
|
|
4766
4800
|
text,
|
|
4767
4801
|
start: this.open[0].t,
|
|
4768
|
-
end: this.open.at(-1).t
|
|
4802
|
+
end: this.open.at(-1).t,
|
|
4803
|
+
wall: Date.now()
|
|
4769
4804
|
});
|
|
4770
4805
|
this.open = [];
|
|
4771
4806
|
}
|
|
@@ -4791,6 +4826,11 @@ var OpenAITranslateProvider = class {
|
|
|
4791
4826
|
reconnects = 0;
|
|
4792
4827
|
/** Audio sent so far (s); a session's positions count from `base`. */
|
|
4793
4828
|
sent = 0;
|
|
4829
|
+
/** Audio waiting to be sent: the service listens in real time, so bursts (a recording read from its
|
|
4830
|
+
* start) go up at most a little faster than that. */
|
|
4831
|
+
queue = [];
|
|
4832
|
+
queued = 0;
|
|
4833
|
+
drain;
|
|
4794
4834
|
base = 0;
|
|
4795
4835
|
src = new Stream(false);
|
|
4796
4836
|
tr;
|
|
@@ -4801,6 +4841,8 @@ var OpenAITranslateProvider = class {
|
|
|
4801
4841
|
seq = 0;
|
|
4802
4842
|
/** End of the last finished line (s): everything before it is on screen. */
|
|
4803
4843
|
readyAt = 0;
|
|
4844
|
+
/** Where the last line ends on screen (lines never overlap). */
|
|
4845
|
+
lastEnd = 0;
|
|
4804
4846
|
/** Arrival delay of text after its audio position, for the page (s). */
|
|
4805
4847
|
lag = 0;
|
|
4806
4848
|
connectedAt = 0;
|
|
@@ -4821,6 +4863,7 @@ var OpenAITranslateProvider = class {
|
|
|
4821
4863
|
this.onError = onError;
|
|
4822
4864
|
await this.connect();
|
|
4823
4865
|
this.quiet = setInterval(() => this.settle(), 400);
|
|
4866
|
+
this.drain = setInterval(() => this.sendDue(), 100);
|
|
4824
4867
|
}
|
|
4825
4868
|
connect() {
|
|
4826
4869
|
return new Promise((resolve, reject) => {
|
|
@@ -4874,35 +4917,52 @@ var OpenAITranslateProvider = class {
|
|
|
4874
4917
|
}
|
|
4875
4918
|
push(pcm) {
|
|
4876
4919
|
if (this.stopped) return;
|
|
4877
|
-
this.
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4920
|
+
this.queue.push(Buffer.from(pcm));
|
|
4921
|
+
this.queued += pcm.length / BYTES_PER_SEC;
|
|
4922
|
+
this.sendDue();
|
|
4923
|
+
}
|
|
4924
|
+
/** Send queued audio, everything including silence, no faster than 1.5× real time since the session began. */
|
|
4925
|
+
sendDue() {
|
|
4926
|
+
if (this.ws?.readyState !== WebSocket.OPEN) return;
|
|
4927
|
+
const allowed = this.base + (Date.now() - this.connectedAt) / 1e3 * 1.5 + 2;
|
|
4928
|
+
while (this.queue.length && this.sent < allowed) {
|
|
4929
|
+
const pcm = this.queue.shift();
|
|
4930
|
+
this.queued -= pcm.length / BYTES_PER_SEC;
|
|
4931
|
+
this.send({
|
|
4932
|
+
type: "session.input_audio_buffer.append",
|
|
4933
|
+
audio: this.up.push(pcm).toString("base64")
|
|
4934
|
+
});
|
|
4935
|
+
this.sent += pcm.length / BYTES_PER_SEC;
|
|
4936
|
+
}
|
|
4882
4937
|
}
|
|
4883
|
-
/** Audio
|
|
4938
|
+
/** Audio received but not yet in a finished line (queued, or sent and still being worked on). */
|
|
4884
4939
|
backlog() {
|
|
4885
|
-
return Math.max(0, this.sent - this.readyAt);
|
|
4940
|
+
return this.queued + Math.max(0, this.sent - this.readyAt);
|
|
4886
4941
|
}
|
|
4887
4942
|
onEvent(ev) {
|
|
4888
4943
|
if (ev.type === "session.input_transcript.delta" || ev.type === "session.output_transcript.delta") {
|
|
4889
4944
|
const t = this.base + (ev.elapsed_ms ?? 0) / 1e3;
|
|
4890
|
-
const
|
|
4891
|
-
this.lag = this.lag ? this.lag * .9 +
|
|
4945
|
+
const behind = Math.max(0, this.sent - t);
|
|
4946
|
+
this.lag = this.lag ? this.lag * .9 + behind * .1 : behind;
|
|
4892
4947
|
const piece = {
|
|
4893
4948
|
t,
|
|
4894
4949
|
text: String(ev.delta ?? "")
|
|
4895
4950
|
};
|
|
4896
4951
|
if (ev.type === "session.input_transcript.delta") this.src.add(piece);
|
|
4897
|
-
else
|
|
4952
|
+
else {
|
|
4953
|
+
const next = this.src.done[this.srcUsed];
|
|
4954
|
+
if (next && this.tr.text.trim() && t - next.end >= 1.2) {
|
|
4955
|
+
if (!this.tr.closeAtPause() && t - next.end >= 3) this.tr.close();
|
|
4956
|
+
}
|
|
4957
|
+
this.tr.add(piece);
|
|
4958
|
+
}
|
|
4898
4959
|
this.pair(false);
|
|
4899
4960
|
this.draft();
|
|
4900
4961
|
} else if (ev.type === "error") this.onError?.(/* @__PURE__ */ new Error(`OpenAI: ${ev.error?.message ?? "error"}`));
|
|
4901
4962
|
}
|
|
4902
4963
|
/** Close streams that went quiet (a pause without punctuation), then pair what is complete. */
|
|
4903
4964
|
settle() {
|
|
4904
|
-
const
|
|
4905
|
-
for (const s of [this.src, this.tr]) if (s.last !== void 0 && now - s.last > QUIET_SEC + this.lag) s.close();
|
|
4965
|
+
for (const s of [this.src, this.tr]) if (s.last !== void 0 && Date.now() - s.lastWall > QUIET_SEC * 1e3) s.close();
|
|
4906
4966
|
this.pair(false);
|
|
4907
4967
|
this.draft();
|
|
4908
4968
|
}
|
|
@@ -4913,7 +4973,10 @@ var OpenAITranslateProvider = class {
|
|
|
4913
4973
|
*/
|
|
4914
4974
|
pair(all) {
|
|
4915
4975
|
while (this.tr.done.length) {
|
|
4916
|
-
const t = this.tr.done
|
|
4976
|
+
const t = this.tr.done[0];
|
|
4977
|
+
const srcReady = this.srcUsed < this.src.done.length && this.src.done[this.srcUsed].end <= t.end + .5;
|
|
4978
|
+
if (!all && !srcReady && this.src.text.trim() && Date.now() - t.wall < 1500) break;
|
|
4979
|
+
this.tr.done.shift();
|
|
4917
4980
|
const want = Array.from(t.text).length * this.ratio;
|
|
4918
4981
|
const src = [];
|
|
4919
4982
|
let len = 0;
|
|
@@ -4936,28 +4999,37 @@ var OpenAITranslateProvider = class {
|
|
|
4936
4999
|
}
|
|
4937
5000
|
line(t, src) {
|
|
4938
5001
|
const srcText = src.map((s) => s.text).join(" ").trim();
|
|
4939
|
-
const
|
|
4940
|
-
|
|
5002
|
+
const spoken = src.length ? {
|
|
5003
|
+
start: src[0].start - .6,
|
|
5004
|
+
end: src.at(-1).end - .2
|
|
5005
|
+
} : {
|
|
5006
|
+
start: (t?.start ?? 0) - 1.4,
|
|
5007
|
+
end: (t?.end ?? 0) - .8
|
|
5008
|
+
};
|
|
5009
|
+
const start = Math.max(0, this.lastEnd, spoken.start);
|
|
5010
|
+
const end = Math.max(start + 1, spoken.end);
|
|
5011
|
+
this.lastEnd = end;
|
|
4941
5012
|
this.emit?.({
|
|
4942
5013
|
key: `ot:${this.seq++}`,
|
|
4943
5014
|
start,
|
|
4944
|
-
end
|
|
5015
|
+
end,
|
|
4945
5016
|
src: srcText,
|
|
4946
5017
|
tr: t ? { [this.opts.target]: t.text } : void 0,
|
|
4947
5018
|
final: true
|
|
4948
5019
|
});
|
|
4949
|
-
this.readyAt = Math.max(this.readyAt, end);
|
|
5020
|
+
this.readyAt = Math.max(this.readyAt, t?.end ?? end, end);
|
|
4950
5021
|
}
|
|
4951
5022
|
/** The sentence being translated, as a draft line. */
|
|
4952
5023
|
draft() {
|
|
4953
5024
|
const tr = this.tr.text.trim();
|
|
4954
5025
|
if (!tr) return;
|
|
4955
5026
|
const srcText = [...this.src.done.slice(this.srcUsed).map((s) => s.text), this.src.text.trim()].filter(Boolean).join(" ");
|
|
4956
|
-
const
|
|
5027
|
+
const from = this.src.done[this.srcUsed]?.start ?? this.src.start;
|
|
5028
|
+
const start = Math.max(0, this.lastEnd, from !== void 0 ? from - .6 : (this.tr.start ?? 0) - 1.4);
|
|
4957
5029
|
this.emit?.({
|
|
4958
5030
|
key: `ot:${this.seq}`,
|
|
4959
5031
|
start,
|
|
4960
|
-
end: Math.max(this.tr.last ?? start
|
|
5032
|
+
end: Math.max(start + 1, (this.src.last ?? this.tr.last ?? start) - .2),
|
|
4961
5033
|
src: srcText,
|
|
4962
5034
|
tr: { [this.opts.target]: tr },
|
|
4963
5035
|
final: false
|
|
@@ -4970,6 +5042,7 @@ var OpenAITranslateProvider = class {
|
|
|
4970
5042
|
async stop() {
|
|
4971
5043
|
await new Promise((r) => setTimeout(r, 2500));
|
|
4972
5044
|
if (this.quiet) clearInterval(this.quiet);
|
|
5045
|
+
if (this.drain) clearInterval(this.drain);
|
|
4973
5046
|
this.src.close();
|
|
4974
5047
|
this.tr.close();
|
|
4975
5048
|
this.pair(true);
|
|
@@ -5046,11 +5119,19 @@ function startOverlay(port, current, onJob, log) {
|
|
|
5046
5119
|
res.end(PAGE);
|
|
5047
5120
|
return;
|
|
5048
5121
|
}
|
|
5122
|
+
if (url.pathname === "/screen" || url.pathname === "/screen/") {
|
|
5123
|
+
res.writeHead(200, {
|
|
5124
|
+
"content-type": "text/html; charset=utf-8",
|
|
5125
|
+
"cache-control": "no-store"
|
|
5126
|
+
});
|
|
5127
|
+
res.end(SCREEN);
|
|
5128
|
+
return;
|
|
5129
|
+
}
|
|
5049
5130
|
res.writeHead(404);
|
|
5050
5131
|
res.end();
|
|
5051
5132
|
});
|
|
5052
5133
|
server.on("error", (err) => log(`overlay: port ${port} unavailable (${err.message})`));
|
|
5053
|
-
server.listen(port,
|
|
5134
|
+
server.listen(port, () => log(`overlay: http://127.0.0.1:${port}/overlay, /screen`));
|
|
5054
5135
|
return () => {
|
|
5055
5136
|
offJob();
|
|
5056
5137
|
detach?.();
|
|
@@ -5098,6 +5179,63 @@ function connect() {
|
|
|
5098
5179
|
}
|
|
5099
5180
|
connect()
|
|
5100
5181
|
<\/script></body></html>`;
|
|
5182
|
+
/**
|
|
5183
|
+
* Subtitles as the whole picture, for a big screen (a hall, a projector, a
|
|
5184
|
+
* second monitor, or OBS as a source of its own): 16:9, the current line large
|
|
5185
|
+
* with the original under it, the two before it smaller and dimmed above.
|
|
5186
|
+
* …/screen?lines=zh,src&theme=dark|light
|
|
5187
|
+
*/
|
|
5188
|
+
const SCREEN = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vidi · subtitles</title>
|
|
5189
|
+
<style>
|
|
5190
|
+
html,body{margin:0;height:100%;background:#000;overflow:hidden;cursor:none}
|
|
5191
|
+
body.light{background:#f4f3ef}
|
|
5192
|
+
#stage{position:absolute;inset:0;margin:auto;width:min(100vw,177.78vh);height:min(56.25vw,100vh);display:flex;flex-direction:column;justify-content:flex-end;box-sizing:border-box;padding:4% 6% 7%;
|
|
5193
|
+
font-family:"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans CJK SC",system-ui,sans-serif;color:#fff;container-type:inline-size}
|
|
5194
|
+
body.light #stage{color:#151513}
|
|
5195
|
+
.cue{text-align:center;text-wrap:balance;line-break:strict;transition:opacity .4s,font-size .3s}
|
|
5196
|
+
.cue+.cue{margin-top:3.2cqw}
|
|
5197
|
+
.cue .main{display:block;font-weight:600;line-height:1.35}
|
|
5198
|
+
.cue .sub{display:block;margin-top:.8cqw;opacity:.62;line-height:1.4}
|
|
5199
|
+
.cue.now .main{font-size:5.4cqw}.cue.now .sub{font-size:2.9cqw}
|
|
5200
|
+
.cue.old .main{font-size:3cqw}.cue.old .sub{font-size:1.9cqw}
|
|
5201
|
+
.cue.old{opacity:.38}.cue.old2{opacity:.2}
|
|
5202
|
+
.cue.draft .main{opacity:.75}
|
|
5203
|
+
</style></head><body><div id="stage"></div>
|
|
5204
|
+
<script>
|
|
5205
|
+
const q = new URLSearchParams(location.search)
|
|
5206
|
+
const lines = (q.get('lines') || 'zh,src').split(',')
|
|
5207
|
+
if (q.get('theme') === 'light') document.body.classList.add('light')
|
|
5208
|
+
const stage = document.getElementById('stage')
|
|
5209
|
+
let done = [], draft = null
|
|
5210
|
+
function texts(c) {
|
|
5211
|
+
const seen = new Set()
|
|
5212
|
+
return lines.map(k => (k === 'src' ? c.src : (c.tr || {})[k]) || '').map(s => s.trim()).filter(s => s && !seen.has(s) && seen.add(s))
|
|
5213
|
+
}
|
|
5214
|
+
function render() {
|
|
5215
|
+
const list = [...done.slice(-3), ...(draft ? [draft] : [])].slice(-3)
|
|
5216
|
+
stage.innerHTML = ''
|
|
5217
|
+
list.forEach((c, i) => {
|
|
5218
|
+
const t = texts(c); if (!t.length) return
|
|
5219
|
+
const d = document.createElement('div')
|
|
5220
|
+
const age = list.length - 1 - i
|
|
5221
|
+
d.className = 'cue ' + (age === 0 ? 'now' : age === 1 ? 'old' : 'old old2') + (c.final ? '' : ' draft')
|
|
5222
|
+
const m = document.createElement('span'); m.className = 'main'; m.textContent = t[0]; d.appendChild(m)
|
|
5223
|
+
if (t[1]) { const s = document.createElement('span'); s.className = 'sub'; s.textContent = t[1]; d.appendChild(s) }
|
|
5224
|
+
stage.appendChild(d)
|
|
5225
|
+
})
|
|
5226
|
+
}
|
|
5227
|
+
function connect() {
|
|
5228
|
+
const es = new EventSource('/overlay/events')
|
|
5229
|
+
es.onmessage = e => {
|
|
5230
|
+
let c; try { c = JSON.parse(e.data) } catch { return }
|
|
5231
|
+
if (!c) return
|
|
5232
|
+
if (c.final) { draft = null; if (!done.length || done.at(-1).src !== c.src) done.push(c); done = done.slice(-6) } else draft = c
|
|
5233
|
+
render()
|
|
5234
|
+
}
|
|
5235
|
+
es.onerror = () => { es.close(); setTimeout(connect, 2000) }
|
|
5236
|
+
}
|
|
5237
|
+
connect()
|
|
5238
|
+
<\/script></body></html>`;
|
|
5101
5239
|
// src/tools.ts
|
|
5102
5240
|
const CLOCK = (s) => {
|
|
5103
5241
|
const h = Math.floor(s / 3600), m = Math.floor(s / 60) % 60, ss = Math.floor(s % 60);
|
|
@@ -5865,7 +6003,11 @@ function apply(ctx, config) {
|
|
|
5865
6003
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
5866
6004
|
const route = url.pathname.replace(/^\/live\/api\/?/, "");
|
|
5867
6005
|
try {
|
|
5868
|
-
if (route === "overlay") return send(res, 200, {
|
|
6006
|
+
if (route === "overlay") return send(res, 200, {
|
|
6007
|
+
url: `http://127.0.0.1:${config.overlayPort}/overlay`,
|
|
6008
|
+
screen: `http://127.0.0.1:${config.overlayPort}/screen`,
|
|
6009
|
+
screenLan: `http://${lanAddress()}:${config.overlayPort}/screen`
|
|
6010
|
+
});
|
|
5869
6011
|
if (route === "deps") {
|
|
5870
6012
|
const [deps] = await Promise.all([checkDeps(config.whisperModel), keys.refresh()]);
|
|
5871
6013
|
localReady = deps.filter((d) => [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vidi",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "A video tool for DeepSeek Harness: watch live streams, videos and podcasts with real-time subtitles in any language, then clip and download",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|