dsh-vidi 0.9.3 → 0.9.5
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 +100 -19
- package/lib/index.mjs +88 -5
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -35416,6 +35416,31 @@ function History({ t, ui, onOpen }) {
|
|
|
35416
35416
|
(0, react.useEffect)(() => {
|
|
35417
35417
|
load(0, 5);
|
|
35418
35418
|
}, []);
|
|
35419
|
+
const busy = items.some((v) => v.work);
|
|
35420
|
+
const shown = (0, react.useRef)(5);
|
|
35421
|
+
shown.current = Math.max(5, items.length);
|
|
35422
|
+
(0, react.useEffect)(() => {
|
|
35423
|
+
if (!busy) return;
|
|
35424
|
+
const timer = window.setInterval(() => void load(0, shown.current), 3e3);
|
|
35425
|
+
return () => window.clearInterval(timer);
|
|
35426
|
+
}, [busy]);
|
|
35427
|
+
const [halting, setHalting] = (0, react.useState)(null);
|
|
35428
|
+
const halt = async (v) => {
|
|
35429
|
+
setHalting(v.id);
|
|
35430
|
+
await fetch("/live/api/halt", {
|
|
35431
|
+
method: "POST",
|
|
35432
|
+
headers: { "content-type": "application/json" },
|
|
35433
|
+
body: JSON.stringify({ jobId: v.id })
|
|
35434
|
+
}).catch(() => {});
|
|
35435
|
+
await load(0, shown.current);
|
|
35436
|
+
setHalting(null);
|
|
35437
|
+
};
|
|
35438
|
+
const doing = (w) => [
|
|
35439
|
+
w.recording ? t("hist.recording") : "",
|
|
35440
|
+
w.loading ? t("hist.loading") : "",
|
|
35441
|
+
w.subs !== void 0 ? w.subs >= 0 ? t("hist.subsPct").replace("{p}", String(w.subs)) : t("hist.subs") : "",
|
|
35442
|
+
w.pushing ? t("hist.pushing") : ""
|
|
35443
|
+
].filter(Boolean);
|
|
35419
35444
|
if (!items.length) return null;
|
|
35420
35445
|
const from = (v) => v.source === "file" ? t("src.file") : v.source === "device" ? t("src.camera") : v.source === "push" ? t("src.push") : siteName(v.url ?? "") || (() => {
|
|
35421
35446
|
try {
|
|
@@ -35441,7 +35466,7 @@ function History({ t, ui, onOpen }) {
|
|
|
35441
35466
|
onKeyDown: (e) => {
|
|
35442
35467
|
if (e.key === "Enter") onOpen(v);
|
|
35443
35468
|
},
|
|
35444
|
-
title: v.live && v.source === "url" && !v.running ? t("hist.goLive") : void 0,
|
|
35469
|
+
title: v.live && v.source === "url" && !v.running && !v.work ? t("hist.goLive") : void 0,
|
|
35445
35470
|
children: [
|
|
35446
35471
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
35447
35472
|
className: "th",
|
|
@@ -35470,11 +35495,23 @@ function History({ t, ui, onOpen }) {
|
|
|
35470
35495
|
].filter(Boolean).join(" · ")
|
|
35471
35496
|
})]
|
|
35472
35497
|
}),
|
|
35473
|
-
v.
|
|
35498
|
+
v.work ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35499
|
+
className: "chip",
|
|
35500
|
+
children: doing(v.work).join(" · ")
|
|
35501
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35502
|
+
className: "dshl-link stop",
|
|
35503
|
+
disabled: halting === v.id,
|
|
35504
|
+
"data-tip": t("hist.stop.tip").replace("{w}", doing(v.work).join("、")),
|
|
35505
|
+
onClick: (e) => {
|
|
35506
|
+
e.stopPropagation();
|
|
35507
|
+
halt(v);
|
|
35508
|
+
},
|
|
35509
|
+
children: t(halting === v.id ? "hist.stopping" : "hist.stop")
|
|
35510
|
+
})] }) : v.running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35474
35511
|
className: "chip",
|
|
35475
35512
|
children: t("hist.running")
|
|
35476
|
-
}),
|
|
35477
|
-
v.live && v.source === "url" && !v.running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35513
|
+
}) : null,
|
|
35514
|
+
v.live && v.source === "url" && !v.running && !v.work && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35478
35515
|
className: "dshl-link replay",
|
|
35479
35516
|
onClick: (e) => {
|
|
35480
35517
|
e.stopPropagation();
|
|
@@ -36256,7 +36293,7 @@ function LiveView(props) {
|
|
|
36256
36293
|
};
|
|
36257
36294
|
const openPast = async (v, replay = false) => {
|
|
36258
36295
|
setError("");
|
|
36259
|
-
if (!replay && v.live && v.source === "url" && v.url && !v.running) {
|
|
36296
|
+
if (!replay && v.live && v.source === "url" && v.url && !v.running && !v.work) {
|
|
36260
36297
|
if (v.conversationId !== sessionId && props.onOpenVideo && await props.onOpenVideo(v.conversationId, v.id, v.url)) return;
|
|
36261
36298
|
start(v.url, "url");
|
|
36262
36299
|
return;
|
|
@@ -36518,21 +36555,23 @@ function LiveView(props) {
|
|
|
36518
36555
|
c.final && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
36519
36556
|
className: "tools",
|
|
36520
36557
|
onClick: (e) => e.stopPropagation(),
|
|
36521
|
-
children: [idx < shown.length - 1 && shown[idx + 1]?.final && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
36558
|
+
children: [idx < shown.length - 1 && shown[idx + 1]?.final && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
36522
36559
|
className: "ghost",
|
|
36523
|
-
|
|
36560
|
+
"data-tip": t("lines.merge"),
|
|
36561
|
+
"aria-label": t("lines.merge"),
|
|
36524
36562
|
onClick: () => void mergeLine(c.id),
|
|
36525
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.merge, {})
|
|
36563
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.merge, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("lines.mergeShort") })]
|
|
36526
36564
|
}), (() => {
|
|
36527
36565
|
const k = shownLines.find((x) => (lineText(c, x) ?? "").length > 1) ?? "src";
|
|
36528
|
-
return Array.from(lineText(c, k) ?? c.src).length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
36566
|
+
return Array.from(lineText(c, k) ?? c.src).length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
36529
36567
|
className: "ghost",
|
|
36530
|
-
|
|
36568
|
+
"data-tip": t("lines.split"),
|
|
36569
|
+
"aria-label": t("lines.split"),
|
|
36531
36570
|
onClick: () => setSplitting({
|
|
36532
36571
|
id: c.id,
|
|
36533
36572
|
key: lineKey(k)
|
|
36534
36573
|
}),
|
|
36535
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.scissors, {})
|
|
36574
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.scissors, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("lines.splitShort") })]
|
|
36536
36575
|
});
|
|
36537
36576
|
})()]
|
|
36538
36577
|
})
|
|
@@ -37060,6 +37099,10 @@ function SubsButton(props) {
|
|
|
37060
37099
|
style: { ["--p"]: `${pct ?? 0}%` },
|
|
37061
37100
|
children: [
|
|
37062
37101
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: "dot" }),
|
|
37102
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
37103
|
+
className: "pz",
|
|
37104
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.pause, {})
|
|
37105
|
+
}),
|
|
37063
37106
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
37064
37107
|
className: "now",
|
|
37065
37108
|
children: pct !== void 0 ? t("subs.progress").replace("{p}", String(pct)) : t("subs.progressLive").replace("{t}", CLOCK(at))
|
|
@@ -37078,7 +37121,14 @@ function SubsButton(props) {
|
|
|
37078
37121
|
className: "dshl-subsbtn",
|
|
37079
37122
|
onClick: onResume,
|
|
37080
37123
|
title: t("subs.resume.tip").replace("{t}", CLOCK(at)),
|
|
37081
|
-
children: [
|
|
37124
|
+
children: [
|
|
37125
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
37126
|
+
className: "pz on",
|
|
37127
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.play, {})
|
|
37128
|
+
}),
|
|
37129
|
+
t("subs.resume"),
|
|
37130
|
+
pct !== void 0 ? ` · ${pct}%` : ""
|
|
37131
|
+
]
|
|
37082
37132
|
});
|
|
37083
37133
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
37084
37134
|
className: "dshl-subsbtn",
|
|
@@ -37758,8 +37808,10 @@ const en = {
|
|
|
37758
37808
|
"dev.and": " and ",
|
|
37759
37809
|
"how.title": "How subtitles are made",
|
|
37760
37810
|
"how.more": "More services and keys are in Settings.",
|
|
37761
|
-
"lines.merge": "Join
|
|
37762
|
-
"lines.split": "
|
|
37811
|
+
"lines.merge": "Join this line and the next into one",
|
|
37812
|
+
"lines.split": "Break this line in two: then click where it should break",
|
|
37813
|
+
"lines.mergeShort": "Join",
|
|
37814
|
+
"lines.splitShort": "Split",
|
|
37763
37815
|
"lines.splitHint": "Click where the line should break.",
|
|
37764
37816
|
"hist.goLive": "Watch live again",
|
|
37765
37817
|
"hist.replay": "Replay",
|
|
@@ -37767,6 +37819,14 @@ const en = {
|
|
|
37767
37819
|
"hist.untitled": "Untitled video",
|
|
37768
37820
|
"hist.live": "live",
|
|
37769
37821
|
"hist.running": "Live now",
|
|
37822
|
+
"hist.recording": "Recording",
|
|
37823
|
+
"hist.loading": "Loading",
|
|
37824
|
+
"hist.subs": "Making subtitles",
|
|
37825
|
+
"hist.subsPct": "Subtitles {p}%",
|
|
37826
|
+
"hist.pushing": "Pushing out",
|
|
37827
|
+
"hist.stop": "Stop",
|
|
37828
|
+
"hist.stopping": "Stopping…",
|
|
37829
|
+
"hist.stop.tip": "Stop: {w}",
|
|
37770
37830
|
"hist.more": "More ({n})",
|
|
37771
37831
|
"set.spoken.tip": "The language people speak in this video; Auto detects it.",
|
|
37772
37832
|
"rs.mode": "Subtitle mode of the push-out",
|
|
@@ -38219,8 +38279,10 @@ const zh = {
|
|
|
38219
38279
|
"dev.and": "和",
|
|
38220
38280
|
"how.title": "字幕怎么做",
|
|
38221
38281
|
"how.more": "更多服务和 key 在设置里。",
|
|
38222
|
-
"lines.merge": "
|
|
38223
|
-
"lines.split": "
|
|
38282
|
+
"lines.merge": "把这一句和下一句合成一句",
|
|
38283
|
+
"lines.split": "把这一句拆成两句:点了以后,再点文字里要断开的位置",
|
|
38284
|
+
"lines.mergeShort": "合并",
|
|
38285
|
+
"lines.splitShort": "拆开",
|
|
38224
38286
|
"lines.splitHint": "点一下要断开的位置。",
|
|
38225
38287
|
"hist.goLive": "回到直播",
|
|
38226
38288
|
"hist.replay": "回放",
|
|
@@ -38228,6 +38290,14 @@ const zh = {
|
|
|
38228
38290
|
"hist.untitled": "未命名视频",
|
|
38229
38291
|
"hist.live": "直播",
|
|
38230
38292
|
"hist.running": "直播中",
|
|
38293
|
+
"hist.recording": "录制中",
|
|
38294
|
+
"hist.loading": "加载中",
|
|
38295
|
+
"hist.subs": "字幕制作中",
|
|
38296
|
+
"hist.subsPct": "字幕 {p}%",
|
|
38297
|
+
"hist.pushing": "推流中",
|
|
38298
|
+
"hist.stop": "停止",
|
|
38299
|
+
"hist.stopping": "停止中…",
|
|
38300
|
+
"hist.stop.tip": "停止:{w}",
|
|
38231
38301
|
"hist.more": "更多(还有 {n} 个)",
|
|
38232
38302
|
"set.spoken.tip": "视频里说的是哪种语言;自动 = 自己识别。",
|
|
38233
38303
|
"rs.mode": "推流用的字幕模式",
|
|
@@ -38859,7 +38929,12 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
|
|
|
38859
38929
|
@container (max-width: 640px) { .dshl-subsbtn .now { display: none; } .dshl-subsbtn.running:not(:hover) .short { display: inline; } }
|
|
38860
38930
|
.dshl-subsbtn.running:hover .now { display: none; }
|
|
38861
38931
|
.dshl-subsbtn.running:hover .act { display: inline; }
|
|
38862
|
-
.dshl-subsbtn
|
|
38932
|
+
.dshl-subsbtn .pz { display: none; margin: 0 -2px 0 -4px; }
|
|
38933
|
+
.dshl-subsbtn .pz svg { display: block; width: 15px; height: 15px; }
|
|
38934
|
+
.dshl-subsbtn .pz.on { display: inline-flex; }
|
|
38935
|
+
/* Hover on a running one: the pause sign stands where the pulsing dot was. */
|
|
38936
|
+
.dshl-subsbtn.running:hover .dot { display: none; }
|
|
38937
|
+
.dshl-subsbtn.running:hover .pz { display: inline-flex; }
|
|
38863
38938
|
@container (max-width: 520px) { .dshl-subsbtn { margin: 0 2px; padding: 0 9px; } }
|
|
38864
38939
|
.dshl-starter { position: relative; display: inline-flex; }
|
|
38865
38940
|
.dshl-startpop { position: absolute; left: 50%; bottom: calc(100% + 26px); z-index: 6; display: grid; gap: 10px; width: 290px; padding: 12px 12px 14px; transform: translateX(-50%);
|
|
@@ -38912,6 +38987,9 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
|
|
|
38912
38987
|
.dshl-history .tx { display: grid; gap: 3px; min-width: 0; flex: 1; }
|
|
38913
38988
|
.dshl-history .tt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; font-weight: 550; }
|
|
38914
38989
|
.dshl-history .mm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--faint); font-size: 12px; }
|
|
38990
|
+
.dshl-history .stop { flex: none; padding: 4px 6px; font-size: 12.5px; color: var(--faint); }
|
|
38991
|
+
.dshl-history .stop:hover { color: var(--live); }
|
|
38992
|
+
.dshl-history .stop:disabled { opacity: .5; }
|
|
38915
38993
|
.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; }
|
|
38916
38994
|
.dshl-history .more { margin: 8px 0 0 6px; font-size: 12.5px; }
|
|
38917
38995
|
.dshl-modemenu .mh { margin: 6px 4px 2px; color: rgba(255,255,255,.55); font-size: 11.5px; }
|
|
@@ -38921,8 +38999,11 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
|
|
|
38921
38999
|
.dshl-line { position: relative; }
|
|
38922
39000
|
.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); }
|
|
38923
39001
|
.dshl-line:hover .tools, .dshl-line:focus-within .tools { display: flex; }
|
|
38924
|
-
.dshl-line .tools .ghost { display:
|
|
38925
|
-
.dshl-line .tools svg { width:
|
|
39002
|
+
.dshl-line .tools .ghost { position: relative; display: flex; align-items: center; gap: 4px; height: 24px; padding: 0 7px 0 5px; border: 0; border-radius: 5px; background: none; color: var(--muted); font-size: 12px; white-space: nowrap; cursor: pointer; }
|
|
39003
|
+
.dshl-line .tools svg { width: 14px; height: 14px; flex: none; }
|
|
39004
|
+
/* Says what a button does the moment the pointer is on it (the system tooltip takes a second). */
|
|
39005
|
+
.dshl-history .stop { position: relative; }
|
|
39006
|
+
.dshl-history .stop[data-tip]:hover::after, .dshl-line .tools .ghost[data-tip]:hover::after { content: attr(data-tip); position: absolute; top: calc(100% + 6px); right: 0; z-index: 5; width: max-content; max-width: 220px; padding: 5px 8px; border-radius: 6px; background: var(--ink); color: var(--bg); font-size: 12px; line-height: 1.45; white-space: normal; text-align: left; pointer-events: none; }
|
|
38926
39007
|
.dshl-line .tools .ghost:hover { background: var(--hover); color: var(--ink); }
|
|
38927
39008
|
.dshl-line.splitting { cursor: default; background: color-mix(in srgb, var(--accent) 8%, transparent); }
|
|
38928
39009
|
.dshl-line .cutline .ch { cursor: col-resize; border-left: 1px solid transparent; }
|
package/lib/index.mjs
CHANGED
|
@@ -2021,6 +2021,28 @@ function cutNear(cs, ratio) {
|
|
|
2021
2021
|
}
|
|
2022
2022
|
return best > 0 ? best : Math.max(1, Math.min(n - 1, target));
|
|
2023
2023
|
}
|
|
2024
|
+
/**
|
|
2025
|
+
* A line meant to be in `lang` that is written in another script: live interpretation now and then
|
|
2026
|
+
* passes the speaker's words through untranslated (Chinese in a Japanese line). Latin words (names,
|
|
2027
|
+
* brands) never count; a Japanese line has kana, a Chinese one none, a Korean one hangul.
|
|
2028
|
+
*/
|
|
2029
|
+
function wrongScript(text, lang) {
|
|
2030
|
+
const n = (re) => (text.match(re) ?? []).length;
|
|
2031
|
+
const han = n(/[\u3400-\u9fff]/g), kana = n(/[\u3040-\u30ff]/g), hangul = n(/[\uac00-\ud7af\u1100-\u11ff]/g);
|
|
2032
|
+
const letters = n(/\p{L}/gu);
|
|
2033
|
+
const base = lang.split("-")[0];
|
|
2034
|
+
if (base === "ja") return kana === 0 && (han >= 4 || /[是吧嗯么们这那的呢吗啊呃对没还就说几]/.test(text)) || hangul >= 2;
|
|
2035
|
+
if (base === "zh") return kana >= 2 || hangul >= 2;
|
|
2036
|
+
if (base === "ko") return hangul === 0 && han + kana >= 3;
|
|
2037
|
+
const cjk = han + kana + hangul;
|
|
2038
|
+
return cjk >= 3 && cjk > letters * .3;
|
|
2039
|
+
}
|
|
2040
|
+
/** When a video was first opened: its folder is named after that moment (UTC); older saves could carry a later time. */
|
|
2041
|
+
function openedAt(id, saved) {
|
|
2042
|
+
const named = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})/.exec(id);
|
|
2043
|
+
const fromName = named ? Date.parse(`${named[1]}T${named[2]}:${named[3]}:${named[4]}Z`) : NaN;
|
|
2044
|
+
return Number.isFinite(fromName) ? fromName : typeof saved === "number" ? saved : void 0;
|
|
2045
|
+
}
|
|
2024
2046
|
var LiveJob = class LiveJob extends EventEmitter {
|
|
2025
2047
|
opts;
|
|
2026
2048
|
deps;
|
|
@@ -2033,6 +2055,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2033
2055
|
status = "resolving";
|
|
2034
2056
|
message;
|
|
2035
2057
|
title;
|
|
2058
|
+
/** When it was first opened (kept across restores, so the history keeps its place). */
|
|
2036
2059
|
startedAt = Date.now();
|
|
2037
2060
|
cues = [];
|
|
2038
2061
|
ffmpeg;
|
|
@@ -2175,6 +2198,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2175
2198
|
const j = JSON.parse(readFileSync(join(dir, "job.json"), "utf8"));
|
|
2176
2199
|
const job = new LiveJob(j.id, j.opts, deps);
|
|
2177
2200
|
job.title = j.title;
|
|
2201
|
+
job.startedAt = openedAt(j.id, j.startedAt) ?? job.startedAt;
|
|
2178
2202
|
job.vod = !!j.vod;
|
|
2179
2203
|
job.duration = j.duration;
|
|
2180
2204
|
job.captureDone = true;
|
|
@@ -2213,6 +2237,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2213
2237
|
state: j.subs?.state === "done" ? "done" : at > 0 ? "paused" : "off",
|
|
2214
2238
|
at
|
|
2215
2239
|
};
|
|
2240
|
+
setTimeout(() => job.repairEchoes(), 3e3);
|
|
2216
2241
|
if (!existsSync(join(dir, "recording.mp4"))) job.remux();
|
|
2217
2242
|
return job;
|
|
2218
2243
|
} catch {
|
|
@@ -2849,20 +2874,49 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2849
2874
|
this.emit("cue", cue);
|
|
2850
2875
|
if (cue.final) {
|
|
2851
2876
|
this.deps.glossary.notice(cue.src);
|
|
2852
|
-
|
|
2877
|
+
const echoed = this.echoOf(cue);
|
|
2878
|
+
if (echoed) this.translate(cue, echoed);
|
|
2879
|
+
else if (this.opts.mode !== "polished" && !this.provider?.translates && cue.tr === void 0 && this.targets().length) this.translate(cue);
|
|
2853
2880
|
else {
|
|
2854
2881
|
this.record(cue);
|
|
2855
2882
|
this.polisher.add(cue);
|
|
2856
2883
|
}
|
|
2857
2884
|
}
|
|
2858
2885
|
}
|
|
2859
|
-
|
|
2886
|
+
/**
|
|
2887
|
+
* Translations of a line that are in the wrong language: taken out (the line shows without them
|
|
2888
|
+
* until the proper one comes), and the text to translate from returned (the line's source, or the
|
|
2889
|
+
* echoed text itself when the line has none of its own: its source sits with the next line).
|
|
2890
|
+
*/
|
|
2891
|
+
echoOf(cue) {
|
|
2892
|
+
let from;
|
|
2893
|
+
for (const t of this.targets()) {
|
|
2894
|
+
const x = cue.tr?.[t];
|
|
2895
|
+
if (!x || !wrongScript(x, t)) continue;
|
|
2896
|
+
from ??= cue.src.trim() || x;
|
|
2897
|
+
delete cue.tr[t];
|
|
2898
|
+
}
|
|
2899
|
+
if (from && cue.tr && !Object.keys(cue.tr).length) cue.tr = void 0;
|
|
2900
|
+
return from;
|
|
2901
|
+
}
|
|
2902
|
+
/** Lines saved with a translation in the wrong language (before this check existed): translate them again. */
|
|
2903
|
+
repairEchoes() {
|
|
2904
|
+
for (const cue of this.cues) {
|
|
2905
|
+
if (!cue.final || cue.edited) continue;
|
|
2906
|
+
const from = this.echoOf(cue);
|
|
2907
|
+
if (from) {
|
|
2908
|
+
this.emit("cue", cue);
|
|
2909
|
+
this.translate(cue, from);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
translate(cue, text = cue.src) {
|
|
2860
2914
|
const context = this.cues.filter((c) => c.final && c.id < cue.id).slice(-6);
|
|
2861
2915
|
this.pendingTranslations++;
|
|
2862
2916
|
this.translating = this.translating.then(async () => {
|
|
2863
2917
|
try {
|
|
2864
2918
|
const ref = this.siteText(cue.start, cue.end).map((x) => `[${x.lang}${x.auto ? ", auto" : ""}] ${x.text}`).join("\n");
|
|
2865
|
-
const tr = await this.deps.translator.translate(this.targets(),
|
|
2919
|
+
const tr = await this.deps.translator.translate(this.targets(), text, context, this.deps.glossary.forPrompt(), void 0, ref);
|
|
2866
2920
|
if (tr) {
|
|
2867
2921
|
cue.tr = {
|
|
2868
2922
|
...cue.tr,
|
|
@@ -6611,7 +6665,7 @@ function apply(ctx, config) {
|
|
|
6611
6665
|
title: j.title || device || (j.opts.source === "url" ? j.opts.url : ""),
|
|
6612
6666
|
source: j.opts.source,
|
|
6613
6667
|
url: j.opts.source === "url" ? j.opts.url : void 0,
|
|
6614
|
-
startedAt: j.startedAt,
|
|
6668
|
+
startedAt: openedAt(j.id, j.startedAt),
|
|
6615
6669
|
duration,
|
|
6616
6670
|
live: !j.vod,
|
|
6617
6671
|
running: false,
|
|
@@ -6627,7 +6681,26 @@ function apply(ctx, config) {
|
|
|
6627
6681
|
}
|
|
6628
6682
|
if (hit.item) out.push(hit.item);
|
|
6629
6683
|
}
|
|
6630
|
-
|
|
6684
|
+
for (const item of out) {
|
|
6685
|
+
const j = jobs.get(String(item.id));
|
|
6686
|
+
if (!j) continue;
|
|
6687
|
+
const w = workOf(j);
|
|
6688
|
+
if (w) item.work = w;
|
|
6689
|
+
}
|
|
6690
|
+
return [...out.filter((i) => i.work), ...out.filter((i) => !i.work)];
|
|
6691
|
+
}
|
|
6692
|
+
/** Still working: recording a live stream, loading a video, making subtitles, pushing out. */
|
|
6693
|
+
function workOf(j) {
|
|
6694
|
+
const s = j.summary();
|
|
6695
|
+
const ok = !s.message;
|
|
6696
|
+
const total = s.vod ? s.duration : s.status === "running" ? void 0 : s.recorded || s.duration;
|
|
6697
|
+
const w = {
|
|
6698
|
+
recording: s.status === "running" && !s.vod && ok,
|
|
6699
|
+
loading: (s.status === "running" || s.status === "resolving") && !!s.vod && ok,
|
|
6700
|
+
subs: s.subs?.state === "running" ? total ? Math.min(99, Math.round(s.subs.at / total * 100)) : -1 : void 0,
|
|
6701
|
+
pushing: !!s.restream?.running
|
|
6702
|
+
};
|
|
6703
|
+
return w.recording || w.loading || w.subs !== void 0 || w.pushing ? w : void 0;
|
|
6631
6704
|
}
|
|
6632
6705
|
/** A small picture of a video for its history row (made once, kept in its folder). */
|
|
6633
6706
|
async function thumbnail(id) {
|
|
@@ -6810,6 +6883,16 @@ function apply(ctx, config) {
|
|
|
6810
6883
|
} catch {}
|
|
6811
6884
|
return send(res, 200, { ok: true });
|
|
6812
6885
|
}
|
|
6886
|
+
if (route === "halt" && req.method === "POST") {
|
|
6887
|
+
const b = await readJson(req);
|
|
6888
|
+
const job = jobs.get(String(b.jobId));
|
|
6889
|
+
if (!job) return send(res, 404, { error: "no such job" });
|
|
6890
|
+
const s = job.summary();
|
|
6891
|
+
if (s.subs?.state === "running") await job.pauseSubs();
|
|
6892
|
+
if (s.restream?.running) await job.stopRestream();
|
|
6893
|
+
if ((s.status === "running" || s.status === "resolving") && !s.vod) await job.stop();
|
|
6894
|
+
return send(res, 200, { ok: true });
|
|
6895
|
+
}
|
|
6813
6896
|
if (route === "stop" && req.method === "POST") {
|
|
6814
6897
|
const b = await readJson(req);
|
|
6815
6898
|
const job = jobs.get(String(b.jobId));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vidi",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
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",
|