dsh-vidi 0.9.0 → 0.9.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 +351 -27
- package/lib/index.mjs +288 -2
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -358,6 +358,13 @@ const Icon = {
|
|
|
358
358
|
rx: "2"
|
|
359
359
|
})
|
|
360
360
|
}),
|
|
361
|
+
merge: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
362
|
+
viewBox: "0 0 24 24",
|
|
363
|
+
width: "16",
|
|
364
|
+
height: "16",
|
|
365
|
+
...P,
|
|
366
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M7 4v5a5 5 0 0 0 5 5 5 5 0 0 1 5 5v1M17 4v5a5 5 0 0 1-3 4.6M12 17l-3 3M12 17l3 3" })
|
|
367
|
+
}),
|
|
361
368
|
timer: () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
362
369
|
viewBox: "0 0 24 24",
|
|
363
370
|
width: "18",
|
|
@@ -35376,6 +35383,106 @@ function useDevices(enabled) {
|
|
|
35376
35383
|
}, [enabled, devices]);
|
|
35377
35384
|
return devices;
|
|
35378
35385
|
}
|
|
35386
|
+
/** "3 分钟前", "5 小时前", "2 天前", then the date. */
|
|
35387
|
+
function ago(ts, ui) {
|
|
35388
|
+
if (!ts) return "";
|
|
35389
|
+
const s = Math.max(0, (Date.now() - ts) / 1e3);
|
|
35390
|
+
if (s < 60) return ui === "zh" ? "刚刚" : "just now";
|
|
35391
|
+
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, ""];
|
|
35392
|
+
if (unit) return `${n}${unit}`;
|
|
35393
|
+
return new Date(ts).toLocaleDateString(ui === "zh" ? "zh-CN" : "en-US", {
|
|
35394
|
+
month: "short",
|
|
35395
|
+
day: "numeric"
|
|
35396
|
+
});
|
|
35397
|
+
}
|
|
35398
|
+
/** Past videos under the start box: the newest few, more on request. */
|
|
35399
|
+
function History({ t, ui, onOpen }) {
|
|
35400
|
+
const [items, setItems] = (0, react.useState)([]);
|
|
35401
|
+
const [total, setTotal] = (0, react.useState)(0);
|
|
35402
|
+
const load = (offset, limit) => fetch(`/live/api/history?offset=${offset}&limit=${limit}`).then((r) => r.json()).then((j) => {
|
|
35403
|
+
setItems((v) => offset ? [...v, ...j.items ?? []] : j.items ?? []);
|
|
35404
|
+
setTotal(j.total ?? 0);
|
|
35405
|
+
}).catch(() => {});
|
|
35406
|
+
(0, react.useEffect)(() => {
|
|
35407
|
+
load(0, 5);
|
|
35408
|
+
}, []);
|
|
35409
|
+
if (!items.length) return null;
|
|
35410
|
+
const from = (v) => v.source === "file" ? t("src.file") : v.source === "device" ? t("src.camera") : v.source === "push" ? t("src.push") : siteName(v.url ?? "") || (() => {
|
|
35411
|
+
try {
|
|
35412
|
+
return new URL(v.url ?? "").hostname.replace(/^www\./, "");
|
|
35413
|
+
} catch {
|
|
35414
|
+
return t("src.link");
|
|
35415
|
+
}
|
|
35416
|
+
})();
|
|
35417
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
35418
|
+
className: "dshl-history",
|
|
35419
|
+
children: [
|
|
35420
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35421
|
+
className: "hh",
|
|
35422
|
+
children: t("hist.title")
|
|
35423
|
+
}),
|
|
35424
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35425
|
+
className: "hl",
|
|
35426
|
+
children: items.map((v) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35427
|
+
role: "button",
|
|
35428
|
+
tabIndex: 0,
|
|
35429
|
+
className: "hi",
|
|
35430
|
+
onClick: () => onOpen(v),
|
|
35431
|
+
onKeyDown: (e) => {
|
|
35432
|
+
if (e.key === "Enter") onOpen(v);
|
|
35433
|
+
},
|
|
35434
|
+
title: v.live && v.source === "url" && !v.running ? t("hist.goLive") : void 0,
|
|
35435
|
+
children: [
|
|
35436
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
35437
|
+
className: "th",
|
|
35438
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
35439
|
+
src: `/live/api/thumb?jobId=${encodeURIComponent(v.id)}`,
|
|
35440
|
+
alt: "",
|
|
35441
|
+
onError: (e) => {
|
|
35442
|
+
e.currentTarget.style.visibility = "hidden";
|
|
35443
|
+
}
|
|
35444
|
+
}), v.duration ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35445
|
+
className: "dur",
|
|
35446
|
+
children: CLOCK(v.duration)
|
|
35447
|
+
}) : null]
|
|
35448
|
+
}),
|
|
35449
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
35450
|
+
className: "tx",
|
|
35451
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35452
|
+
className: "tt",
|
|
35453
|
+
children: v.title || (v.source === "push" ? from(v) : t("hist.untitled"))
|
|
35454
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35455
|
+
className: "mm",
|
|
35456
|
+
children: [
|
|
35457
|
+
v.title || v.source !== "push" ? from(v) : "",
|
|
35458
|
+
v.live ? t("hist.live") : "",
|
|
35459
|
+
ago(v.startedAt, ui)
|
|
35460
|
+
].filter(Boolean).join(" · ")
|
|
35461
|
+
})]
|
|
35462
|
+
}),
|
|
35463
|
+
v.running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35464
|
+
className: "chip",
|
|
35465
|
+
children: t("hist.running")
|
|
35466
|
+
}),
|
|
35467
|
+
v.live && v.source === "url" && !v.running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35468
|
+
className: "dshl-link replay",
|
|
35469
|
+
onClick: (e) => {
|
|
35470
|
+
e.stopPropagation();
|
|
35471
|
+
onOpen(v, true);
|
|
35472
|
+
},
|
|
35473
|
+
children: t("hist.replay")
|
|
35474
|
+
})
|
|
35475
|
+
]
|
|
35476
|
+
}, v.id))
|
|
35477
|
+
}),
|
|
35478
|
+
items.length < total && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35479
|
+
className: "dshl-link more",
|
|
35480
|
+
onClick: () => void load(items.length, 10),
|
|
35481
|
+
children: t("hist.more").replace("{n}", String(total - items.length))
|
|
35482
|
+
})
|
|
35483
|
+
]
|
|
35484
|
+
});
|
|
35485
|
+
}
|
|
35379
35486
|
/** The push stream key: copy it, or change it (typed, or a new random one). */
|
|
35380
35487
|
function KeyRow({ value, t, onChanged }) {
|
|
35381
35488
|
const [edit, setEdit] = (0, react.useState)(null);
|
|
@@ -35844,6 +35951,20 @@ function LiveView(props) {
|
|
|
35844
35951
|
}).catch(() => job));
|
|
35845
35952
|
};
|
|
35846
35953
|
/** Switching modes while a video is open takes effect right away: no delay stops transcribing, the others (re)start it. */
|
|
35954
|
+
const switchEngine = async (id) => {
|
|
35955
|
+
setEngine(id);
|
|
35956
|
+
if (!job || subs.state === "off" || job.status === "error") return;
|
|
35957
|
+
try {
|
|
35958
|
+
setJob(await post("subs", {
|
|
35959
|
+
...options(),
|
|
35960
|
+
provider: id,
|
|
35961
|
+
jobId: job.id,
|
|
35962
|
+
action: "start"
|
|
35963
|
+
}));
|
|
35964
|
+
} catch (e) {
|
|
35965
|
+
setError(e.message);
|
|
35966
|
+
}
|
|
35967
|
+
};
|
|
35847
35968
|
const changeMode = (m) => {
|
|
35848
35969
|
if (m === mode) return;
|
|
35849
35970
|
setMode(m);
|
|
@@ -36123,6 +36244,27 @@ function LiveView(props) {
|
|
|
36123
36244
|
return "camera";
|
|
36124
36245
|
}
|
|
36125
36246
|
};
|
|
36247
|
+
const openPast = async (v, replay = false) => {
|
|
36248
|
+
setError("");
|
|
36249
|
+
if (!replay && v.live && v.source === "url" && v.url && !v.running) {
|
|
36250
|
+
if (v.conversationId !== sessionId && props.onOpenVideo && await props.onOpenVideo(v.conversationId, v.id, v.url)) return;
|
|
36251
|
+
start(v.url, "url");
|
|
36252
|
+
return;
|
|
36253
|
+
}
|
|
36254
|
+
try {
|
|
36255
|
+
if (v.conversationId !== sessionId && props.onOpenVideo && await props.onOpenVideo(v.conversationId, v.id)) return;
|
|
36256
|
+
const j = await post("reopen", {
|
|
36257
|
+
jobId: v.id,
|
|
36258
|
+
conversationId: sessionId
|
|
36259
|
+
});
|
|
36260
|
+
setCues(j.cues ?? []);
|
|
36261
|
+
setTerms(j.terms ?? []);
|
|
36262
|
+
setJob(j);
|
|
36263
|
+
setArmed(null);
|
|
36264
|
+
} catch (e) {
|
|
36265
|
+
setError(e.message);
|
|
36266
|
+
}
|
|
36267
|
+
};
|
|
36126
36268
|
const emptyBox = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
36127
36269
|
className: "dshl-drop",
|
|
36128
36270
|
children: [
|
|
@@ -36132,6 +36274,11 @@ function LiveView(props) {
|
|
|
36132
36274
|
children: sourceBar
|
|
36133
36275
|
}),
|
|
36134
36276
|
/* @__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") }),
|
|
36277
|
+
pageMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(History, {
|
|
36278
|
+
t,
|
|
36279
|
+
ui,
|
|
36280
|
+
onOpen: (v, replay) => void openPast(v, replay)
|
|
36281
|
+
}),
|
|
36135
36282
|
srcTab === "push" && armed?.rtmp && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36136
36283
|
className: "dshl-pushinfo",
|
|
36137
36284
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyRow, {
|
|
@@ -36206,6 +36353,34 @@ function LiveView(props) {
|
|
|
36206
36353
|
openBench: setBench
|
|
36207
36354
|
} : void 0;
|
|
36208
36355
|
/** Subtitle lines and glossary: under the player on the video page, beside it in a conversation. */
|
|
36356
|
+
const [splitting, setSplitting] = (0, react.useState)(null);
|
|
36357
|
+
const lineKey = (k) => k === "src" || k === spokenBase ? "src" : k;
|
|
36358
|
+
const mergeLine = async (id) => {
|
|
36359
|
+
try {
|
|
36360
|
+
await post("lines", {
|
|
36361
|
+
jobId: job.id,
|
|
36362
|
+
action: "merge",
|
|
36363
|
+
id
|
|
36364
|
+
});
|
|
36365
|
+
} catch (e) {
|
|
36366
|
+
setError(e.message);
|
|
36367
|
+
}
|
|
36368
|
+
};
|
|
36369
|
+
const splitLine = async (id, key, at) => {
|
|
36370
|
+
setSplitting(null);
|
|
36371
|
+
if (at <= 0) return;
|
|
36372
|
+
try {
|
|
36373
|
+
await post("lines", {
|
|
36374
|
+
jobId: job.id,
|
|
36375
|
+
action: "split",
|
|
36376
|
+
id,
|
|
36377
|
+
key,
|
|
36378
|
+
at
|
|
36379
|
+
});
|
|
36380
|
+
} catch (e) {
|
|
36381
|
+
setError(e.message);
|
|
36382
|
+
}
|
|
36383
|
+
};
|
|
36209
36384
|
const transcriptBox = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
36210
36385
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
36211
36386
|
className: "dshl-sidetabs",
|
|
@@ -36270,9 +36445,45 @@ function LiveView(props) {
|
|
|
36270
36445
|
children: [shown.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36271
36446
|
className: "dshl-muted pad",
|
|
36272
36447
|
children: emptyLines
|
|
36273
|
-
}), shown.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
36448
|
+
}), shown.map((c, idx) => splitting?.id === c.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
36449
|
+
className: "dshl-line splitting",
|
|
36450
|
+
children: [
|
|
36451
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
36452
|
+
className: "ts",
|
|
36453
|
+
children: CLOCK(c.start)
|
|
36454
|
+
}),
|
|
36455
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
36456
|
+
className: "txt",
|
|
36457
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
36458
|
+
className: "main cutline",
|
|
36459
|
+
children: Array.from((splitting.key === "src" ? c.src : c.tr?.[splitting.key]) ?? "").map((ch, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
36460
|
+
className: "ch",
|
|
36461
|
+
onClick: () => void splitLine(c.id, splitting.key, i),
|
|
36462
|
+
children: ch
|
|
36463
|
+
}, i))
|
|
36464
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
36465
|
+
className: "hint",
|
|
36466
|
+
children: [
|
|
36467
|
+
t("lines.splitHint"),
|
|
36468
|
+
" ",
|
|
36469
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
36470
|
+
className: "dshl-link",
|
|
36471
|
+
onClick: () => setSplitting(null),
|
|
36472
|
+
children: t("player.cancel")
|
|
36473
|
+
})
|
|
36474
|
+
]
|
|
36475
|
+
})]
|
|
36476
|
+
}),
|
|
36477
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {})
|
|
36478
|
+
]
|
|
36479
|
+
}, c.id) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
36480
|
+
role: "button",
|
|
36481
|
+
tabIndex: 0,
|
|
36274
36482
|
className: `dshl-line ${c.id === activeId ? "active" : ""} ${c.polished ? "ok" : "raw"}`,
|
|
36275
36483
|
onClick: () => player.current?.seek(c.start),
|
|
36484
|
+
onKeyDown: (e) => {
|
|
36485
|
+
if (e.key === "Enter") player.current?.seek(c.start);
|
|
36486
|
+
},
|
|
36276
36487
|
title: c.polished ? t("tr.polished") : t("tr.raw"),
|
|
36277
36488
|
children: [
|
|
36278
36489
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -36293,6 +36504,27 @@ function LiveView(props) {
|
|
|
36293
36504
|
className: "mark",
|
|
36294
36505
|
"aria-hidden": "true",
|
|
36295
36506
|
children: c.polished && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.check, {})
|
|
36507
|
+
}),
|
|
36508
|
+
c.final && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
36509
|
+
className: "tools",
|
|
36510
|
+
onClick: (e) => e.stopPropagation(),
|
|
36511
|
+
children: [idx < shown.length - 1 && shown[idx + 1]?.final && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
36512
|
+
className: "ghost",
|
|
36513
|
+
title: t("lines.merge"),
|
|
36514
|
+
onClick: () => void mergeLine(c.id),
|
|
36515
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.merge, {})
|
|
36516
|
+
}), (() => {
|
|
36517
|
+
const k = shownLines.find((x) => (lineText(c, x) ?? "").length > 1) ?? "src";
|
|
36518
|
+
return Array.from(lineText(c, k) ?? c.src).length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
36519
|
+
className: "ghost",
|
|
36520
|
+
title: t("lines.split"),
|
|
36521
|
+
onClick: () => setSplitting({
|
|
36522
|
+
id: c.id,
|
|
36523
|
+
key: lineKey(k)
|
|
36524
|
+
}),
|
|
36525
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.scissors, {})
|
|
36526
|
+
});
|
|
36527
|
+
})()]
|
|
36296
36528
|
})
|
|
36297
36529
|
]
|
|
36298
36530
|
}, c.id))]
|
|
@@ -36632,30 +36864,52 @@ function LiveView(props) {
|
|
|
36632
36864
|
t
|
|
36633
36865
|
}) })
|
|
36634
36866
|
},
|
|
36635
|
-
|
|
36636
|
-
id: "
|
|
36867
|
+
{
|
|
36868
|
+
id: "how",
|
|
36637
36869
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.timer, {}),
|
|
36638
|
-
title: t("
|
|
36639
|
-
label: t(`mode.${mode}`),
|
|
36640
|
-
content: /* @__PURE__ */ (0, react_jsx_runtime.
|
|
36870
|
+
title: t("how.title"),
|
|
36871
|
+
label: [!job.vod ? t(`mode.${mode}`) : "", engineName(usableEngine())].filter(Boolean).join(" · "),
|
|
36872
|
+
content: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
36641
36873
|
className: "dshl-modemenu",
|
|
36642
36874
|
children: [
|
|
36643
|
-
"
|
|
36644
|
-
|
|
36645
|
-
|
|
36646
|
-
|
|
36647
|
-
|
|
36648
|
-
|
|
36649
|
-
|
|
36650
|
-
|
|
36651
|
-
|
|
36652
|
-
|
|
36653
|
-
|
|
36654
|
-
|
|
36655
|
-
|
|
36656
|
-
|
|
36875
|
+
!job.vod && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36876
|
+
className: "mh",
|
|
36877
|
+
children: t("set.mode")
|
|
36878
|
+
}), [
|
|
36879
|
+
"live",
|
|
36880
|
+
"realtime",
|
|
36881
|
+
"polished"
|
|
36882
|
+
].map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
36883
|
+
className: `mm ${mode === m ? "on" : ""}`,
|
|
36884
|
+
onClick: () => changeMode(m),
|
|
36885
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
36886
|
+
className: "n",
|
|
36887
|
+
children: [t(`mode.${m}`), mode === m && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.check, {})]
|
|
36888
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
36889
|
+
className: "d",
|
|
36890
|
+
children: t(`mode.${m}.tip`)
|
|
36891
|
+
})]
|
|
36892
|
+
}, m))] }),
|
|
36893
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36894
|
+
className: "mh",
|
|
36895
|
+
children: t("set.engine")
|
|
36896
|
+
}),
|
|
36897
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
36898
|
+
value: usableEngine(),
|
|
36899
|
+
onChange: (id) => void switchEngine(id),
|
|
36900
|
+
options: providers.filter((p) => p.ready).map((p) => ({
|
|
36901
|
+
value: p.id,
|
|
36902
|
+
label: engineName(p.id),
|
|
36903
|
+
hint: p.model
|
|
36904
|
+
}))
|
|
36905
|
+
}),
|
|
36906
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
36907
|
+
className: "mt",
|
|
36908
|
+
children: t("how.more")
|
|
36909
|
+
})
|
|
36910
|
+
]
|
|
36657
36911
|
})
|
|
36658
|
-
}
|
|
36912
|
+
},
|
|
36659
36913
|
{
|
|
36660
36914
|
id: "restream",
|
|
36661
36915
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.push, {}),
|
|
@@ -37492,6 +37746,18 @@ const en = {
|
|
|
37492
37746
|
"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.",
|
|
37493
37747
|
"dev.denied": "DeepSeek Harness needs permission to use the {d}. Open System Settings, switch it on in the list, then retry.",
|
|
37494
37748
|
"dev.and": " and ",
|
|
37749
|
+
"how.title": "How subtitles are made",
|
|
37750
|
+
"how.more": "More services and keys are in Settings.",
|
|
37751
|
+
"lines.merge": "Join with the next line",
|
|
37752
|
+
"lines.split": "Split this line",
|
|
37753
|
+
"lines.splitHint": "Click where the line should break.",
|
|
37754
|
+
"hist.goLive": "Watch live again",
|
|
37755
|
+
"hist.replay": "Replay",
|
|
37756
|
+
"hist.title": "Recent videos",
|
|
37757
|
+
"hist.untitled": "Untitled video",
|
|
37758
|
+
"hist.live": "live",
|
|
37759
|
+
"hist.running": "Live now",
|
|
37760
|
+
"hist.more": "More ({n})",
|
|
37495
37761
|
"set.spoken.tip": "The language people speak in this video; Auto detects it.",
|
|
37496
37762
|
"rs.mode": "Subtitle mode of the push-out",
|
|
37497
37763
|
"rs.mode.live": "Pushed out about {s} s behind; subtitles come a little after the words.",
|
|
@@ -37941,6 +38207,18 @@ const zh = {
|
|
|
37941
38207
|
"dev.blocked": "设备没有传来画面和声音:DeepSeek Harness 需要系统授权。点「打开系统设置」,在列表里打开 DeepSeek Harness,再点重试。",
|
|
37942
38208
|
"dev.denied": "DeepSeek Harness 需要{d}权限。点「打开系统设置」,在列表里打开它,再点重试。",
|
|
37943
38209
|
"dev.and": "和",
|
|
38210
|
+
"how.title": "字幕怎么做",
|
|
38211
|
+
"how.more": "更多服务和 key 在设置里。",
|
|
38212
|
+
"lines.merge": "和下一句合并",
|
|
38213
|
+
"lines.split": "拆成两句",
|
|
38214
|
+
"lines.splitHint": "点一下要断开的位置。",
|
|
38215
|
+
"hist.goLive": "回到直播",
|
|
38216
|
+
"hist.replay": "回放",
|
|
38217
|
+
"hist.title": "最近的视频",
|
|
38218
|
+
"hist.untitled": "未命名视频",
|
|
38219
|
+
"hist.live": "直播",
|
|
38220
|
+
"hist.running": "直播中",
|
|
38221
|
+
"hist.more": "更多(还有 {n} 个)",
|
|
37944
38222
|
"set.spoken.tip": "视频里说的是哪种语言;自动 = 自己识别。",
|
|
37945
38223
|
"rs.mode": "推流用的字幕模式",
|
|
37946
38224
|
"rs.mode.live": "推出去比实时晚约 {s} 秒,字幕会比说话稍晚出现。",
|
|
@@ -38613,6 +38891,34 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
|
|
|
38613
38891
|
.dshl-menu .dshl-seg button.on { background: rgba(255,255,255,.92); color: #111; box-shadow: none; }
|
|
38614
38892
|
.dshl-menu .dshl-switch { background: rgba(255,255,255,.22); }
|
|
38615
38893
|
.dshl-menu .dshl-switch.on { background: var(--accent); }
|
|
38894
|
+
.dshl-history { width: 100%; max-width: 680px; margin-top: 40px; text-align: left; }
|
|
38895
|
+
.dshl-history .hh { margin: 0 0 8px 4px; color: var(--faint); font-size: 12.5px; }
|
|
38896
|
+
.dshl-history .hl { display: grid; gap: 2px; }
|
|
38897
|
+
.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; }
|
|
38898
|
+
.dshl-history .hi:hover { background: var(--hover); }
|
|
38899
|
+
.dshl-history .th { position: relative; flex: none; width: 96px; aspect-ratio: 16 / 9; overflow: hidden; border-radius: 6px; background: var(--l2); }
|
|
38900
|
+
.dshl-history .th img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
38901
|
+
.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; }
|
|
38902
|
+
.dshl-history .tx { display: grid; gap: 3px; min-width: 0; flex: 1; }
|
|
38903
|
+
.dshl-history .tt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; font-weight: 550; }
|
|
38904
|
+
.dshl-history .mm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--faint); font-size: 12px; }
|
|
38905
|
+
.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; }
|
|
38906
|
+
.dshl-history .more { margin: 8px 0 0 6px; font-size: 12.5px; }
|
|
38907
|
+
.dshl-modemenu .mh { margin: 6px 4px 2px; color: rgba(255,255,255,.55); font-size: 11.5px; }
|
|
38908
|
+
.dshl-modemenu .mh:first-child { margin-top: 0; }
|
|
38909
|
+
.dshl-modemenu .dshl-select { width: 100%; }
|
|
38910
|
+
.dshl-modemenu .mt { margin: 2px 4px 0; color: rgba(255,255,255,.45); font-size: 11.5px; }
|
|
38911
|
+
.dshl-line { position: relative; }
|
|
38912
|
+
.dshl-line .tools { position: absolute; top: 4px; right: 4px; display: none; gap: 2px; padding: 2px; border-radius: 7px; background: var(--bg); box-shadow: 0 1px 4px rgba(0,0,0,.12); }
|
|
38913
|
+
.dshl-line:hover .tools, .dshl-line:focus-within .tools { display: flex; }
|
|
38914
|
+
.dshl-line .tools .ghost { display: grid; place-items: center; width: 26px; height: 24px; border: 0; border-radius: 5px; background: none; color: var(--muted); cursor: pointer; }
|
|
38915
|
+
.dshl-line .tools svg { width: 15px; height: 15px; }
|
|
38916
|
+
.dshl-line .tools .ghost:hover { background: var(--hover); color: var(--ink); }
|
|
38917
|
+
.dshl-line.splitting { cursor: default; background: color-mix(in srgb, var(--accent) 8%, transparent); }
|
|
38918
|
+
.dshl-line .cutline .ch { cursor: col-resize; border-left: 1px solid transparent; }
|
|
38919
|
+
.dshl-line .cutline .ch:hover { border-left-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
|
|
38920
|
+
.dshl-line .hint { color: var(--faint); font-size: 12px; }
|
|
38921
|
+
.dshl-history .replay { flex: none; font-size: 12.5px; }
|
|
38616
38922
|
.dshl-modemenu { display: grid; gap: 4px; width: 300px; max-width: 100%; }
|
|
38617
38923
|
.dshl-modemenu .mm { display: grid; gap: 3px; padding: 9px 10px; border: 0; border-radius: 8px; background: none; color: inherit; text-align: left; cursor: pointer; }
|
|
38618
38924
|
.dshl-modemenu .mm:hover { background: rgba(255,255,255,.08); }
|
|
@@ -38987,7 +39293,11 @@ async function openPage(sessions, opts = {}) {
|
|
|
38987
39293
|
}
|
|
38988
39294
|
async function showPage(sessions, opts) {
|
|
38989
39295
|
const current = sessions.current();
|
|
38990
|
-
const fresh = () =>
|
|
39296
|
+
const fresh = async () => {
|
|
39297
|
+
let id = await pageSession(sessions, opts.workspaces, opts.title ?? "Video");
|
|
39298
|
+
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);
|
|
39299
|
+
return id;
|
|
39300
|
+
};
|
|
38991
39301
|
let sid = opts.sessionId && sessions.has(opts.sessionId) ? opts.sessionId : await fresh();
|
|
38992
39302
|
if (sid !== current) try {
|
|
38993
39303
|
sessions.open(sid);
|
|
@@ -39052,6 +39362,24 @@ function videoHandlers(sessions, workspaces, sessionId, fallbackTitle) {
|
|
|
39052
39362
|
return false;
|
|
39053
39363
|
}
|
|
39054
39364
|
},
|
|
39365
|
+
/** Open a past video in its own conversation; false when that conversation is gone. */
|
|
39366
|
+
onOpenVideo: async (conversationId, jobId, pending) => {
|
|
39367
|
+
if (!conversationId || !sessions.has(conversationId)) return false;
|
|
39368
|
+
if (!pending) await fetch("/live/api/reopen", {
|
|
39369
|
+
method: "POST",
|
|
39370
|
+
headers: { "content-type": "application/json" },
|
|
39371
|
+
body: JSON.stringify({
|
|
39372
|
+
jobId,
|
|
39373
|
+
conversationId
|
|
39374
|
+
})
|
|
39375
|
+
}).catch(() => {});
|
|
39376
|
+
await openPage(sessions, {
|
|
39377
|
+
sessionId: conversationId,
|
|
39378
|
+
workspaces,
|
|
39379
|
+
pending
|
|
39380
|
+
});
|
|
39381
|
+
return true;
|
|
39382
|
+
},
|
|
39055
39383
|
onTitle: (title) => {
|
|
39056
39384
|
own().then((yes) => {
|
|
39057
39385
|
const now = sessions.info?.(sessionId)?.title;
|
|
@@ -39137,11 +39465,7 @@ function SidebarButton(props) {
|
|
|
39137
39465
|
const pct = item?.subs === "running" && item.duration ? Math.min(99, Math.round(item.at / item.duration * 100)) : void 0;
|
|
39138
39466
|
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");
|
|
39139
39467
|
const thumb = busy && frame && (!item || frame.jobId === item.jobId) ? frame.dataUrl : void 0;
|
|
39140
|
-
const open = () => void openPage(sessions,
|
|
39141
|
-
sessionId: item.conversationId,
|
|
39142
|
-
workspaces,
|
|
39143
|
-
title: t("view.live")
|
|
39144
|
-
} : {
|
|
39468
|
+
const open = () => void openPage(sessions, {
|
|
39145
39469
|
workspaces,
|
|
39146
39470
|
title: t("view.live")
|
|
39147
39471
|
});
|
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";
|
|
@@ -1990,6 +1990,32 @@ const AHEAD_MAX_LINES = 10;
|
|
|
1990
1990
|
const seekAfter = (skip) => skip > .01 ? ["-ss", skip.toFixed(3)] : [];
|
|
1991
1991
|
const SOI = Buffer.from([255, 216]);
|
|
1992
1992
|
const EOI = Buffer.from([255, 217]);
|
|
1993
|
+
const CJK_CHAR = /[\u3040-\u30ff\u3400-\u9fff\uac00-\ud7af\uff00-\uffef\u3000-\u303f]/;
|
|
1994
|
+
/** Two pieces of one language as one line: no space between CJK text, a space between words otherwise. */
|
|
1995
|
+
function joinText(a, b) {
|
|
1996
|
+
const x = a.trim(), y = b.trim();
|
|
1997
|
+
if (!x || !y) return x || y;
|
|
1998
|
+
return CJK_CHAR.test(x.at(-1)) || CJK_CHAR.test(y[0]) ? x + y : `${x} ${y}`;
|
|
1999
|
+
}
|
|
2000
|
+
/** Where to cut a text near `ratio` of its length: after a pause mark if one is close, else between words. */
|
|
2001
|
+
function cutNear(cs, ratio) {
|
|
2002
|
+
const n = cs.length;
|
|
2003
|
+
if (n < 2) return n;
|
|
2004
|
+
const target = Math.round(n * ratio);
|
|
2005
|
+
let best = -1, bestScore = Infinity;
|
|
2006
|
+
for (let i = 1; i < n; i++) {
|
|
2007
|
+
const prev = cs[i - 1], here = cs[i];
|
|
2008
|
+
const pause = /[,。、;:!?,.;:!?]/.test(prev);
|
|
2009
|
+
const word = here === " " || CJK_CHAR.test(prev) || CJK_CHAR.test(here);
|
|
2010
|
+
if (!pause && !word) continue;
|
|
2011
|
+
const score = Math.abs(i - target) - (pause ? n * .12 : 0);
|
|
2012
|
+
if (score < bestScore) {
|
|
2013
|
+
bestScore = score;
|
|
2014
|
+
best = i;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
return best > 0 ? best : Math.max(1, Math.min(n - 1, target));
|
|
2018
|
+
}
|
|
1993
2019
|
var LiveJob = class LiveJob extends EventEmitter {
|
|
1994
2020
|
opts;
|
|
1995
2021
|
deps;
|
|
@@ -2052,7 +2078,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2052
2078
|
targets: () => this.targets(),
|
|
2053
2079
|
still: (at) => this.still(at),
|
|
2054
2080
|
onLine: (cue, src, tr, speaker) => {
|
|
2055
|
-
if (!this.cues.includes(cue)) return;
|
|
2081
|
+
if (!this.cues.includes(cue) || cue.edited) return;
|
|
2056
2082
|
this.deps.glossary.notice(src);
|
|
2057
2083
|
cue.src = src;
|
|
2058
2084
|
if (speaker) cue.speaker = speaker;
|
|
@@ -2111,6 +2137,12 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2111
2137
|
this.persist();
|
|
2112
2138
|
this.emit("state", this.summary());
|
|
2113
2139
|
}
|
|
2140
|
+
/** Show this video in another conversation (opened again from the history): saved, so it stays there. */
|
|
2141
|
+
rebind(conversationId) {
|
|
2142
|
+
this.opts.conversationId = conversationId;
|
|
2143
|
+
this.closed = false;
|
|
2144
|
+
this.persist();
|
|
2145
|
+
}
|
|
2114
2146
|
/** job.json: enough to bring the job back after a restart (finished, not recording). */
|
|
2115
2147
|
persist() {
|
|
2116
2148
|
if (this.opts.source === "push" && this.message) return;
|
|
@@ -2854,6 +2886,76 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2854
2886
|
})).join("\n") + "\n");
|
|
2855
2887
|
this.emit("cues", this.cues);
|
|
2856
2888
|
}
|
|
2889
|
+
/** Join a finished line with the next one (the user's own line breaks). */
|
|
2890
|
+
mergeCue(id) {
|
|
2891
|
+
const i = this.cues.findIndex((c) => c.id === id);
|
|
2892
|
+
const a = this.cues[i], b = this.cues[i + 1];
|
|
2893
|
+
if (!a || !b || !a.final || !b.final) return false;
|
|
2894
|
+
a.src = joinText(a.src, b.src);
|
|
2895
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a.tr ?? {}), ...Object.keys(b.tr ?? {})]);
|
|
2896
|
+
if (keys.size) a.tr = Object.fromEntries([...keys].map((k) => [k, joinText(a.tr?.[k] ?? "", b.tr?.[k] ?? "")]));
|
|
2897
|
+
a.end = Math.max(a.end, b.end);
|
|
2898
|
+
a.polished = !!a.polished && !!b.polished;
|
|
2899
|
+
a.edited = true;
|
|
2900
|
+
this.cues.splice(i + 1, 1);
|
|
2901
|
+
appendFileSync(join(this.deps.dir, "cues.jsonl"), JSON.stringify({
|
|
2902
|
+
id: b.id,
|
|
2903
|
+
dropped: true
|
|
2904
|
+
}) + "\n");
|
|
2905
|
+
this.record(a);
|
|
2906
|
+
this.emit("cues", this.cues);
|
|
2907
|
+
return true;
|
|
2908
|
+
}
|
|
2909
|
+
/**
|
|
2910
|
+
* Split a finished line in two where the user pointed: `key` is the text they pointed in ('src' or a
|
|
2911
|
+
* language) and `at` the character position. The other texts are cut at the matching place (at a
|
|
2912
|
+
* pause or a word nearby), and the time in proportion.
|
|
2913
|
+
*/
|
|
2914
|
+
splitCue(id, key, at) {
|
|
2915
|
+
const i = this.cues.findIndex((c) => c.id === id);
|
|
2916
|
+
const a = this.cues[i];
|
|
2917
|
+
if (!a || !a.final) return false;
|
|
2918
|
+
const text = key === "src" ? a.src : a.tr?.[key];
|
|
2919
|
+
if (!text) return false;
|
|
2920
|
+
const chars = Array.from(text);
|
|
2921
|
+
const cut = Math.max(1, Math.min(chars.length - 1, Math.round(at)));
|
|
2922
|
+
const ratio = cut / chars.length;
|
|
2923
|
+
const pieces = (t, own) => {
|
|
2924
|
+
const cs = Array.from(t);
|
|
2925
|
+
const k = own ? cut : cutNear(cs, ratio);
|
|
2926
|
+
return [cs.slice(0, k).join("").trim(), cs.slice(k).join("").trim()];
|
|
2927
|
+
};
|
|
2928
|
+
const [srcA, srcB] = pieces(a.src, key === "src");
|
|
2929
|
+
if (!srcA || !srcB) return false;
|
|
2930
|
+
const trA = {}, trB = {};
|
|
2931
|
+
for (const [k, v] of Object.entries(a.tr ?? {})) {
|
|
2932
|
+
const [x, y] = pieces(v, k === key);
|
|
2933
|
+
trA[k] = x;
|
|
2934
|
+
trB[k] = y;
|
|
2935
|
+
}
|
|
2936
|
+
const mid = a.start + (a.end - a.start) * ratio;
|
|
2937
|
+
const b = {
|
|
2938
|
+
id: this.nextId++,
|
|
2939
|
+
start: mid,
|
|
2940
|
+
end: a.end,
|
|
2941
|
+
src: srcB,
|
|
2942
|
+
tr: Object.keys(trB).length ? trB : void 0,
|
|
2943
|
+
lang: a.lang,
|
|
2944
|
+
final: true,
|
|
2945
|
+
polished: a.polished,
|
|
2946
|
+
speaker: a.speaker,
|
|
2947
|
+
edited: true
|
|
2948
|
+
};
|
|
2949
|
+
a.src = srcA;
|
|
2950
|
+
if (Object.keys(trA).length) a.tr = trA;
|
|
2951
|
+
a.end = mid;
|
|
2952
|
+
a.edited = true;
|
|
2953
|
+
this.cues.splice(i + 1, 0, b);
|
|
2954
|
+
this.record(a);
|
|
2955
|
+
this.record(b);
|
|
2956
|
+
this.emit("cues", this.cues);
|
|
2957
|
+
return true;
|
|
2958
|
+
}
|
|
2857
2959
|
/** Append a finished cue (with its translation, if any) to cues.jsonl. */
|
|
2858
2960
|
record(cue) {
|
|
2859
2961
|
appendFileSync(join(this.deps.dir, "cues.jsonl"), JSON.stringify(cue) + "\n");
|
|
@@ -6430,6 +6532,137 @@ function apply(ctx, config) {
|
|
|
6430
6532
|
};
|
|
6431
6533
|
}
|
|
6432
6534
|
/** The conversation's video: in memory, or the latest one on disk (after a restart). */
|
|
6535
|
+
/** A video by id: the running one, or brought back from its folder. */
|
|
6536
|
+
function loadJob(id) {
|
|
6537
|
+
if (jobs.has(id)) return jobs.get(id);
|
|
6538
|
+
if (!/^[\w-]+$/.test(id)) return void 0;
|
|
6539
|
+
const dir = join(config.dataDir, "jobs", id);
|
|
6540
|
+
const file = join(dir, "job.json");
|
|
6541
|
+
if (!existsSync(file)) return void 0;
|
|
6542
|
+
try {
|
|
6543
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
6544
|
+
let job;
|
|
6545
|
+
job = LiveJob.restore(dir, jobDeps(dir, raw.opts, () => job));
|
|
6546
|
+
if (job) jobs.set(job.id, job);
|
|
6547
|
+
return job;
|
|
6548
|
+
} catch {
|
|
6549
|
+
return;
|
|
6550
|
+
}
|
|
6551
|
+
}
|
|
6552
|
+
/** Past videos for the start page, newest first: those with something to watch. */
|
|
6553
|
+
const historyCache = /* @__PURE__ */ new Map();
|
|
6554
|
+
function history() {
|
|
6555
|
+
const root = join(config.dataDir, "jobs");
|
|
6556
|
+
if (!existsSync(root)) return [];
|
|
6557
|
+
const out = [];
|
|
6558
|
+
for (const name of readdirSync(root).filter((n) => /^[\w-]+$/.test(n)).sort().reverse()) {
|
|
6559
|
+
const dir = join(root, name), file = join(dir, "job.json");
|
|
6560
|
+
const live = jobs.get(name);
|
|
6561
|
+
if (live && live.summary().status === "running" && !live.summary().message) {
|
|
6562
|
+
const s = live.summary();
|
|
6563
|
+
out.push({
|
|
6564
|
+
id: s.id,
|
|
6565
|
+
title: s.title || (s.source === "url" ? s.url : ""),
|
|
6566
|
+
source: s.source,
|
|
6567
|
+
url: s.source === "url" ? s.url : void 0,
|
|
6568
|
+
startedAt: s.startedAt,
|
|
6569
|
+
duration: s.vod ? s.duration : s.recorded,
|
|
6570
|
+
live: !s.vod,
|
|
6571
|
+
running: true,
|
|
6572
|
+
conversationId: s.conversationId
|
|
6573
|
+
});
|
|
6574
|
+
continue;
|
|
6575
|
+
}
|
|
6576
|
+
let mtime = 0;
|
|
6577
|
+
try {
|
|
6578
|
+
mtime = statSync(file).mtimeMs;
|
|
6579
|
+
} catch {
|
|
6580
|
+
continue;
|
|
6581
|
+
}
|
|
6582
|
+
let hit = historyCache.get(name);
|
|
6583
|
+
if (!hit || hit.mtime !== mtime) {
|
|
6584
|
+
let item = null;
|
|
6585
|
+
try {
|
|
6586
|
+
const j = JSON.parse(readFileSync(file, "utf8"));
|
|
6587
|
+
if ((existsSync(join(dir, "recording.mp4")) || existsSync(join(dir, "hls", "index.m3u8"))) && j.status !== "error") {
|
|
6588
|
+
const device = j.opts.source === "device" ? (() => {
|
|
6589
|
+
try {
|
|
6590
|
+
return JSON.parse(j.opts.url).title;
|
|
6591
|
+
} catch {
|
|
6592
|
+
return "";
|
|
6593
|
+
}
|
|
6594
|
+
})() : "";
|
|
6595
|
+
let duration = j.duration;
|
|
6596
|
+
if (!duration) try {
|
|
6597
|
+
duration = [...readFileSync(join(dir, "hls", "index.m3u8"), "utf8").matchAll(/#EXTINF:([\d.]+)/g)].reduce((n, m) => n + Number(m[1]), 0) || void 0;
|
|
6598
|
+
} catch {}
|
|
6599
|
+
item = {
|
|
6600
|
+
id: j.id,
|
|
6601
|
+
title: j.title || device || (j.opts.source === "url" ? j.opts.url : ""),
|
|
6602
|
+
source: j.opts.source,
|
|
6603
|
+
url: j.opts.source === "url" ? j.opts.url : void 0,
|
|
6604
|
+
startedAt: j.startedAt,
|
|
6605
|
+
duration,
|
|
6606
|
+
live: !j.vod,
|
|
6607
|
+
running: false,
|
|
6608
|
+
conversationId: j.opts.conversationId
|
|
6609
|
+
};
|
|
6610
|
+
}
|
|
6611
|
+
} catch {}
|
|
6612
|
+
hit = {
|
|
6613
|
+
mtime,
|
|
6614
|
+
item
|
|
6615
|
+
};
|
|
6616
|
+
historyCache.set(name, hit);
|
|
6617
|
+
}
|
|
6618
|
+
if (hit.item) out.push(hit.item);
|
|
6619
|
+
}
|
|
6620
|
+
return out;
|
|
6621
|
+
}
|
|
6622
|
+
/** A small picture of a video for its history row (made once, kept in its folder). */
|
|
6623
|
+
async function thumbnail(id) {
|
|
6624
|
+
if (!/^[\w-]+$/.test(id)) return void 0;
|
|
6625
|
+
const dir = join(config.dataDir, "jobs", id);
|
|
6626
|
+
const out = join(dir, "thumb.jpg");
|
|
6627
|
+
if (existsSync(out)) return out;
|
|
6628
|
+
for (const cover of ["cover.jpg", "cover.png"]) if (existsSync(join(dir, cover))) return join(dir, cover);
|
|
6629
|
+
const src = existsSync(join(dir, "recording.mp4")) ? join(dir, "recording.mp4") : existsSync(join(dir, "hls", "index.m3u8")) ? join(dir, "hls", "index.m3u8") : void 0;
|
|
6630
|
+
if (!src) return void 0;
|
|
6631
|
+
if (await new Promise((resolve) => {
|
|
6632
|
+
execFile("ffmpeg", [
|
|
6633
|
+
"-v",
|
|
6634
|
+
"error",
|
|
6635
|
+
"-y",
|
|
6636
|
+
"-i",
|
|
6637
|
+
src,
|
|
6638
|
+
"-ss",
|
|
6639
|
+
"3",
|
|
6640
|
+
"-frames:v",
|
|
6641
|
+
"1",
|
|
6642
|
+
"-vf",
|
|
6643
|
+
"scale=320:-2",
|
|
6644
|
+
"-q:v",
|
|
6645
|
+
"5",
|
|
6646
|
+
out
|
|
6647
|
+
], { timeout: 2e4 }, (err) => resolve(!err && existsSync(out)));
|
|
6648
|
+
})) return out;
|
|
6649
|
+
return await new Promise((resolve) => {
|
|
6650
|
+
execFile("ffmpeg", [
|
|
6651
|
+
"-v",
|
|
6652
|
+
"error",
|
|
6653
|
+
"-y",
|
|
6654
|
+
"-i",
|
|
6655
|
+
src,
|
|
6656
|
+
"-frames:v",
|
|
6657
|
+
"1",
|
|
6658
|
+
"-vf",
|
|
6659
|
+
"scale=320:-2",
|
|
6660
|
+
"-q:v",
|
|
6661
|
+
"5",
|
|
6662
|
+
out
|
|
6663
|
+
], { timeout: 2e4 }, (err) => resolve(!err && existsSync(out)));
|
|
6664
|
+
}) ? out : void 0;
|
|
6665
|
+
}
|
|
6433
6666
|
function jobForConversation(conversationId) {
|
|
6434
6667
|
const id = byConversation.get(conversationId);
|
|
6435
6668
|
if (id === "") return void 0;
|
|
@@ -6637,6 +6870,8 @@ function apply(ctx, config) {
|
|
|
6637
6870
|
if (!job) return send(res, 404, { error: "no such job" });
|
|
6638
6871
|
if (b.action === "pause") await job.pauseSubs();
|
|
6639
6872
|
else {
|
|
6873
|
+
const cur = job.summary();
|
|
6874
|
+
if ((typeof b.provider === "string" && b.provider !== cur.provider || typeof b.mode === "string" && modeOf(b.mode) !== cur.mode) && cur.subs?.state === "running") await job.pauseSubs();
|
|
6640
6875
|
const opts = {};
|
|
6641
6876
|
if (Array.isArray(b.tgtLangs)) opts.tgtLangs = b.tgtLangs.map(String).slice(0, 4);
|
|
6642
6877
|
if (typeof b.srcLang === "string") opts.srcLang = b.srcLang;
|
|
@@ -6890,6 +7125,57 @@ function apply(ctx, config) {
|
|
|
6890
7125
|
saveAs: typeof b.saveAs === "string" ? b.saveAs.replace(/[\\/:*?"<>|]+/g, " ").slice(0, 150) : "audio"
|
|
6891
7126
|
}));
|
|
6892
7127
|
}
|
|
7128
|
+
if (route === "history") {
|
|
7129
|
+
const all = history();
|
|
7130
|
+
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
7131
|
+
const limit = Math.min(50, Math.max(1, Number(url.searchParams.get("limit")) || 5));
|
|
7132
|
+
return send(res, 200, {
|
|
7133
|
+
items: all.slice(offset, offset + limit),
|
|
7134
|
+
total: all.length
|
|
7135
|
+
});
|
|
7136
|
+
}
|
|
7137
|
+
if (route === "thumb") {
|
|
7138
|
+
const file = await thumbnail(url.searchParams.get("jobId") ?? "");
|
|
7139
|
+
if (!file) {
|
|
7140
|
+
res.writeHead(404);
|
|
7141
|
+
res.end();
|
|
7142
|
+
return;
|
|
7143
|
+
}
|
|
7144
|
+
res.writeHead(200, {
|
|
7145
|
+
"content-type": file.endsWith(".png") ? "image/png" : "image/jpeg",
|
|
7146
|
+
"cache-control": "max-age=3600"
|
|
7147
|
+
});
|
|
7148
|
+
createReadStream(file).pipe(res);
|
|
7149
|
+
return;
|
|
7150
|
+
}
|
|
7151
|
+
if (route === "lines" && req.method === "POST") {
|
|
7152
|
+
const b = await readJson(req);
|
|
7153
|
+
const job = jobs.get(String(b.jobId));
|
|
7154
|
+
if (!job) return send(res, 404, { error: "no such job" });
|
|
7155
|
+
const ok = b.action === "merge" ? job.mergeCue(Number(b.id)) : b.action === "split" ? job.splitCue(Number(b.id), String(b.key ?? "src"), Number(b.at)) : false;
|
|
7156
|
+
return send(res, ok ? 200 : 400, ok ? { ok: true } : { error: "cannot change that line" });
|
|
7157
|
+
}
|
|
7158
|
+
if (route === "reopen" && req.method === "POST") {
|
|
7159
|
+
const b = await readJson(req);
|
|
7160
|
+
const job = loadJob(String(b.jobId));
|
|
7161
|
+
const conv = String(b.conversationId ?? "");
|
|
7162
|
+
if (!job || !conv) return send(res, 404, { error: "no such video" });
|
|
7163
|
+
try {
|
|
7164
|
+
rmSync(join(job.summary().dir, "closed"), { force: true });
|
|
7165
|
+
} catch {}
|
|
7166
|
+
const prev = byConversation.get(conv);
|
|
7167
|
+
if (prev && prev !== job.id) {
|
|
7168
|
+
const other = jobs.get(prev);
|
|
7169
|
+
if (other && other.summary().status !== "running") byConversation.delete(conv);
|
|
7170
|
+
}
|
|
7171
|
+
job.rebind(conv);
|
|
7172
|
+
byConversation.set(conv, job.id);
|
|
7173
|
+
return send(res, 200, {
|
|
7174
|
+
...job.summary(),
|
|
7175
|
+
cues: job.cues,
|
|
7176
|
+
terms: job.terms
|
|
7177
|
+
});
|
|
7178
|
+
}
|
|
6893
7179
|
if (route === "preview") {
|
|
6894
7180
|
const job = jobs.get(url.searchParams.get("jobId") ?? "");
|
|
6895
7181
|
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.9.
|
|
3
|
+
"version": "0.9.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",
|