dsh-vidi 0.4.1 → 0.4.3
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 +73 -5
- package/lib/index.mjs +36 -17
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -656,6 +656,42 @@ function ChatPanel(props) {
|
|
|
656
656
|
});
|
|
657
657
|
}
|
|
658
658
|
|
|
659
|
+
// src/client/Guard.tsx
|
|
660
|
+
/**
|
|
661
|
+
* Keeps a companion plugin's part of the page (a tab, a player menu, a
|
|
662
|
+
* workbench) from taking the whole video page down: if it fails, only its
|
|
663
|
+
* own area says so, with a way out, and the video keeps playing.
|
|
664
|
+
*/
|
|
665
|
+
var Guard = class extends react.Component {
|
|
666
|
+
state = {};
|
|
667
|
+
static getDerivedStateFromError(error) {
|
|
668
|
+
return { error };
|
|
669
|
+
}
|
|
670
|
+
componentDidCatch(error) {
|
|
671
|
+
console.error(`[dsh-vidi] ${this.props.name} failed`, error);
|
|
672
|
+
}
|
|
673
|
+
render() {
|
|
674
|
+
if (!this.state.error) return this.props.children;
|
|
675
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
676
|
+
className: "dshl-guard",
|
|
677
|
+
role: "alert",
|
|
678
|
+
children: [
|
|
679
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: this.props.message }),
|
|
680
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: this.state.error.message }),
|
|
681
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
682
|
+
className: "dshl-btn small",
|
|
683
|
+
onClick: () => this.setState({ error: void 0 }),
|
|
684
|
+
children: "↻"
|
|
685
|
+
}), this.props.onClose && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
686
|
+
className: "dshl-btn small",
|
|
687
|
+
onClick: this.props.onClose,
|
|
688
|
+
children: this.props.closeLabel
|
|
689
|
+
})] })
|
|
690
|
+
]
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
659
695
|
// src/client/langs.ts
|
|
660
696
|
/** Language names for the UI, by interface language. */
|
|
661
697
|
const NAMES = {
|
|
@@ -35250,7 +35286,11 @@ function LiveView(props) {
|
|
|
35250
35286
|
}),
|
|
35251
35287
|
job && view && extPanels.filter((p) => p.id === tab).map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35252
35288
|
className: "dshl-panel dshl-ext",
|
|
35253
|
-
children:
|
|
35289
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Guard, {
|
|
35290
|
+
name: `tab ${p.id}`,
|
|
35291
|
+
message: t("ext.failed"),
|
|
35292
|
+
children: p.render(view)
|
|
35293
|
+
})
|
|
35254
35294
|
}, p.id)),
|
|
35255
35295
|
tab === "glossary" && job && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GlossaryPanel, {
|
|
35256
35296
|
jobId: job.id,
|
|
@@ -35479,7 +35519,11 @@ function LiveView(props) {
|
|
|
35479
35519
|
id: `ext:${m.id}`,
|
|
35480
35520
|
icon: m.icon,
|
|
35481
35521
|
title: m.title(ui),
|
|
35482
|
-
content: view ?
|
|
35522
|
+
content: view ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Guard, {
|
|
35523
|
+
name: `menu ${m.id}`,
|
|
35524
|
+
message: t("ext.failed"),
|
|
35525
|
+
children: m.render(view, () => player.current?.closeMenu())
|
|
35526
|
+
}) : null
|
|
35483
35527
|
})),
|
|
35484
35528
|
{
|
|
35485
35529
|
id: "download",
|
|
@@ -35535,7 +35579,13 @@ function LiveView(props) {
|
|
|
35535
35579
|
className: "dshl-side",
|
|
35536
35580
|
children: activeBench && view ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35537
35581
|
className: "dshl-benchbox",
|
|
35538
|
-
children:
|
|
35582
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Guard, {
|
|
35583
|
+
name: `workbench ${activeBench.id}`,
|
|
35584
|
+
message: t("ext.failed"),
|
|
35585
|
+
onClose: () => setBench(null),
|
|
35586
|
+
closeLabel: t("ext.close"),
|
|
35587
|
+
children: activeBench.render(view)
|
|
35588
|
+
}, activeBench.id)
|
|
35539
35589
|
}) : pageMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChatPanel, {
|
|
35540
35590
|
face: chatFace,
|
|
35541
35591
|
source: chatSource,
|
|
@@ -35582,6 +35632,10 @@ function SubsButton(props) {
|
|
|
35582
35632
|
className: "now",
|
|
35583
35633
|
children: pct !== void 0 ? t("subs.progress").replace("{p}", String(pct)) : t("subs.progressLive").replace("{t}", CLOCK(at))
|
|
35584
35634
|
}),
|
|
35635
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35636
|
+
className: "short",
|
|
35637
|
+
children: pct !== void 0 ? `${pct}%` : CLOCK(at)
|
|
35638
|
+
}),
|
|
35585
35639
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35586
35640
|
className: "act",
|
|
35587
35641
|
children: t("subs.pause")
|
|
@@ -36292,6 +36346,8 @@ const en = {
|
|
|
36292
36346
|
"player.polished": "This line is polished",
|
|
36293
36347
|
"page.open": "Vidi video",
|
|
36294
36348
|
"page.failed": "The video page could not open: {e}",
|
|
36349
|
+
"ext.failed": "This part (from another plugin) ran into an error. The video keeps playing; restarting DSH usually fixes it after an update.",
|
|
36350
|
+
"ext.close": "Back to chat",
|
|
36295
36351
|
"side.subs": "Subtitles {p}%",
|
|
36296
36352
|
"side.subsLive": "Subtitling",
|
|
36297
36353
|
"side.recording": "Recording",
|
|
@@ -36614,6 +36670,8 @@ const zh = {
|
|
|
36614
36670
|
"player.polished": "这句字幕已精修",
|
|
36615
36671
|
"page.open": "Vidi 视频",
|
|
36616
36672
|
"page.failed": "视频页打不开:{e}",
|
|
36673
|
+
"ext.failed": "这一块(来自其他插件)出错了。视频照常播放;刚更新过插件的话,重启一下 DSH 一般就好。",
|
|
36674
|
+
"ext.close": "回到对话",
|
|
36617
36675
|
"side.subs": "字幕 {p}%",
|
|
36618
36676
|
"side.subsLive": "生成字幕中",
|
|
36619
36677
|
"side.recording": "录制中",
|
|
@@ -36897,6 +36955,10 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, s
|
|
|
36897
36955
|
.dshl.page .dshl-side { position: static; height: 100%; max-height: none; }
|
|
36898
36956
|
/* Workbench open: the video and its subtitles keep a column on the left; the bench takes the rest. */
|
|
36899
36957
|
.dshl.page.bench .dshl-main { grid-template-columns: clamp(340px, 32vw, 520px) minmax(0, 1fr); }
|
|
36958
|
+
.dshl-guard { display: grid; gap: 8px; margin: 12px; padding: 12px 14px; border: 1px solid color-mix(in srgb, #d1434b 35%, var(--line)); border-radius: 10px; color: var(--ink); font-size: 13px; }
|
|
36959
|
+
.dshl-guard p { margin: 0; line-height: 1.6; }
|
|
36960
|
+
.dshl-guard code { color: var(--muted); font-size: 12px; white-space: pre-wrap; word-break: break-word; }
|
|
36961
|
+
.dshl-guard div { display: flex; gap: 8px; }
|
|
36900
36962
|
.dshl-benchbox { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain; }
|
|
36901
36963
|
.dshl-under { display: flex; flex-direction: column; flex: 1; min-height: 210px; overflow: hidden; border: 1px solid var(--line); border-radius: 12px; background: var(--l1); }
|
|
36902
36964
|
.dshl-under .dshl-sidetabs { align-items: center; padding: 0 12px 0 10px; }
|
|
@@ -36992,7 +37054,12 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, s
|
|
|
36992
37054
|
.dshl-ctrlrow .pill svg { flex: none; width: 18px; height: 18px; }
|
|
36993
37055
|
.dshl-ctrlrow .pill .txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
36994
37056
|
.dshl-ctrlrow .pill .caret { flex: none; width: 5px; height: 5px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; opacity: .8; transform: translateY(1px) rotate(-135deg); }
|
|
36995
|
-
@container (max-width: 640px) {
|
|
37057
|
+
@container (max-width: 640px) {
|
|
37058
|
+
.dshl-ctrlrow .pill .txt, .dshl-ctrlrow .pill .caret { display: none; } .dshl-ctrlrow .pill { padding: 0 6px; }
|
|
37059
|
+
/* Narrow player (a workbench beside it, a phone): fewer words, no volume slider (the mute button stays). */
|
|
37060
|
+
.dshl-ctrlrow .vol, .dshl-ctrlrow .theater { display: none; }
|
|
37061
|
+
.dshl-ctrlrow .ic { width: 32px; height: 32px; }
|
|
37062
|
+
}
|
|
36996
37063
|
@container (max-width: 520px) {
|
|
36997
37064
|
.dshl-ctrlrow { gap: 2px; }
|
|
36998
37065
|
.dshl-ctrlrow .vol, .dshl-ctrlrow .mute, .dshl-ctrlrow .theater { display: none; }
|
|
@@ -37008,7 +37075,8 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, s
|
|
|
37008
37075
|
.dshl-subsbtn:hover { filter: brightness(1.08); }
|
|
37009
37076
|
.dshl-subsbtn.running { background: linear-gradient(to right, rgba(127,176,255,.45) var(--p, 0%), rgba(255,255,255,.14) var(--p, 0%)); font-weight: 500; }
|
|
37010
37077
|
.dshl-subsbtn.running .dot { width: 7px; height: 7px; border-radius: 50%; background: #ff5a5f; animation: dshl-pulse 1.6s ease-in-out infinite; }
|
|
37011
|
-
.dshl-subsbtn.running .act { display: none; }
|
|
37078
|
+
.dshl-subsbtn.running .act, .dshl-subsbtn .short { display: none; }
|
|
37079
|
+
@container (max-width: 640px) { .dshl-subsbtn .now { display: none; } .dshl-subsbtn.running:not(:hover) .short { display: inline; } }
|
|
37012
37080
|
.dshl-subsbtn.running:hover .now { display: none; }
|
|
37013
37081
|
.dshl-subsbtn.running:hover .act { display: inline; }
|
|
37014
37082
|
.dshl-subsbtn.running:hover .dot { width: 8px; height: 8px; border-radius: 1px; background: #fff; animation: none; }
|
package/lib/index.mjs
CHANGED
|
@@ -1693,6 +1693,8 @@ const AHEAD_MAX_LINES = 10;
|
|
|
1693
1693
|
*
|
|
1694
1694
|
* Events: 'state' (JobSummary), 'cue' (Cue — new or revised).
|
|
1695
1695
|
*/
|
|
1696
|
+
/** Drop the first `skip` seconds after decoding (an output -ss; empty when there is nothing to drop). */
|
|
1697
|
+
const seekAfter = (skip) => skip > .01 ? ["-ss", skip.toFixed(3)] : [];
|
|
1696
1698
|
var LiveJob = class LiveJob extends EventEmitter {
|
|
1697
1699
|
opts;
|
|
1698
1700
|
deps;
|
|
@@ -2594,7 +2596,14 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2594
2596
|
exportText(langs, format) {
|
|
2595
2597
|
return exportCues(this.cues, langs, format, this.title);
|
|
2596
2598
|
}
|
|
2597
|
-
/**
|
|
2599
|
+
/**
|
|
2600
|
+
* Best available picture source for time range [from, to]: original
|
|
2601
|
+
* download, finished recording, or recorded segments. `args` are the input
|
|
2602
|
+
* arguments; `skip` is how much to drop after decoding (output -ss). Files
|
|
2603
|
+
* seek exactly on input; live segments do not (a seek lands between
|
|
2604
|
+
* keyframes — YouTube live has one every 5 s — and decodes nothing), so they
|
|
2605
|
+
* are read from the start of the first segment and trimmed after decoding.
|
|
2606
|
+
*/
|
|
2598
2607
|
mediaFor(from, to) {
|
|
2599
2608
|
const original = readdirSync(this.deps.dir).find((n) => /^original\.(mp4|mkv|webm)$/.test(n));
|
|
2600
2609
|
if (original) return {
|
|
@@ -2604,7 +2613,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2604
2613
|
"-i",
|
|
2605
2614
|
join(this.deps.dir, original)
|
|
2606
2615
|
],
|
|
2607
|
-
|
|
2616
|
+
skip: 0
|
|
2608
2617
|
};
|
|
2609
2618
|
if (this.captureDone && existsSync(join(this.deps.dir, "recording.mp4"))) return {
|
|
2610
2619
|
args: [
|
|
@@ -2613,7 +2622,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2613
2622
|
"-i",
|
|
2614
2623
|
join(this.deps.dir, "recording.mp4")
|
|
2615
2624
|
],
|
|
2616
|
-
|
|
2625
|
+
skip: 0
|
|
2617
2626
|
};
|
|
2618
2627
|
const segs = this.segments();
|
|
2619
2628
|
const files = readFileSync(this.m3u8, "utf8").split("\n").filter((l) => l && !l.startsWith("#"));
|
|
@@ -2621,29 +2630,26 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2621
2630
|
let last = segs.findIndex((sg) => sg.start + sg.dur >= to);
|
|
2622
2631
|
if (last < 0) last = segs.length - 1;
|
|
2623
2632
|
if (!segs.length || first > last) throw new Error("that part has not been recorded yet");
|
|
2624
|
-
const list = files.slice(first, last + 1).map((f) => join(this.deps.dir, this.hlsName, f)).join("|");
|
|
2625
2633
|
return {
|
|
2626
|
-
args: [
|
|
2627
|
-
|
|
2628
|
-
String(from - segs[first].start),
|
|
2629
|
-
"-i",
|
|
2630
|
-
`concat:${list}`
|
|
2631
|
-
],
|
|
2632
|
-
offset: 0
|
|
2634
|
+
args: ["-i", `concat:${files.slice(first, last + 1).map((f) => join(this.deps.dir, this.hlsName, f)).join("|")}`],
|
|
2635
|
+
skip: Math.max(0, from - segs[first].start)
|
|
2633
2636
|
};
|
|
2634
2637
|
}
|
|
2635
2638
|
/** One small JPEG of the picture at media time `at`, for the model to look at (undefined for audio-only or on failure). */
|
|
2636
2639
|
async still(at) {
|
|
2637
2640
|
try {
|
|
2638
|
-
const { args } = this.mediaFor(at, at + 1);
|
|
2641
|
+
const { args, skip } = this.mediaFor(at, at + 1);
|
|
2639
2642
|
const { stdout } = await run$1("ffmpeg", [
|
|
2640
2643
|
"-v",
|
|
2641
2644
|
"error",
|
|
2642
2645
|
...args,
|
|
2646
|
+
...seekAfter(skip),
|
|
2643
2647
|
"-frames:v",
|
|
2644
2648
|
"1",
|
|
2645
2649
|
"-vf",
|
|
2646
2650
|
"scale=768:-2",
|
|
2651
|
+
"-pix_fmt",
|
|
2652
|
+
"yuvj420p",
|
|
2647
2653
|
"-q:v",
|
|
2648
2654
|
"5",
|
|
2649
2655
|
"-f",
|
|
@@ -2666,18 +2672,22 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2666
2672
|
const dir = join(this.deps.dir, "frames");
|
|
2667
2673
|
mkdirSync(dir, { recursive: true });
|
|
2668
2674
|
const out = join(dir, `frame_${at.toFixed(1).replace(".", "_")}.jpg`);
|
|
2669
|
-
const { args } = this.mediaFor(at, at + 1);
|
|
2675
|
+
const { args, skip } = this.mediaFor(at, at + 1);
|
|
2670
2676
|
await run$1("ffmpeg", [
|
|
2671
2677
|
"-v",
|
|
2672
2678
|
"error",
|
|
2673
2679
|
"-y",
|
|
2674
2680
|
...args,
|
|
2681
|
+
...seekAfter(skip),
|
|
2675
2682
|
"-frames:v",
|
|
2676
2683
|
"1",
|
|
2684
|
+
"-pix_fmt",
|
|
2685
|
+
"yuvj420p",
|
|
2677
2686
|
"-q:v",
|
|
2678
2687
|
"2",
|
|
2679
2688
|
out
|
|
2680
2689
|
], { timeout: 6e4 });
|
|
2690
|
+
if (!existsSync(out)) throw new Error("no picture at that moment");
|
|
2681
2691
|
return out;
|
|
2682
2692
|
}
|
|
2683
2693
|
/**
|
|
@@ -2690,14 +2700,14 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2690
2700
|
mkdirSync(dir, { recursive: true });
|
|
2691
2701
|
const name = `clip_${from.toFixed(1)}-${to.toFixed(1)}`.replace(/\./g, "_");
|
|
2692
2702
|
const out = join(dir, `${name}.mp4`);
|
|
2693
|
-
const { args } = this.mediaFor(from, to);
|
|
2703
|
+
const { args, skip } = this.mediaFor(from, to);
|
|
2694
2704
|
const subArgs = [];
|
|
2695
2705
|
const maps = [];
|
|
2696
2706
|
if (langs?.length) {
|
|
2697
2707
|
const inRange = this.cues.filter((c) => c.final && c.end > from && c.start < to).map((c) => ({
|
|
2698
2708
|
...c,
|
|
2699
|
-
start: Math.max(0, c.start - from),
|
|
2700
|
-
end: Math.min(to, c.end) - from
|
|
2709
|
+
start: Math.max(0, c.start - from) + skip,
|
|
2710
|
+
end: Math.min(to, c.end) - from + skip
|
|
2701
2711
|
}));
|
|
2702
2712
|
(langs.length > 1 ? [langs, ...langs.map((l) => [l])] : [langs]).forEach((ls, i) => {
|
|
2703
2713
|
const f = join(dir, `.${name}.${i}.srt`);
|
|
@@ -2712,6 +2722,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2712
2722
|
"-y",
|
|
2713
2723
|
...args,
|
|
2714
2724
|
...subArgs,
|
|
2725
|
+
...seekAfter(skip),
|
|
2715
2726
|
"-t",
|
|
2716
2727
|
String(to - from),
|
|
2717
2728
|
"-map",
|
|
@@ -2764,8 +2775,16 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2764
2775
|
rows
|
|
2765
2776
|
});
|
|
2766
2777
|
}
|
|
2778
|
+
const { args, skip } = this.mediaFor(from, to);
|
|
2767
2779
|
const pictures = /* @__PURE__ */ new Map();
|
|
2768
2780
|
const lines = ["ffconcat version 1.0"];
|
|
2781
|
+
if (skip > 0) {
|
|
2782
|
+
const blank = await drawSubtitles([], style, w, h);
|
|
2783
|
+
if (!blank) throw new Error("subtitles could not be drawn on this computer");
|
|
2784
|
+
const file = join(tmp, "blank.png");
|
|
2785
|
+
writeFileSync(file, blank);
|
|
2786
|
+
lines.push(`file '${file.replace(/'/g, "'\\''")}'`, `duration ${skip.toFixed(3)}`);
|
|
2787
|
+
}
|
|
2769
2788
|
for (const s of spans) {
|
|
2770
2789
|
let file = pictures.get(s.key);
|
|
2771
2790
|
if (!file) {
|
|
@@ -2780,7 +2799,6 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2780
2799
|
lines.push(`file '${pictures.get(spans.at(-1).key).replace(/'/g, "'\\''")}'`);
|
|
2781
2800
|
const list = join(tmp, "subs.ffconcat");
|
|
2782
2801
|
writeFileSync(list, lines.join("\n") + "\n");
|
|
2783
|
-
const { args } = this.mediaFor(from, to);
|
|
2784
2802
|
try {
|
|
2785
2803
|
await run$1("ffmpeg", [
|
|
2786
2804
|
"-v",
|
|
@@ -2793,6 +2811,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2793
2811
|
"0",
|
|
2794
2812
|
"-i",
|
|
2795
2813
|
list,
|
|
2814
|
+
...seekAfter(skip),
|
|
2796
2815
|
"-t",
|
|
2797
2816
|
String(to - from),
|
|
2798
2817
|
"-filter_complex",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vidi",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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",
|