dsh-vidi 0.9.1 → 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 +204 -27
- package/lib/index.mjs +106 -1
- 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",
|
|
@@ -35416,9 +35423,15 @@ function History({ t, ui, onOpen }) {
|
|
|
35416
35423
|
}),
|
|
35417
35424
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35418
35425
|
className: "hl",
|
|
35419
|
-
children: items.map((v) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
35426
|
+
children: items.map((v) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35427
|
+
role: "button",
|
|
35428
|
+
tabIndex: 0,
|
|
35420
35429
|
className: "hi",
|
|
35421
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,
|
|
35422
35435
|
children: [
|
|
35423
35436
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
35424
35437
|
className: "th",
|
|
@@ -35450,6 +35463,14 @@ function History({ t, ui, onOpen }) {
|
|
|
35450
35463
|
v.running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35451
35464
|
className: "chip",
|
|
35452
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")
|
|
35453
35474
|
})
|
|
35454
35475
|
]
|
|
35455
35476
|
}, v.id))
|
|
@@ -35930,6 +35951,20 @@ function LiveView(props) {
|
|
|
35930
35951
|
}).catch(() => job));
|
|
35931
35952
|
};
|
|
35932
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
|
+
};
|
|
35933
35968
|
const changeMode = (m) => {
|
|
35934
35969
|
if (m === mode) return;
|
|
35935
35970
|
setMode(m);
|
|
@@ -36209,8 +36244,13 @@ function LiveView(props) {
|
|
|
36209
36244
|
return "camera";
|
|
36210
36245
|
}
|
|
36211
36246
|
};
|
|
36212
|
-
const openPast = async (v) => {
|
|
36247
|
+
const openPast = async (v, replay = false) => {
|
|
36213
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
|
+
}
|
|
36214
36254
|
try {
|
|
36215
36255
|
if (v.conversationId !== sessionId && props.onOpenVideo && await props.onOpenVideo(v.conversationId, v.id)) return;
|
|
36216
36256
|
const j = await post("reopen", {
|
|
@@ -36237,7 +36277,7 @@ function LiveView(props) {
|
|
|
36237
36277
|
pageMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(History, {
|
|
36238
36278
|
t,
|
|
36239
36279
|
ui,
|
|
36240
|
-
onOpen: (v) => void openPast(v)
|
|
36280
|
+
onOpen: (v, replay) => void openPast(v, replay)
|
|
36241
36281
|
}),
|
|
36242
36282
|
srcTab === "push" && armed?.rtmp && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36243
36283
|
className: "dshl-pushinfo",
|
|
@@ -36313,6 +36353,34 @@ function LiveView(props) {
|
|
|
36313
36353
|
openBench: setBench
|
|
36314
36354
|
} : void 0;
|
|
36315
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
|
+
};
|
|
36316
36384
|
const transcriptBox = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
36317
36385
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
36318
36386
|
className: "dshl-sidetabs",
|
|
@@ -36377,9 +36445,45 @@ function LiveView(props) {
|
|
|
36377
36445
|
children: [shown.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36378
36446
|
className: "dshl-muted pad",
|
|
36379
36447
|
children: emptyLines
|
|
36380
|
-
}), 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,
|
|
36381
36482
|
className: `dshl-line ${c.id === activeId ? "active" : ""} ${c.polished ? "ok" : "raw"}`,
|
|
36382
36483
|
onClick: () => player.current?.seek(c.start),
|
|
36484
|
+
onKeyDown: (e) => {
|
|
36485
|
+
if (e.key === "Enter") player.current?.seek(c.start);
|
|
36486
|
+
},
|
|
36383
36487
|
title: c.polished ? t("tr.polished") : t("tr.raw"),
|
|
36384
36488
|
children: [
|
|
36385
36489
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -36400,6 +36504,27 @@ function LiveView(props) {
|
|
|
36400
36504
|
className: "mark",
|
|
36401
36505
|
"aria-hidden": "true",
|
|
36402
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
|
+
})()]
|
|
36403
36528
|
})
|
|
36404
36529
|
]
|
|
36405
36530
|
}, c.id))]
|
|
@@ -36739,30 +36864,52 @@ function LiveView(props) {
|
|
|
36739
36864
|
t
|
|
36740
36865
|
}) })
|
|
36741
36866
|
},
|
|
36742
|
-
|
|
36743
|
-
id: "
|
|
36867
|
+
{
|
|
36868
|
+
id: "how",
|
|
36744
36869
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.timer, {}),
|
|
36745
|
-
title: t("
|
|
36746
|
-
label: t(`mode.${mode}`),
|
|
36747
|
-
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", {
|
|
36748
36873
|
className: "dshl-modemenu",
|
|
36749
36874
|
children: [
|
|
36750
|
-
"
|
|
36751
|
-
|
|
36752
|
-
|
|
36753
|
-
|
|
36754
|
-
|
|
36755
|
-
|
|
36756
|
-
|
|
36757
|
-
|
|
36758
|
-
|
|
36759
|
-
|
|
36760
|
-
|
|
36761
|
-
|
|
36762
|
-
|
|
36763
|
-
|
|
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
|
+
]
|
|
36764
36911
|
})
|
|
36765
|
-
}
|
|
36912
|
+
},
|
|
36766
36913
|
{
|
|
36767
36914
|
id: "restream",
|
|
36768
36915
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.push, {}),
|
|
@@ -37599,6 +37746,13 @@ const en = {
|
|
|
37599
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.",
|
|
37600
37747
|
"dev.denied": "DeepSeek Harness needs permission to use the {d}. Open System Settings, switch it on in the list, then retry.",
|
|
37601
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",
|
|
37602
37756
|
"hist.title": "Recent videos",
|
|
37603
37757
|
"hist.untitled": "Untitled video",
|
|
37604
37758
|
"hist.live": "live",
|
|
@@ -38053,6 +38207,13 @@ const zh = {
|
|
|
38053
38207
|
"dev.blocked": "设备没有传来画面和声音:DeepSeek Harness 需要系统授权。点「打开系统设置」,在列表里打开 DeepSeek Harness,再点重试。",
|
|
38054
38208
|
"dev.denied": "DeepSeek Harness 需要{d}权限。点「打开系统设置」,在列表里打开它,再点重试。",
|
|
38055
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": "回放",
|
|
38056
38217
|
"hist.title": "最近的视频",
|
|
38057
38218
|
"hist.untitled": "未命名视频",
|
|
38058
38219
|
"hist.live": "直播",
|
|
@@ -38743,6 +38904,21 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
|
|
|
38743
38904
|
.dshl-history .mm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--faint); font-size: 12px; }
|
|
38744
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; }
|
|
38745
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; }
|
|
38746
38922
|
.dshl-modemenu { display: grid; gap: 4px; width: 300px; max-width: 100%; }
|
|
38747
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; }
|
|
38748
38924
|
.dshl-modemenu .mm:hover { background: rgba(255,255,255,.08); }
|
|
@@ -39187,9 +39363,9 @@ function videoHandlers(sessions, workspaces, sessionId, fallbackTitle) {
|
|
|
39187
39363
|
}
|
|
39188
39364
|
},
|
|
39189
39365
|
/** Open a past video in its own conversation; false when that conversation is gone. */
|
|
39190
|
-
onOpenVideo: async (conversationId, jobId) => {
|
|
39366
|
+
onOpenVideo: async (conversationId, jobId, pending) => {
|
|
39191
39367
|
if (!conversationId || !sessions.has(conversationId)) return false;
|
|
39192
|
-
await fetch("/live/api/reopen", {
|
|
39368
|
+
if (!pending) await fetch("/live/api/reopen", {
|
|
39193
39369
|
method: "POST",
|
|
39194
39370
|
headers: { "content-type": "application/json" },
|
|
39195
39371
|
body: JSON.stringify({
|
|
@@ -39199,7 +39375,8 @@ function videoHandlers(sessions, workspaces, sessionId, fallbackTitle) {
|
|
|
39199
39375
|
}).catch(() => {});
|
|
39200
39376
|
await openPage(sessions, {
|
|
39201
39377
|
sessionId: conversationId,
|
|
39202
|
-
workspaces
|
|
39378
|
+
workspaces,
|
|
39379
|
+
pending
|
|
39203
39380
|
});
|
|
39204
39381
|
return true;
|
|
39205
39382
|
},
|
package/lib/index.mjs
CHANGED
|
@@ -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;
|
|
@@ -2860,6 +2886,76 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2860
2886
|
})).join("\n") + "\n");
|
|
2861
2887
|
this.emit("cues", this.cues);
|
|
2862
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
|
+
}
|
|
2863
2959
|
/** Append a finished cue (with its translation, if any) to cues.jsonl. */
|
|
2864
2960
|
record(cue) {
|
|
2865
2961
|
appendFileSync(join(this.deps.dir, "cues.jsonl"), JSON.stringify(cue) + "\n");
|
|
@@ -6774,6 +6870,8 @@ function apply(ctx, config) {
|
|
|
6774
6870
|
if (!job) return send(res, 404, { error: "no such job" });
|
|
6775
6871
|
if (b.action === "pause") await job.pauseSubs();
|
|
6776
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();
|
|
6777
6875
|
const opts = {};
|
|
6778
6876
|
if (Array.isArray(b.tgtLangs)) opts.tgtLangs = b.tgtLangs.map(String).slice(0, 4);
|
|
6779
6877
|
if (typeof b.srcLang === "string") opts.srcLang = b.srcLang;
|
|
@@ -7050,6 +7148,13 @@ function apply(ctx, config) {
|
|
|
7050
7148
|
createReadStream(file).pipe(res);
|
|
7051
7149
|
return;
|
|
7052
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
|
+
}
|
|
7053
7158
|
if (route === "reopen" && req.method === "POST") {
|
|
7054
7159
|
const b = await readJson(req);
|
|
7055
7160
|
const job = loadJob(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.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",
|