dsh-vidi 0.8.9 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +219 -50
- package/lib/index.mjs +182 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -358,6 +358,17 @@ const Icon = {
|
|
|
358
358
|
rx: "2"
|
|
359
359
|
})
|
|
360
360
|
}),
|
|
361
|
+
timer: () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
362
|
+
viewBox: "0 0 24 24",
|
|
363
|
+
width: "18",
|
|
364
|
+
height: "18",
|
|
365
|
+
...P,
|
|
366
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
367
|
+
cx: "12",
|
|
368
|
+
cy: "13.5",
|
|
369
|
+
r: "7.5"
|
|
370
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 9.5v4l2.5 1.5M10 3h4M12 3v3" })]
|
|
371
|
+
}),
|
|
361
372
|
live: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
362
373
|
viewBox: "0 0 24 24",
|
|
363
374
|
width: "16",
|
|
@@ -1062,7 +1073,7 @@ const BGS = {
|
|
|
1062
1073
|
* position and background.
|
|
1063
1074
|
*/
|
|
1064
1075
|
function SubtitleMenu(props) {
|
|
1065
|
-
const { lines, onLines, options, site, name, style, onStyle, t } = props;
|
|
1076
|
+
const { lines, onLines, options, site, name, style, onStyle, spoken, t } = props;
|
|
1066
1077
|
const main = lines[0] ?? "src";
|
|
1067
1078
|
const second = lines[1] ?? "";
|
|
1068
1079
|
const label = (k) => site.find((s) => s.key === k)?.label ?? name(k);
|
|
@@ -1127,6 +1138,21 @@ function SubtitleMenu(props) {
|
|
|
1127
1138
|
className: "swap",
|
|
1128
1139
|
onClick: () => onLines([second, main]),
|
|
1129
1140
|
children: ["⇅ ", t("sub.swapMain")]
|
|
1141
|
+
}),
|
|
1142
|
+
spoken && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1143
|
+
className: "slot",
|
|
1144
|
+
title: t("set.spoken.tip"),
|
|
1145
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("set.spoken") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
1146
|
+
value: spoken.value,
|
|
1147
|
+
onChange: spoken.onChange,
|
|
1148
|
+
options: [{
|
|
1149
|
+
value: "auto",
|
|
1150
|
+
label: t("set.auto")
|
|
1151
|
+
}, ...spoken.options.map(([k, v]) => ({
|
|
1152
|
+
value: k,
|
|
1153
|
+
label: v
|
|
1154
|
+
}))]
|
|
1155
|
+
})]
|
|
1130
1156
|
})
|
|
1131
1157
|
]
|
|
1132
1158
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -34897,7 +34923,7 @@ async function post$1(path, body) {
|
|
|
34897
34923
|
return j;
|
|
34898
34924
|
}
|
|
34899
34925
|
function Settings(props) {
|
|
34900
|
-
const { engine, onEngine,
|
|
34926
|
+
const { engine, onEngine, login, onLogin, browsers, providers, shared, name, onKey, disabled, open, onOpen, update, onCheckUpdate, t } = props;
|
|
34901
34927
|
const [checking, setChecking] = (0, react.useState)(false);
|
|
34902
34928
|
const [checked, setChecked] = (0, react.useState)(false);
|
|
34903
34929
|
const [installing, setInstalling] = (0, react.useState)(false);
|
|
@@ -35209,43 +35235,6 @@ function Settings(props) {
|
|
|
35209
35235
|
})
|
|
35210
35236
|
]
|
|
35211
35237
|
}),
|
|
35212
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35213
|
-
className: "srow col",
|
|
35214
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
35215
|
-
className: "pair",
|
|
35216
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("set.mode") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35217
|
-
className: "dshl-seg",
|
|
35218
|
-
children: [
|
|
35219
|
-
"live",
|
|
35220
|
-
"realtime",
|
|
35221
|
-
"polished"
|
|
35222
|
-
].map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35223
|
-
className: mode === m ? "on" : "",
|
|
35224
|
-
title: t(`mode.${m}.tip`),
|
|
35225
|
-
onClick: () => onMode(m),
|
|
35226
|
-
children: t(`mode.${m}`)
|
|
35227
|
-
}, m))
|
|
35228
|
-
})]
|
|
35229
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35230
|
-
className: "hint",
|
|
35231
|
-
children: t(`mode.${mode}.tip`)
|
|
35232
|
-
})]
|
|
35233
|
-
}),
|
|
35234
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35235
|
-
className: "srow",
|
|
35236
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("set.spoken") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
35237
|
-
value: spoken,
|
|
35238
|
-
disabled,
|
|
35239
|
-
onChange: onSpoken,
|
|
35240
|
-
options: [{
|
|
35241
|
-
value: "auto",
|
|
35242
|
-
label: t("set.auto")
|
|
35243
|
-
}, ...langOptions.map(([k, v]) => ({
|
|
35244
|
-
value: k,
|
|
35245
|
-
label: v
|
|
35246
|
-
}))]
|
|
35247
|
-
})]
|
|
35248
|
-
}),
|
|
35249
35238
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35250
35239
|
className: "srow",
|
|
35251
35240
|
title: t("set.login.tip"),
|
|
@@ -35387,6 +35376,92 @@ function useDevices(enabled) {
|
|
|
35387
35376
|
}, [enabled, devices]);
|
|
35388
35377
|
return devices;
|
|
35389
35378
|
}
|
|
35379
|
+
/** "3 分钟前", "5 小时前", "2 天前", then the date. */
|
|
35380
|
+
function ago(ts, ui) {
|
|
35381
|
+
if (!ts) return "";
|
|
35382
|
+
const s = Math.max(0, (Date.now() - ts) / 1e3);
|
|
35383
|
+
if (s < 60) return ui === "zh" ? "刚刚" : "just now";
|
|
35384
|
+
const [n, unit] = s < 3600 ? [Math.round(s / 60), ui === "zh" ? " 分钟前" : " min ago"] : s < 86400 ? [Math.round(s / 3600), ui === "zh" ? " 小时前" : " h ago"] : s < 604800 ? [Math.round(s / 86400), ui === "zh" ? " 天前" : " d ago"] : [0, ""];
|
|
35385
|
+
if (unit) return `${n}${unit}`;
|
|
35386
|
+
return new Date(ts).toLocaleDateString(ui === "zh" ? "zh-CN" : "en-US", {
|
|
35387
|
+
month: "short",
|
|
35388
|
+
day: "numeric"
|
|
35389
|
+
});
|
|
35390
|
+
}
|
|
35391
|
+
/** Past videos under the start box: the newest few, more on request. */
|
|
35392
|
+
function History({ t, ui, onOpen }) {
|
|
35393
|
+
const [items, setItems] = (0, react.useState)([]);
|
|
35394
|
+
const [total, setTotal] = (0, react.useState)(0);
|
|
35395
|
+
const load = (offset, limit) => fetch(`/live/api/history?offset=${offset}&limit=${limit}`).then((r) => r.json()).then((j) => {
|
|
35396
|
+
setItems((v) => offset ? [...v, ...j.items ?? []] : j.items ?? []);
|
|
35397
|
+
setTotal(j.total ?? 0);
|
|
35398
|
+
}).catch(() => {});
|
|
35399
|
+
(0, react.useEffect)(() => {
|
|
35400
|
+
load(0, 5);
|
|
35401
|
+
}, []);
|
|
35402
|
+
if (!items.length) return null;
|
|
35403
|
+
const from = (v) => v.source === "file" ? t("src.file") : v.source === "device" ? t("src.camera") : v.source === "push" ? t("src.push") : siteName(v.url ?? "") || (() => {
|
|
35404
|
+
try {
|
|
35405
|
+
return new URL(v.url ?? "").hostname.replace(/^www\./, "");
|
|
35406
|
+
} catch {
|
|
35407
|
+
return t("src.link");
|
|
35408
|
+
}
|
|
35409
|
+
})();
|
|
35410
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
35411
|
+
className: "dshl-history",
|
|
35412
|
+
children: [
|
|
35413
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35414
|
+
className: "hh",
|
|
35415
|
+
children: t("hist.title")
|
|
35416
|
+
}),
|
|
35417
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35418
|
+
className: "hl",
|
|
35419
|
+
children: items.map((v) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
35420
|
+
className: "hi",
|
|
35421
|
+
onClick: () => onOpen(v),
|
|
35422
|
+
children: [
|
|
35423
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
35424
|
+
className: "th",
|
|
35425
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
35426
|
+
src: `/live/api/thumb?jobId=${encodeURIComponent(v.id)}`,
|
|
35427
|
+
alt: "",
|
|
35428
|
+
onError: (e) => {
|
|
35429
|
+
e.currentTarget.style.visibility = "hidden";
|
|
35430
|
+
}
|
|
35431
|
+
}), v.duration ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35432
|
+
className: "dur",
|
|
35433
|
+
children: CLOCK(v.duration)
|
|
35434
|
+
}) : null]
|
|
35435
|
+
}),
|
|
35436
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
35437
|
+
className: "tx",
|
|
35438
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35439
|
+
className: "tt",
|
|
35440
|
+
children: v.title || (v.source === "push" ? from(v) : t("hist.untitled"))
|
|
35441
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35442
|
+
className: "mm",
|
|
35443
|
+
children: [
|
|
35444
|
+
v.title || v.source !== "push" ? from(v) : "",
|
|
35445
|
+
v.live ? t("hist.live") : "",
|
|
35446
|
+
ago(v.startedAt, ui)
|
|
35447
|
+
].filter(Boolean).join(" · ")
|
|
35448
|
+
})]
|
|
35449
|
+
}),
|
|
35450
|
+
v.running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35451
|
+
className: "chip",
|
|
35452
|
+
children: t("hist.running")
|
|
35453
|
+
})
|
|
35454
|
+
]
|
|
35455
|
+
}, v.id))
|
|
35456
|
+
}),
|
|
35457
|
+
items.length < total && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35458
|
+
className: "dshl-link more",
|
|
35459
|
+
onClick: () => void load(items.length, 10),
|
|
35460
|
+
children: t("hist.more").replace("{n}", String(total - items.length))
|
|
35461
|
+
})
|
|
35462
|
+
]
|
|
35463
|
+
});
|
|
35464
|
+
}
|
|
35390
35465
|
/** The push stream key: copy it, or change it (typed, or a new random one). */
|
|
35391
35466
|
function KeyRow({ value, t, onChanged }) {
|
|
35392
35467
|
const [edit, setEdit] = (0, react.useState)(null);
|
|
@@ -36134,6 +36209,22 @@ function LiveView(props) {
|
|
|
36134
36209
|
return "camera";
|
|
36135
36210
|
}
|
|
36136
36211
|
};
|
|
36212
|
+
const openPast = async (v) => {
|
|
36213
|
+
setError("");
|
|
36214
|
+
try {
|
|
36215
|
+
if (v.conversationId !== sessionId && props.onOpenVideo && await props.onOpenVideo(v.conversationId, v.id)) return;
|
|
36216
|
+
const j = await post("reopen", {
|
|
36217
|
+
jobId: v.id,
|
|
36218
|
+
conversationId: sessionId
|
|
36219
|
+
});
|
|
36220
|
+
setCues(j.cues ?? []);
|
|
36221
|
+
setTerms(j.terms ?? []);
|
|
36222
|
+
setJob(j);
|
|
36223
|
+
setArmed(null);
|
|
36224
|
+
} catch (e) {
|
|
36225
|
+
setError(e.message);
|
|
36226
|
+
}
|
|
36227
|
+
};
|
|
36137
36228
|
const emptyBox = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
36138
36229
|
className: "dshl-drop",
|
|
36139
36230
|
children: [
|
|
@@ -36143,6 +36234,11 @@ function LiveView(props) {
|
|
|
36143
36234
|
children: sourceBar
|
|
36144
36235
|
}),
|
|
36145
36236
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: srcTab === "camera" && devices && !mics.length ? t("src.cam.none") : t(srcTab === "file" ? "src.file.body" : srcTab === "camera" ? "src.cam.body" : srcTab === "push" ? "src.push.body" : "empty.body") }),
|
|
36237
|
+
pageMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(History, {
|
|
36238
|
+
t,
|
|
36239
|
+
ui,
|
|
36240
|
+
onOpen: (v) => void openPast(v)
|
|
36241
|
+
}),
|
|
36146
36242
|
srcTab === "push" && armed?.rtmp && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36147
36243
|
className: "dshl-pushinfo",
|
|
36148
36244
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyRow, {
|
|
@@ -36448,11 +36544,6 @@ function LiveView(props) {
|
|
|
36448
36544
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Settings, {
|
|
36449
36545
|
engine: engine || "local",
|
|
36450
36546
|
onEngine: setEngine,
|
|
36451
|
-
mode,
|
|
36452
|
-
onMode: changeMode,
|
|
36453
|
-
spoken,
|
|
36454
|
-
onSpoken: setSpoken,
|
|
36455
|
-
langOptions,
|
|
36456
36547
|
login,
|
|
36457
36548
|
onLogin: chooseLogin,
|
|
36458
36549
|
browsers,
|
|
@@ -36640,9 +36731,38 @@ function LiveView(props) {
|
|
|
36640
36731
|
name: nameWithSpoken,
|
|
36641
36732
|
style: subStyle,
|
|
36642
36733
|
onStyle: setSubStyle,
|
|
36734
|
+
spoken: {
|
|
36735
|
+
value: spoken,
|
|
36736
|
+
options: langOptions,
|
|
36737
|
+
onChange: setSpoken
|
|
36738
|
+
},
|
|
36643
36739
|
t
|
|
36644
36740
|
}) })
|
|
36645
36741
|
},
|
|
36742
|
+
...!job.vod && job.status === "running" ? [{
|
|
36743
|
+
id: "mode",
|
|
36744
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.timer, {}),
|
|
36745
|
+
title: t("set.mode"),
|
|
36746
|
+
label: t(`mode.${mode}`),
|
|
36747
|
+
content: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36748
|
+
className: "dshl-modemenu",
|
|
36749
|
+
children: [
|
|
36750
|
+
"live",
|
|
36751
|
+
"realtime",
|
|
36752
|
+
"polished"
|
|
36753
|
+
].map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
36754
|
+
className: `mm ${mode === m ? "on" : ""}`,
|
|
36755
|
+
onClick: () => changeMode(m),
|
|
36756
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
36757
|
+
className: "n",
|
|
36758
|
+
children: [t(`mode.${m}`), mode === m && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.check, {})]
|
|
36759
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
36760
|
+
className: "d",
|
|
36761
|
+
children: t(`mode.${m}.tip`)
|
|
36762
|
+
})]
|
|
36763
|
+
}, m))
|
|
36764
|
+
})
|
|
36765
|
+
}] : [],
|
|
36646
36766
|
{
|
|
36647
36767
|
id: "restream",
|
|
36648
36768
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.push, {}),
|
|
@@ -37479,6 +37599,12 @@ const en = {
|
|
|
37479
37599
|
"dev.blocked": "No picture or sound from the device: DeepSeek Harness needs the system’s permission. Open System Settings, switch DeepSeek Harness on, then retry.",
|
|
37480
37600
|
"dev.denied": "DeepSeek Harness needs permission to use the {d}. Open System Settings, switch it on in the list, then retry.",
|
|
37481
37601
|
"dev.and": " and ",
|
|
37602
|
+
"hist.title": "Recent videos",
|
|
37603
|
+
"hist.untitled": "Untitled video",
|
|
37604
|
+
"hist.live": "live",
|
|
37605
|
+
"hist.running": "Live now",
|
|
37606
|
+
"hist.more": "More ({n})",
|
|
37607
|
+
"set.spoken.tip": "The language people speak in this video; Auto detects it.",
|
|
37482
37608
|
"rs.mode": "Subtitle mode of the push-out",
|
|
37483
37609
|
"rs.mode.live": "Pushed out about {s} s behind; subtitles come a little after the words.",
|
|
37484
37610
|
"rs.mode.realtime": "Pushed out about {s} s behind, with first-pass subtitles in step.",
|
|
@@ -37927,6 +38053,12 @@ const zh = {
|
|
|
37927
38053
|
"dev.blocked": "设备没有传来画面和声音:DeepSeek Harness 需要系统授权。点「打开系统设置」,在列表里打开 DeepSeek Harness,再点重试。",
|
|
37928
38054
|
"dev.denied": "DeepSeek Harness 需要{d}权限。点「打开系统设置」,在列表里打开它,再点重试。",
|
|
37929
38055
|
"dev.and": "和",
|
|
38056
|
+
"hist.title": "最近的视频",
|
|
38057
|
+
"hist.untitled": "未命名视频",
|
|
38058
|
+
"hist.live": "直播",
|
|
38059
|
+
"hist.running": "直播中",
|
|
38060
|
+
"hist.more": "更多(还有 {n} 个)",
|
|
38061
|
+
"set.spoken.tip": "视频里说的是哪种语言;自动 = 自己识别。",
|
|
37930
38062
|
"rs.mode": "推流用的字幕模式",
|
|
37931
38063
|
"rs.mode.live": "推出去比实时晚约 {s} 秒,字幕会比说话稍晚出现。",
|
|
37932
38064
|
"rs.mode.realtime": "推出去比实时晚约 {s} 秒,带对齐好的初版字幕。",
|
|
@@ -38598,6 +38730,26 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
|
|
|
38598
38730
|
.dshl-menu .dshl-seg button.on { background: rgba(255,255,255,.92); color: #111; box-shadow: none; }
|
|
38599
38731
|
.dshl-menu .dshl-switch { background: rgba(255,255,255,.22); }
|
|
38600
38732
|
.dshl-menu .dshl-switch.on { background: var(--accent); }
|
|
38733
|
+
.dshl-history { width: 100%; max-width: 680px; margin-top: 40px; text-align: left; }
|
|
38734
|
+
.dshl-history .hh { margin: 0 0 8px 4px; color: var(--faint); font-size: 12.5px; }
|
|
38735
|
+
.dshl-history .hl { display: grid; gap: 2px; }
|
|
38736
|
+
.dshl-history .hi { display: flex; align-items: center; gap: 12px; width: 100%; padding: 6px; border: 0; border-radius: 10px; background: none; color: var(--ink); text-align: left; cursor: pointer; }
|
|
38737
|
+
.dshl-history .hi:hover { background: var(--hover); }
|
|
38738
|
+
.dshl-history .th { position: relative; flex: none; width: 96px; aspect-ratio: 16 / 9; overflow: hidden; border-radius: 6px; background: var(--l2); }
|
|
38739
|
+
.dshl-history .th img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
38740
|
+
.dshl-history .dur { position: absolute; right: 4px; bottom: 3px; padding: 0 4px; border-radius: 3px; background: rgba(0,0,0,.65); color: #fff; font-size: 10.5px; line-height: 16px; font-variant-numeric: tabular-nums; }
|
|
38741
|
+
.dshl-history .tx { display: grid; gap: 3px; min-width: 0; flex: 1; }
|
|
38742
|
+
.dshl-history .tt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; font-weight: 550; }
|
|
38743
|
+
.dshl-history .mm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--faint); font-size: 12px; }
|
|
38744
|
+
.dshl-history .chip { flex: none; padding: 2px 8px; border-radius: 999px; background: color-mix(in srgb, var(--live) 14%, transparent); color: var(--live); font-size: 11.5px; font-weight: 600; }
|
|
38745
|
+
.dshl-history .more { margin: 8px 0 0 6px; font-size: 12.5px; }
|
|
38746
|
+
.dshl-modemenu { display: grid; gap: 4px; width: 300px; max-width: 100%; }
|
|
38747
|
+
.dshl-modemenu .mm { display: grid; gap: 3px; padding: 9px 10px; border: 0; border-radius: 8px; background: none; color: inherit; text-align: left; cursor: pointer; }
|
|
38748
|
+
.dshl-modemenu .mm:hover { background: rgba(255,255,255,.08); }
|
|
38749
|
+
.dshl-modemenu .mm.on { background: rgba(255,255,255,.12); }
|
|
38750
|
+
.dshl-modemenu .n { display: flex; align-items: center; justify-content: space-between; font-size: 13.5px; font-weight: 600; }
|
|
38751
|
+
.dshl-modemenu .n svg { width: 14px; height: 14px; }
|
|
38752
|
+
.dshl-modemenu .d { color: rgba(255,255,255,.62); font-size: 12px; line-height: 1.45; }
|
|
38601
38753
|
.dshl-menu .opt { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 32px; padding: 0 6px; color: var(--muted); }
|
|
38602
38754
|
.dshl-menu .opt .dshl-seg { max-width: 100%; overflow-x: auto; scrollbar-width: none; }
|
|
38603
38755
|
.dshl-menu .opt input[type=range] { flex: 1; max-width: 180px; accent-color: var(--accent); }
|
|
@@ -38965,7 +39117,11 @@ async function openPage(sessions, opts = {}) {
|
|
|
38965
39117
|
}
|
|
38966
39118
|
async function showPage(sessions, opts) {
|
|
38967
39119
|
const current = sessions.current();
|
|
38968
|
-
const fresh = () =>
|
|
39120
|
+
const fresh = async () => {
|
|
39121
|
+
let id = await pageSession(sessions, opts.workspaces, opts.title ?? "Video");
|
|
39122
|
+
if (await fetch(`/live/api/job?conversationId=${encodeURIComponent(id)}`).then((r) => r.json()).catch(() => null)) id = await pageSession(sessions, opts.workspaces, opts.title ?? "Video", true);
|
|
39123
|
+
return id;
|
|
39124
|
+
};
|
|
38969
39125
|
let sid = opts.sessionId && sessions.has(opts.sessionId) ? opts.sessionId : await fresh();
|
|
38970
39126
|
if (sid !== current) try {
|
|
38971
39127
|
sessions.open(sid);
|
|
@@ -39030,6 +39186,23 @@ function videoHandlers(sessions, workspaces, sessionId, fallbackTitle) {
|
|
|
39030
39186
|
return false;
|
|
39031
39187
|
}
|
|
39032
39188
|
},
|
|
39189
|
+
/** Open a past video in its own conversation; false when that conversation is gone. */
|
|
39190
|
+
onOpenVideo: async (conversationId, jobId) => {
|
|
39191
|
+
if (!conversationId || !sessions.has(conversationId)) return false;
|
|
39192
|
+
await fetch("/live/api/reopen", {
|
|
39193
|
+
method: "POST",
|
|
39194
|
+
headers: { "content-type": "application/json" },
|
|
39195
|
+
body: JSON.stringify({
|
|
39196
|
+
jobId,
|
|
39197
|
+
conversationId
|
|
39198
|
+
})
|
|
39199
|
+
}).catch(() => {});
|
|
39200
|
+
await openPage(sessions, {
|
|
39201
|
+
sessionId: conversationId,
|
|
39202
|
+
workspaces
|
|
39203
|
+
});
|
|
39204
|
+
return true;
|
|
39205
|
+
},
|
|
39033
39206
|
onTitle: (title) => {
|
|
39034
39207
|
own().then((yes) => {
|
|
39035
39208
|
const now = sessions.info?.(sessionId)?.title;
|
|
@@ -39115,11 +39288,7 @@ function SidebarButton(props) {
|
|
|
39115
39288
|
const pct = item?.subs === "running" && item.duration ? Math.min(99, Math.round(item.at / item.duration * 100)) : void 0;
|
|
39116
39289
|
const meta = !busy ? "" : item?.subs === "running" ? pct !== void 0 ? t("side.subs").replace("{p}", String(pct)) : t("side.subsLive") : item?.recording ? t("side.recording") : item?.loading ? t("side.loading") : t("side.downloading");
|
|
39117
39290
|
const thumb = busy && frame && (!item || frame.jobId === item.jobId) ? frame.dataUrl : void 0;
|
|
39118
|
-
const open = () => void openPage(sessions,
|
|
39119
|
-
sessionId: item.conversationId,
|
|
39120
|
-
workspaces,
|
|
39121
|
-
title: t("view.live")
|
|
39122
|
-
} : {
|
|
39291
|
+
const open = () => void openPage(sessions, {
|
|
39123
39292
|
workspaces,
|
|
39124
39293
|
title: t("view.live")
|
|
39125
39294
|
});
|
package/lib/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
1
2
|
import { appendFileSync, chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { homedir, networkInterfaces } from "node:os";
|
|
3
4
|
import { basename, dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
4
5
|
import z from "@deepseek-ai/schemastery";
|
|
5
|
-
import { execFile, spawn } from "node:child_process";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { EventEmitter } from "node:events";
|
|
8
8
|
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
@@ -2111,6 +2111,12 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2111
2111
|
this.persist();
|
|
2112
2112
|
this.emit("state", this.summary());
|
|
2113
2113
|
}
|
|
2114
|
+
/** Show this video in another conversation (opened again from the history): saved, so it stays there. */
|
|
2115
|
+
rebind(conversationId) {
|
|
2116
|
+
this.opts.conversationId = conversationId;
|
|
2117
|
+
this.closed = false;
|
|
2118
|
+
this.persist();
|
|
2119
|
+
}
|
|
2114
2120
|
/** job.json: enough to bring the job back after a restart (finished, not recording). */
|
|
2115
2121
|
persist() {
|
|
2116
2122
|
if (this.opts.source === "push" && this.message) return;
|
|
@@ -6430,6 +6436,137 @@ function apply(ctx, config) {
|
|
|
6430
6436
|
};
|
|
6431
6437
|
}
|
|
6432
6438
|
/** The conversation's video: in memory, or the latest one on disk (after a restart). */
|
|
6439
|
+
/** A video by id: the running one, or brought back from its folder. */
|
|
6440
|
+
function loadJob(id) {
|
|
6441
|
+
if (jobs.has(id)) return jobs.get(id);
|
|
6442
|
+
if (!/^[\w-]+$/.test(id)) return void 0;
|
|
6443
|
+
const dir = join(config.dataDir, "jobs", id);
|
|
6444
|
+
const file = join(dir, "job.json");
|
|
6445
|
+
if (!existsSync(file)) return void 0;
|
|
6446
|
+
try {
|
|
6447
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
6448
|
+
let job;
|
|
6449
|
+
job = LiveJob.restore(dir, jobDeps(dir, raw.opts, () => job));
|
|
6450
|
+
if (job) jobs.set(job.id, job);
|
|
6451
|
+
return job;
|
|
6452
|
+
} catch {
|
|
6453
|
+
return;
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
/** Past videos for the start page, newest first: those with something to watch. */
|
|
6457
|
+
const historyCache = /* @__PURE__ */ new Map();
|
|
6458
|
+
function history() {
|
|
6459
|
+
const root = join(config.dataDir, "jobs");
|
|
6460
|
+
if (!existsSync(root)) return [];
|
|
6461
|
+
const out = [];
|
|
6462
|
+
for (const name of readdirSync(root).filter((n) => /^[\w-]+$/.test(n)).sort().reverse()) {
|
|
6463
|
+
const dir = join(root, name), file = join(dir, "job.json");
|
|
6464
|
+
const live = jobs.get(name);
|
|
6465
|
+
if (live && live.summary().status === "running" && !live.summary().message) {
|
|
6466
|
+
const s = live.summary();
|
|
6467
|
+
out.push({
|
|
6468
|
+
id: s.id,
|
|
6469
|
+
title: s.title || (s.source === "url" ? s.url : ""),
|
|
6470
|
+
source: s.source,
|
|
6471
|
+
url: s.source === "url" ? s.url : void 0,
|
|
6472
|
+
startedAt: s.startedAt,
|
|
6473
|
+
duration: s.vod ? s.duration : s.recorded,
|
|
6474
|
+
live: !s.vod,
|
|
6475
|
+
running: true,
|
|
6476
|
+
conversationId: s.conversationId
|
|
6477
|
+
});
|
|
6478
|
+
continue;
|
|
6479
|
+
}
|
|
6480
|
+
let mtime = 0;
|
|
6481
|
+
try {
|
|
6482
|
+
mtime = statSync(file).mtimeMs;
|
|
6483
|
+
} catch {
|
|
6484
|
+
continue;
|
|
6485
|
+
}
|
|
6486
|
+
let hit = historyCache.get(name);
|
|
6487
|
+
if (!hit || hit.mtime !== mtime) {
|
|
6488
|
+
let item = null;
|
|
6489
|
+
try {
|
|
6490
|
+
const j = JSON.parse(readFileSync(file, "utf8"));
|
|
6491
|
+
if ((existsSync(join(dir, "recording.mp4")) || existsSync(join(dir, "hls", "index.m3u8"))) && j.status !== "error") {
|
|
6492
|
+
const device = j.opts.source === "device" ? (() => {
|
|
6493
|
+
try {
|
|
6494
|
+
return JSON.parse(j.opts.url).title;
|
|
6495
|
+
} catch {
|
|
6496
|
+
return "";
|
|
6497
|
+
}
|
|
6498
|
+
})() : "";
|
|
6499
|
+
let duration = j.duration;
|
|
6500
|
+
if (!duration) try {
|
|
6501
|
+
duration = [...readFileSync(join(dir, "hls", "index.m3u8"), "utf8").matchAll(/#EXTINF:([\d.]+)/g)].reduce((n, m) => n + Number(m[1]), 0) || void 0;
|
|
6502
|
+
} catch {}
|
|
6503
|
+
item = {
|
|
6504
|
+
id: j.id,
|
|
6505
|
+
title: j.title || device || (j.opts.source === "url" ? j.opts.url : ""),
|
|
6506
|
+
source: j.opts.source,
|
|
6507
|
+
url: j.opts.source === "url" ? j.opts.url : void 0,
|
|
6508
|
+
startedAt: j.startedAt,
|
|
6509
|
+
duration,
|
|
6510
|
+
live: !j.vod,
|
|
6511
|
+
running: false,
|
|
6512
|
+
conversationId: j.opts.conversationId
|
|
6513
|
+
};
|
|
6514
|
+
}
|
|
6515
|
+
} catch {}
|
|
6516
|
+
hit = {
|
|
6517
|
+
mtime,
|
|
6518
|
+
item
|
|
6519
|
+
};
|
|
6520
|
+
historyCache.set(name, hit);
|
|
6521
|
+
}
|
|
6522
|
+
if (hit.item) out.push(hit.item);
|
|
6523
|
+
}
|
|
6524
|
+
return out;
|
|
6525
|
+
}
|
|
6526
|
+
/** A small picture of a video for its history row (made once, kept in its folder). */
|
|
6527
|
+
async function thumbnail(id) {
|
|
6528
|
+
if (!/^[\w-]+$/.test(id)) return void 0;
|
|
6529
|
+
const dir = join(config.dataDir, "jobs", id);
|
|
6530
|
+
const out = join(dir, "thumb.jpg");
|
|
6531
|
+
if (existsSync(out)) return out;
|
|
6532
|
+
for (const cover of ["cover.jpg", "cover.png"]) if (existsSync(join(dir, cover))) return join(dir, cover);
|
|
6533
|
+
const src = existsSync(join(dir, "recording.mp4")) ? join(dir, "recording.mp4") : existsSync(join(dir, "hls", "index.m3u8")) ? join(dir, "hls", "index.m3u8") : void 0;
|
|
6534
|
+
if (!src) return void 0;
|
|
6535
|
+
if (await new Promise((resolve) => {
|
|
6536
|
+
execFile("ffmpeg", [
|
|
6537
|
+
"-v",
|
|
6538
|
+
"error",
|
|
6539
|
+
"-y",
|
|
6540
|
+
"-i",
|
|
6541
|
+
src,
|
|
6542
|
+
"-ss",
|
|
6543
|
+
"3",
|
|
6544
|
+
"-frames:v",
|
|
6545
|
+
"1",
|
|
6546
|
+
"-vf",
|
|
6547
|
+
"scale=320:-2",
|
|
6548
|
+
"-q:v",
|
|
6549
|
+
"5",
|
|
6550
|
+
out
|
|
6551
|
+
], { timeout: 2e4 }, (err) => resolve(!err && existsSync(out)));
|
|
6552
|
+
})) return out;
|
|
6553
|
+
return await new Promise((resolve) => {
|
|
6554
|
+
execFile("ffmpeg", [
|
|
6555
|
+
"-v",
|
|
6556
|
+
"error",
|
|
6557
|
+
"-y",
|
|
6558
|
+
"-i",
|
|
6559
|
+
src,
|
|
6560
|
+
"-frames:v",
|
|
6561
|
+
"1",
|
|
6562
|
+
"-vf",
|
|
6563
|
+
"scale=320:-2",
|
|
6564
|
+
"-q:v",
|
|
6565
|
+
"5",
|
|
6566
|
+
out
|
|
6567
|
+
], { timeout: 2e4 }, (err) => resolve(!err && existsSync(out)));
|
|
6568
|
+
}) ? out : void 0;
|
|
6569
|
+
}
|
|
6433
6570
|
function jobForConversation(conversationId) {
|
|
6434
6571
|
const id = byConversation.get(conversationId);
|
|
6435
6572
|
if (id === "") return void 0;
|
|
@@ -6890,6 +7027,50 @@ function apply(ctx, config) {
|
|
|
6890
7027
|
saveAs: typeof b.saveAs === "string" ? b.saveAs.replace(/[\\/:*?"<>|]+/g, " ").slice(0, 150) : "audio"
|
|
6891
7028
|
}));
|
|
6892
7029
|
}
|
|
7030
|
+
if (route === "history") {
|
|
7031
|
+
const all = history();
|
|
7032
|
+
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
7033
|
+
const limit = Math.min(50, Math.max(1, Number(url.searchParams.get("limit")) || 5));
|
|
7034
|
+
return send(res, 200, {
|
|
7035
|
+
items: all.slice(offset, offset + limit),
|
|
7036
|
+
total: all.length
|
|
7037
|
+
});
|
|
7038
|
+
}
|
|
7039
|
+
if (route === "thumb") {
|
|
7040
|
+
const file = await thumbnail(url.searchParams.get("jobId") ?? "");
|
|
7041
|
+
if (!file) {
|
|
7042
|
+
res.writeHead(404);
|
|
7043
|
+
res.end();
|
|
7044
|
+
return;
|
|
7045
|
+
}
|
|
7046
|
+
res.writeHead(200, {
|
|
7047
|
+
"content-type": file.endsWith(".png") ? "image/png" : "image/jpeg",
|
|
7048
|
+
"cache-control": "max-age=3600"
|
|
7049
|
+
});
|
|
7050
|
+
createReadStream(file).pipe(res);
|
|
7051
|
+
return;
|
|
7052
|
+
}
|
|
7053
|
+
if (route === "reopen" && req.method === "POST") {
|
|
7054
|
+
const b = await readJson(req);
|
|
7055
|
+
const job = loadJob(String(b.jobId));
|
|
7056
|
+
const conv = String(b.conversationId ?? "");
|
|
7057
|
+
if (!job || !conv) return send(res, 404, { error: "no such video" });
|
|
7058
|
+
try {
|
|
7059
|
+
rmSync(join(job.summary().dir, "closed"), { force: true });
|
|
7060
|
+
} catch {}
|
|
7061
|
+
const prev = byConversation.get(conv);
|
|
7062
|
+
if (prev && prev !== job.id) {
|
|
7063
|
+
const other = jobs.get(prev);
|
|
7064
|
+
if (other && other.summary().status !== "running") byConversation.delete(conv);
|
|
7065
|
+
}
|
|
7066
|
+
job.rebind(conv);
|
|
7067
|
+
byConversation.set(conv, job.id);
|
|
7068
|
+
return send(res, 200, {
|
|
7069
|
+
...job.summary(),
|
|
7070
|
+
cues: job.cues,
|
|
7071
|
+
terms: job.terms
|
|
7072
|
+
});
|
|
7073
|
+
}
|
|
6893
7074
|
if (route === "preview") {
|
|
6894
7075
|
const job = jobs.get(url.searchParams.get("jobId") ?? "");
|
|
6895
7076
|
if (!job) return send(res, 404, { error: "no such job" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vidi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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",
|