dsh-vidi 0.3.0 → 0.4.0
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/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/lib/client.js +234 -101
- package/lib/index.mjs +199 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,16 +8,16 @@ A video tool for DeepSeek Harness: watch live streams, videos and podcasts with
|
|
|
8
8
|
|
|
9
9
|
- **Opens anything**: YouTube, Bilibili and other sites yt-dlp supports, Xiaoyuzhou podcast episodes, m3u8 / rtmp / srt streams, local files (drop them in), or a stream pushed from OBS.
|
|
10
10
|
- **Subtitles start by themselves** when a video opens; pause and resume any time. One or two languages (a main line and a second line); the site's own subtitles can be shown too, or used as a reference.
|
|
11
|
-
- **Two paces**: real-time shows lines as they are heard; polished arrives a little later with misheard words fixed from context.
|
|
11
|
+
- **Two paces**: real-time shows lines as they are heard; polished arrives a little later with misheard words fixed from context. In the subtitle list, polished lines carry a small tick; lines not polished yet are lighter.
|
|
12
12
|
- **The subtitle model looks at the video**: polishing uses the page's title, description and comments, and — when the chosen model reads images — a frame of that stretch. It also keeps a glossary of names and terms for the video.
|
|
13
13
|
- **Chat with DeepSeek beside the video**: grab this moment, cut this part, summarise, download the original — all in one conversation, with files saved in the video's folder.
|
|
14
14
|
- **Restream**: push the video with its subtitles drawn into the picture to Bilibili, Douyin, Kuaishou, WeChat Channels, Douyu, Huya, YouTube, Twitch or any RTMP/SRT address — several at once; each platform connects and reconnects on its own.
|
|
15
|
-
- **
|
|
15
|
+
- **Download and export**: the original or a video with subtitle tracks, just the sound (M4A), SRT / VTT subtitles, transcripts by line or paragraph.
|
|
16
16
|
- English and Chinese interface; works on phones.
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
20
|
-
In DSH, open **Plugins → Install third-party plugin** and enter `dsh-vidi`. A **
|
|
20
|
+
In DSH, open **Plugins → Install third-party plugin** and enter `dsh-vidi`. A **Vidi video** button appears in the sidebar (the video page takes the main area like a chat, and the sidebar opens and closes as usual); pasting a video link in a chat also offers to open it.
|
|
21
21
|
|
|
22
22
|
Needs DSH 0.2.2 or later.
|
|
23
23
|
|
package/README.zh-CN.md
CHANGED
|
@@ -8,16 +8,16 @@ DeepSeek Harness 里的视频工具:看直播、视频和播客,同时出任
|
|
|
8
8
|
|
|
9
9
|
- **打开任何来源**:YouTube、B 站等 yt-dlp 支持的网站,小宇宙节目,m3u8 / rtmp / srt 直播流,本地文件(直接拖进来),也可以用 OBS 推流过来。
|
|
10
10
|
- **自动出字幕**:视频一打开就开始,可以随时暂停、继续。单语、双语都行,主字幕和副字幕各选一种语言;网站自带的字幕也能直接显示,或者作为参考。
|
|
11
|
-
-
|
|
11
|
+
- **两种节奏**:实时模式边播边出;精修模式晚一点出,但会结合上下文把听错的词改对。字幕列表里精修过的句子带一个小勾,还没精修的颜色浅一些。
|
|
12
12
|
- **字幕模型会看画面**:精修时结合视频页面的标题、简介、评论,所选模型能看图时还会带上那一段的画面,顺便整理这期视频的词表(人名、术语)。
|
|
13
13
|
- **在视频旁边和 DeepSeek 对话**:截取此刻、剪下这段、总结要点、下载原片,都在同一个会话里完成,文件存在视频目录下。
|
|
14
14
|
- **转播**:把视频连同画面里的字幕推到 B站、抖音、快手、视频号、斗鱼、虎牙、YouTube、Twitch 或任意 RTMP/SRT 地址,可以同时推多个平台;每个平台各自连接、断了各自重连。
|
|
15
|
-
-
|
|
15
|
+
- **下载和导出**:原片或带字幕轨的视频、只要声音的音频(M4A)、SRT / VTT 字幕,逐句或按段落的文字稿。
|
|
16
16
|
- 中英文界面,手机上也能用。
|
|
17
17
|
|
|
18
18
|
## 安装
|
|
19
19
|
|
|
20
|
-
在 DSH 里打开 **插件 → 安装第三方插件**,填 `dsh-vidi`。装好后侧栏会出现
|
|
20
|
+
在 DSH 里打开 **插件 → 安装第三方插件**,填 `dsh-vidi`。装好后侧栏会出现 **Vidi 视频** 按钮,视频页和对话一样占主区域,侧栏照常收起、展开;在对话里贴视频链接,输入框上方也会出现「在视频里打开」。
|
|
21
21
|
|
|
22
22
|
需要 DSH 0.2.2 或更新的版本。
|
|
23
23
|
|
package/lib/client.js
CHANGED
|
@@ -247,6 +247,14 @@ const Icon = {
|
|
|
247
247
|
...P,
|
|
248
248
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" })
|
|
249
249
|
}),
|
|
250
|
+
check: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
251
|
+
viewBox: "0 0 24 24",
|
|
252
|
+
width: "12",
|
|
253
|
+
height: "12",
|
|
254
|
+
...P,
|
|
255
|
+
strokeWidth: 2.6,
|
|
256
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5 12.5l4.5 4.5L19 7.5" })
|
|
257
|
+
}),
|
|
250
258
|
plus: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
251
259
|
viewBox: "0 0 24 24",
|
|
252
260
|
width: "14",
|
|
@@ -33299,9 +33307,12 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
|
|
|
33299
33307
|
const idleTimer = (0, react.useRef)();
|
|
33300
33308
|
/** Where playback was, so a new source (quality switch) continues from there. */
|
|
33301
33309
|
const lastTime = (0, react.useRef)(0);
|
|
33302
|
-
(0, react.useImperativeHandle)(ref, () => ({
|
|
33303
|
-
|
|
33304
|
-
|
|
33310
|
+
(0, react.useImperativeHandle)(ref, () => ({
|
|
33311
|
+
seek: (time) => {
|
|
33312
|
+
if (video.current) video.current.currentTime = Math.max(0, time);
|
|
33313
|
+
},
|
|
33314
|
+
closeMenu: () => setMenu(null)
|
|
33315
|
+
}), []);
|
|
33305
33316
|
(0, react.useEffect)(() => {
|
|
33306
33317
|
const el = video.current;
|
|
33307
33318
|
if (!src || !el) return;
|
|
@@ -33836,6 +33847,11 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
|
|
|
33836
33847
|
className: "txt",
|
|
33837
33848
|
children: m.label
|
|
33838
33849
|
}),
|
|
33850
|
+
m.id === "cc" && ccOn && current?.polished && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
33851
|
+
className: "dshl-okmark",
|
|
33852
|
+
title: t("player.polished"),
|
|
33853
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.check, {})
|
|
33854
|
+
}),
|
|
33839
33855
|
m.label && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "caret" })
|
|
33840
33856
|
]
|
|
33841
33857
|
}, m.id)),
|
|
@@ -33890,9 +33906,10 @@ function registry() {
|
|
|
33890
33906
|
* itself stays in this package; companions only add.
|
|
33891
33907
|
*/
|
|
33892
33908
|
const vidiUi = {
|
|
33893
|
-
version:
|
|
33909
|
+
version: 2,
|
|
33894
33910
|
panels: registry(),
|
|
33895
|
-
playerMenus: registry()
|
|
33911
|
+
playerMenus: registry(),
|
|
33912
|
+
benches: registry()
|
|
33896
33913
|
};
|
|
33897
33914
|
|
|
33898
33915
|
// src/client/RestreamPanel.tsx
|
|
@@ -33974,7 +33991,7 @@ function RestreamPanel(props) {
|
|
|
33974
33991
|
return () => clearInterval(id);
|
|
33975
33992
|
}, [running]);
|
|
33976
33993
|
/** Known notes from the host, in the page's language. */
|
|
33977
|
-
const note = (m) => m === "ended" ? t("rs.m.ended") : m === "no-burn" ? t("rs.m.noBurn") : m;
|
|
33994
|
+
const note = (m) => m === "ended" ? t("rs.m.ended") : m === "no-burn" ? t("rs.m.noBurn") : m === "stalled" ? t("rs.m.stalled") : m;
|
|
33978
33995
|
const pname = (p) => t(`rs.p.${p}`);
|
|
33979
33996
|
const save = async () => {
|
|
33980
33997
|
if (!editing) return;
|
|
@@ -34589,7 +34606,7 @@ const siteName = (url) => /youtu\.?be/.test(url) ? "YouTube" : /bilibili|b23\.tv
|
|
|
34589
34606
|
const media = (dirId, file) => `/live/media/${encodeURIComponent(dirId)}/${encodeURIComponent(file)}`;
|
|
34590
34607
|
function LiveView(props) {
|
|
34591
34608
|
const { sessionId, t, chatFace, chatSource, onClose, pending, onPendingDone } = props;
|
|
34592
|
-
const pageMode = !!onClose;
|
|
34609
|
+
const pageMode = !!onClose || !!props.page;
|
|
34593
34610
|
const [input, setInput] = (0, react.useState)("");
|
|
34594
34611
|
const [lines, setLinesRaw] = (0, react.useState)(() => store.get("lines", [browserLang(), "src"]));
|
|
34595
34612
|
const setLines = (l) => {
|
|
@@ -34632,6 +34649,8 @@ function LiveView(props) {
|
|
|
34632
34649
|
const [tab, setTab] = (0, react.useState)("transcript");
|
|
34633
34650
|
const [query, setQuery] = (0, react.useState)("");
|
|
34634
34651
|
const [trMode, setTrMode] = (0, react.useState)("lines");
|
|
34652
|
+
/** Video page: the workbench shown in place of the chat (companion plugins), if any. */
|
|
34653
|
+
const [bench, setBench] = (0, react.useState)(null);
|
|
34635
34654
|
/** Reading view: which languages to show ('src' = original); empty = follow the subtitle lines. */
|
|
34636
34655
|
const [readPick, setReadPick] = (0, react.useState)([]);
|
|
34637
34656
|
const [now, setNow] = (0, react.useState)(0);
|
|
@@ -34846,19 +34865,6 @@ function LiveView(props) {
|
|
|
34846
34865
|
const stop = async () => {
|
|
34847
34866
|
if (job) await post("stop", { jobId: job.id }).catch(() => {});
|
|
34848
34867
|
};
|
|
34849
|
-
const downloadOnly = async () => {
|
|
34850
|
-
const c = classify(input);
|
|
34851
|
-
if (c?.source !== "url") return;
|
|
34852
|
-
try {
|
|
34853
|
-
await post("download", {
|
|
34854
|
-
conversationId: sessionId,
|
|
34855
|
-
url: c.url
|
|
34856
|
-
});
|
|
34857
|
-
refreshTasks();
|
|
34858
|
-
} catch (e) {
|
|
34859
|
-
setError(e.message);
|
|
34860
|
-
}
|
|
34861
|
-
};
|
|
34862
34868
|
const onFile = async (file) => {
|
|
34863
34869
|
setError("");
|
|
34864
34870
|
setUploading(0);
|
|
@@ -35036,7 +35042,6 @@ function LiveView(props) {
|
|
|
35036
35042
|
const statusDetail = job?.status === "running" && !job.message && !job.vod ? CLOCK(Math.max(0, (tick - job.startedAt) / 1e3)) : "";
|
|
35037
35043
|
const missing = deps.filter((d) => !d.ok && (d.name === "ffmpeg" || d.name === "yt-dlp" || (engine || "local") === "local" && d.name.startsWith("whisper")));
|
|
35038
35044
|
const needsModel = !hasTranslator && shownLines.some((l) => l !== "src");
|
|
35039
|
-
const canDownloadLink = classify(input)?.source === "url" && !/^(rtmps?|srt|rtsp|udp):/i.test(classify(input).url);
|
|
35040
35045
|
const hasMain = !!job && !(job.status === "error" && !job.files.length);
|
|
35041
35046
|
const emptyBox = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35042
35047
|
className: "dshl-drop",
|
|
@@ -35112,6 +35117,8 @@ function LiveView(props) {
|
|
|
35112
35117
|
const inline = pageMode && tab === "transcript" && !!job;
|
|
35113
35118
|
const extPanels = (0, react.useSyncExternalStore)(vidiUi.panels.subscribe, vidiUi.panels.get);
|
|
35114
35119
|
const extMenus = (0, react.useSyncExternalStore)(vidiUi.playerMenus.subscribe, vidiUi.playerMenus.get);
|
|
35120
|
+
const extBenches = (0, react.useSyncExternalStore)(vidiUi.benches.subscribe, vidiUi.benches.get);
|
|
35121
|
+
const activeBench = pageMode && job ? extBenches.find((b) => b.id === bench) : void 0;
|
|
35115
35122
|
const view = job ? {
|
|
35116
35123
|
job,
|
|
35117
35124
|
cues,
|
|
@@ -35120,7 +35127,9 @@ function LiveView(props) {
|
|
|
35120
35127
|
seek: (s) => player.current?.seek(s),
|
|
35121
35128
|
style: subStyle,
|
|
35122
35129
|
lines: shownLines,
|
|
35123
|
-
ui
|
|
35130
|
+
ui,
|
|
35131
|
+
bench: activeBench?.id ?? null,
|
|
35132
|
+
openBench: setBench
|
|
35124
35133
|
} : void 0;
|
|
35125
35134
|
/** Subtitle lines and glossary: under the player on the video page, beside it in a conversation. */
|
|
35126
35135
|
const transcriptBox = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
@@ -35188,21 +35197,30 @@ function LiveView(props) {
|
|
|
35188
35197
|
className: "dshl-muted pad",
|
|
35189
35198
|
children: t("tr.empty")
|
|
35190
35199
|
}), shown.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
35191
|
-
className: `dshl-line ${c.id === activeId ? "active" : ""} ${c.polished ? "" : "raw"}`,
|
|
35200
|
+
className: `dshl-line ${c.id === activeId ? "active" : ""} ${c.polished ? "ok" : "raw"}`,
|
|
35192
35201
|
onClick: () => player.current?.seek(c.start),
|
|
35193
|
-
|
|
35194
|
-
|
|
35195
|
-
|
|
35196
|
-
|
|
35197
|
-
|
|
35198
|
-
|
|
35199
|
-
|
|
35200
|
-
|
|
35201
|
-
|
|
35202
|
-
|
|
35203
|
-
|
|
35202
|
+
title: c.polished ? t("tr.polished") : t("tr.raw"),
|
|
35203
|
+
children: [
|
|
35204
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35205
|
+
className: "ts",
|
|
35206
|
+
children: CLOCK(c.start)
|
|
35207
|
+
}),
|
|
35208
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35209
|
+
className: "txt",
|
|
35210
|
+
children: shownLines.map((k, i) => {
|
|
35211
|
+
const v = lineText(c, k);
|
|
35212
|
+
return v ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35213
|
+
className: i === 0 ? "main" : "sub",
|
|
35214
|
+
children: v
|
|
35215
|
+
}, k) : null;
|
|
35216
|
+
})
|
|
35217
|
+
}),
|
|
35218
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35219
|
+
className: "mark",
|
|
35220
|
+
"aria-hidden": "true",
|
|
35221
|
+
children: c.polished && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.check, {})
|
|
35204
35222
|
})
|
|
35205
|
-
|
|
35223
|
+
]
|
|
35206
35224
|
}, c.id))]
|
|
35207
35225
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35208
35226
|
className: "dshl-read",
|
|
@@ -35244,7 +35262,7 @@ function LiveView(props) {
|
|
|
35244
35262
|
})
|
|
35245
35263
|
] });
|
|
35246
35264
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35247
|
-
className: `dshl ${theater ? "theater" : ""} ${pageMode ? "page" : ""}`,
|
|
35265
|
+
className: `dshl ${theater && !activeBench ? "theater" : ""} ${pageMode ? "page" : ""} ${activeBench ? "bench" : ""}`,
|
|
35248
35266
|
"data-dsh-own-drop": "dsh-vidi",
|
|
35249
35267
|
onDragEnter: (e) => {
|
|
35250
35268
|
if (e.dataTransfer.types.includes("Files")) e.stopPropagation();
|
|
@@ -35315,12 +35333,6 @@ function LiveView(props) {
|
|
|
35315
35333
|
})
|
|
35316
35334
|
]
|
|
35317
35335
|
}),
|
|
35318
|
-
canDownloadLink && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35319
|
-
className: "dshl-btn icon",
|
|
35320
|
-
onClick: downloadOnly,
|
|
35321
|
-
title: t("in.download.tip"),
|
|
35322
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.download, {})
|
|
35323
|
-
}),
|
|
35324
35336
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35325
35337
|
className: "dshl-btn primary",
|
|
35326
35338
|
onClick: () => start(),
|
|
@@ -35376,13 +35388,6 @@ function LiveView(props) {
|
|
|
35376
35388
|
children: t("st.retry")
|
|
35377
35389
|
})]
|
|
35378
35390
|
}),
|
|
35379
|
-
tasks.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35380
|
-
className: "dshl-tasks",
|
|
35381
|
-
children: tasks.filter((x) => x.kind === "original").slice(0, 3).map((x) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskRow, {
|
|
35382
|
-
task: x,
|
|
35383
|
-
t
|
|
35384
|
-
}, x.id))
|
|
35385
|
-
}),
|
|
35386
35391
|
!hasMain && !pageMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
35387
35392
|
className: "dshl-empty",
|
|
35388
35393
|
children: emptyBox
|
|
@@ -35474,7 +35479,7 @@ function LiveView(props) {
|
|
|
35474
35479
|
id: `ext:${m.id}`,
|
|
35475
35480
|
icon: m.icon,
|
|
35476
35481
|
title: m.title(ui),
|
|
35477
|
-
content: view ? m.render(view, () =>
|
|
35482
|
+
content: view ? m.render(view, () => player.current?.closeMenu()) : null
|
|
35478
35483
|
})),
|
|
35479
35484
|
{
|
|
35480
35485
|
id: "download",
|
|
@@ -35528,7 +35533,10 @@ function LiveView(props) {
|
|
|
35528
35533
|
] })
|
|
35529
35534
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
|
|
35530
35535
|
className: "dshl-side",
|
|
35531
|
-
children:
|
|
35536
|
+
children: activeBench && view ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35537
|
+
className: "dshl-benchbox",
|
|
35538
|
+
children: activeBench.render(view)
|
|
35539
|
+
}) : pageMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChatPanel, {
|
|
35532
35540
|
face: chatFace,
|
|
35533
35541
|
source: chatSource,
|
|
35534
35542
|
now,
|
|
@@ -35729,36 +35737,6 @@ function UsagePill({ usage, t, ui }) {
|
|
|
35729
35737
|
})]
|
|
35730
35738
|
});
|
|
35731
35739
|
}
|
|
35732
|
-
function TaskRow({ task, t }) {
|
|
35733
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35734
|
-
className: `dshl-task ${task.status}`,
|
|
35735
|
-
children: [
|
|
35736
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.download, {}),
|
|
35737
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35738
|
-
className: "name",
|
|
35739
|
-
children: task.title ?? task.file
|
|
35740
|
-
}),
|
|
35741
|
-
task.status === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35742
|
-
className: "bar",
|
|
35743
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: { width: `${task.pct}%` } })
|
|
35744
|
-
}),
|
|
35745
|
-
task.status === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35746
|
-
className: "pct",
|
|
35747
|
-
children: t("dl.pct").replace("{p}", String(task.pct))
|
|
35748
|
-
}),
|
|
35749
|
-
task.status === "done" && task.file && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
35750
|
-
href: media(task.dirId, task.file),
|
|
35751
|
-
download: true,
|
|
35752
|
-
children: task.file
|
|
35753
|
-
}),
|
|
35754
|
-
task.status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35755
|
-
className: "bad",
|
|
35756
|
-
title: task.error,
|
|
35757
|
-
children: t("dl.failed")
|
|
35758
|
-
})
|
|
35759
|
-
]
|
|
35760
|
-
});
|
|
35761
|
-
}
|
|
35762
35740
|
/** Download: choose what (video or subtitles), then one button that says exactly what you get. */
|
|
35763
35741
|
function DownloadPanel(props) {
|
|
35764
35742
|
const { job, lines, tasks, sessionId, onTask, langName, siteChips, t } = props;
|
|
@@ -35781,6 +35759,13 @@ function DownloadPanel(props) {
|
|
|
35781
35759
|
].filter(Boolean).join(" · ");
|
|
35782
35760
|
const saveAs = `${title}(${[qLabel, withSubs ? subsLabel : ""].filter(Boolean).join(",")}).mp4`;
|
|
35783
35761
|
const task = tasks.find((x) => x.kind === "video" && x.saveAs === saveAs);
|
|
35762
|
+
const audioAs = `${title}(${t("dl.tab.audio")})`;
|
|
35763
|
+
const audioTask = tasks.find((x) => x.kind === "audio" && x.saveAs === audioAs);
|
|
35764
|
+
const goAudio = () => post("audio", {
|
|
35765
|
+
conversationId: sessionId,
|
|
35766
|
+
jobId: job.id,
|
|
35767
|
+
saveAs: audioAs
|
|
35768
|
+
}).then(onTask).catch(() => onTask());
|
|
35784
35769
|
const go = () => post("video", {
|
|
35785
35770
|
conversationId: sessionId,
|
|
35786
35771
|
jobId: job.id,
|
|
@@ -35800,15 +35785,23 @@ function DownloadPanel(props) {
|
|
|
35800
35785
|
className: "dshl-dl",
|
|
35801
35786
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35802
35787
|
className: "dshl-seg wide",
|
|
35803
|
-
children: [
|
|
35804
|
-
|
|
35805
|
-
|
|
35806
|
-
|
|
35807
|
-
|
|
35808
|
-
|
|
35809
|
-
|
|
35810
|
-
|
|
35811
|
-
|
|
35788
|
+
children: [
|
|
35789
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35790
|
+
className: tab === "video" ? "on" : "",
|
|
35791
|
+
onClick: () => setTab("video"),
|
|
35792
|
+
children: t("dl.tab.video")
|
|
35793
|
+
}),
|
|
35794
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35795
|
+
className: tab === "audio" ? "on" : "",
|
|
35796
|
+
onClick: () => setTab("audio"),
|
|
35797
|
+
children: t("dl.tab.audio")
|
|
35798
|
+
}),
|
|
35799
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35800
|
+
className: tab === "subs" ? "on" : "",
|
|
35801
|
+
onClick: () => setTab("subs"),
|
|
35802
|
+
children: t("dl.tab.subs")
|
|
35803
|
+
})
|
|
35804
|
+
]
|
|
35812
35805
|
}), tab === "video" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
35813
35806
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35814
35807
|
className: "grp",
|
|
@@ -35856,6 +35849,37 @@ function DownloadPanel(props) {
|
|
|
35856
35849
|
className: "err",
|
|
35857
35850
|
children: task.error
|
|
35858
35851
|
})
|
|
35852
|
+
] }) : tab === "audio" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
35853
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
35854
|
+
className: "dshl-dl-note",
|
|
35855
|
+
children: t("dl.audio.d")
|
|
35856
|
+
}),
|
|
35857
|
+
audioTask?.status === "running" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35858
|
+
className: "dshl-btn block progress",
|
|
35859
|
+
style: { ["--p"]: `${audioTask.pct}%` },
|
|
35860
|
+
children: audioTask.stage === "mux" ? t("dl.stage.audio") : t("dl.stage.audioDl").replace("{p}", String(audioTask.pct))
|
|
35861
|
+
}) : audioTask?.status === "done" && audioTask.file ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
35862
|
+
className: "dshl-btn block primary",
|
|
35863
|
+
href: media(audioTask.dirId, audioTask.file),
|
|
35864
|
+
download: `${audioAs}${audioTask.file.slice(audioTask.file.lastIndexOf("."))}`,
|
|
35865
|
+
children: [
|
|
35866
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.download, {}),
|
|
35867
|
+
" ",
|
|
35868
|
+
t("dl.save")
|
|
35869
|
+
]
|
|
35870
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
35871
|
+
className: "dshl-btn block primary",
|
|
35872
|
+
onClick: () => void goAudio(),
|
|
35873
|
+
children: [
|
|
35874
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.download, {}),
|
|
35875
|
+
" ",
|
|
35876
|
+
t("dl.go").replace("{what}", t("dl.audio.what"))
|
|
35877
|
+
]
|
|
35878
|
+
}),
|
|
35879
|
+
audioTask?.status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
35880
|
+
className: "err",
|
|
35881
|
+
children: audioTask.error
|
|
35882
|
+
})
|
|
35859
35883
|
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
35860
35884
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35861
35885
|
className: "grp",
|
|
@@ -36091,6 +36115,7 @@ const en = {
|
|
|
36091
36115
|
"rs.stopOne": "Stop this one",
|
|
36092
36116
|
"rs.join": "Add",
|
|
36093
36117
|
"rs.m.ended": "The video ended, so restreaming stopped.",
|
|
36118
|
+
"rs.m.stalled": "Nothing more to send (the recording stopped), so restreaming stopped.",
|
|
36094
36119
|
"rs.m.noBurn": "Subtitles could not be drawn on this computer; streaming without them.",
|
|
36095
36120
|
"rs.note": "Starts from where you are watching, with the subtitles you see. Keys stay on this computer.",
|
|
36096
36121
|
"rs.s.connecting": "Connecting",
|
|
@@ -36136,8 +36161,6 @@ const en = {
|
|
|
36136
36161
|
"st.ready": "Ready",
|
|
36137
36162
|
"in.start": "Start",
|
|
36138
36163
|
"in.stop": "Stop",
|
|
36139
|
-
"in.download": "Download",
|
|
36140
|
-
"in.download.tip": "Download the video only",
|
|
36141
36164
|
"in.file": "File",
|
|
36142
36165
|
"in.file.tip": "Open a video or audio file",
|
|
36143
36166
|
"in.push": "Push",
|
|
@@ -36210,6 +36233,11 @@ const en = {
|
|
|
36210
36233
|
"subs.progressLive": "Generating subtitles · {t}",
|
|
36211
36234
|
"dl.tab.video": "Video",
|
|
36212
36235
|
"dl.tab.subs": "Subtitles",
|
|
36236
|
+
"dl.tab.audio": "Audio",
|
|
36237
|
+
"dl.audio.d": "Just the sound, for podcasts, listening on the go or other transcription tools.",
|
|
36238
|
+
"dl.audio.what": "Audio",
|
|
36239
|
+
"dl.stage.audioDl": "Downloading audio {p}%",
|
|
36240
|
+
"dl.stage.audio": "Taking out the sound…",
|
|
36213
36241
|
"dl.quality": "Picture",
|
|
36214
36242
|
"dl.q.original": "Original",
|
|
36215
36243
|
"dl.q.original.d": "Best quality from the site",
|
|
@@ -36259,6 +36287,9 @@ const en = {
|
|
|
36259
36287
|
"chat.p.summary": "Summarise the key points of this video so far from the subtitles.",
|
|
36260
36288
|
"chat.p.download": "Download the original of this video.",
|
|
36261
36289
|
"page.back": "Back to chats",
|
|
36290
|
+
"tr.polished": "Polished",
|
|
36291
|
+
"tr.raw": "First pass; polishing comes a little later",
|
|
36292
|
+
"player.polished": "This line is polished",
|
|
36262
36293
|
"page.open": "Vidi video",
|
|
36263
36294
|
"page.failed": "The video page could not open: {e}",
|
|
36264
36295
|
"side.subs": "Subtitles {p}%",
|
|
@@ -36406,6 +36437,7 @@ const zh = {
|
|
|
36406
36437
|
"rs.stopOne": "停止这一路",
|
|
36407
36438
|
"rs.join": "加入",
|
|
36408
36439
|
"rs.m.ended": "视频播完了,转播已停止。",
|
|
36440
|
+
"rs.m.stalled": "没有新画面可推了(录制已停),转播已停止。",
|
|
36409
36441
|
"rs.m.noBurn": "这台电脑画不了字幕,先不带字幕推流。",
|
|
36410
36442
|
"rs.note": "从你正在看的位置开始,带着你看到的字幕。推流码只存在这台电脑上。",
|
|
36411
36443
|
"rs.s.connecting": "连接中",
|
|
@@ -36451,8 +36483,6 @@ const zh = {
|
|
|
36451
36483
|
"st.ready": "已就绪",
|
|
36452
36484
|
"in.start": "开始",
|
|
36453
36485
|
"in.stop": "停止",
|
|
36454
|
-
"in.download": "下载",
|
|
36455
|
-
"in.download.tip": "只下载视频",
|
|
36456
36486
|
"in.file": "文件",
|
|
36457
36487
|
"in.file.tip": "打开视频或音频文件",
|
|
36458
36488
|
"in.push": "推流",
|
|
@@ -36525,6 +36555,11 @@ const zh = {
|
|
|
36525
36555
|
"subs.progressLive": "正在生成字幕 · 已到 {t}",
|
|
36526
36556
|
"dl.tab.video": "视频",
|
|
36527
36557
|
"dl.tab.subs": "字幕",
|
|
36558
|
+
"dl.tab.audio": "音频",
|
|
36559
|
+
"dl.audio.d": "只要声音,适合当播客听,或交给别的转写工具。",
|
|
36560
|
+
"dl.audio.what": "音频",
|
|
36561
|
+
"dl.stage.audioDl": "正在下载音频 {p}%",
|
|
36562
|
+
"dl.stage.audio": "正在提取声音…",
|
|
36528
36563
|
"dl.quality": "画质",
|
|
36529
36564
|
"dl.q.original": "原片",
|
|
36530
36565
|
"dl.q.original.d": "网站最高画质",
|
|
@@ -36574,6 +36609,9 @@ const zh = {
|
|
|
36574
36609
|
"chat.p.summary": "根据字幕,总结这个视频到目前为止的要点。",
|
|
36575
36610
|
"chat.p.download": "下载这个视频的原片。",
|
|
36576
36611
|
"page.back": "返回对话",
|
|
36612
|
+
"tr.polished": "已精修",
|
|
36613
|
+
"tr.raw": "第一轮识别,稍后精修",
|
|
36614
|
+
"player.polished": "这句字幕已精修",
|
|
36577
36615
|
"page.open": "Vidi 视频",
|
|
36578
36616
|
"page.failed": "视频页打不开:{e}",
|
|
36579
36617
|
"side.subs": "字幕 {p}%",
|
|
@@ -36834,9 +36872,18 @@ const CSS = `
|
|
|
36834
36872
|
with them, and its empty space moves the window like the app's own top strip. Full screen and
|
|
36835
36873
|
the web page need none of this. */
|
|
36836
36874
|
.dshl-pagewrap { top: var(--dsh-frame-chrome-top, 0px); }
|
|
36837
|
-
html[data-platform='darwin']:not([data-fullscreen]) .dshl.page .dshl-top { padding: 4px 16px 8px 84px;
|
|
36838
|
-
html[data-platform='darwin']:not([data-fullscreen]) .dshl.page .dshl-top
|
|
36839
|
-
|
|
36875
|
+
html[data-platform='darwin']:not([data-fullscreen]) .dshl-pagewrap .dshl.page .dshl-top { padding: 4px 16px 8px 84px; }
|
|
36876
|
+
@media (max-width: 600px) { html[data-platform='darwin']:not([data-fullscreen]) .dshl-pagewrap .dshl.page .dshl-top { padding-left: 80px; } }
|
|
36877
|
+
/* Newer DS-Harness: the page is a main panel beside the app's sidebar. When the sidebar is closed
|
|
36878
|
+
the app puts its own controls (traffic lights, sidebar toggle, new chat) at the top-left and
|
|
36879
|
+
publishes how much room they take: the top bar starts after them. */
|
|
36880
|
+
.dshl-panelwrap { height: 100%; min-height: 0; background: var(--dsw-alias-bg-base, #fff); }
|
|
36881
|
+
.dshl-panelwrap .dshl.page .dshl-top { padding-left: max(20px, var(--dsh-frame-leading-clearance, 0px)); }
|
|
36882
|
+
/* Narrow windows (sidebar as a drawer): the app gives its opener a row of its own above the page. */
|
|
36883
|
+
[data-sidebar-drawer] .dshl-panelwrap .dshl.page .dshl-top { padding-left: 12px; }
|
|
36884
|
+
html[data-platform='darwin']:not([data-fullscreen]) .dshl-panelwrap .dshl.page .dshl-top { padding-top: 4px; padding-bottom: 8px; }
|
|
36885
|
+
html[data-platform='darwin'] .dshl.page .dshl-top { -webkit-app-region: drag; app-region: drag; }
|
|
36886
|
+
html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, select, .dshl-pick) { -webkit-app-region: no-drag; app-region: no-drag; }
|
|
36840
36887
|
.dshl.page { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
|
36841
36888
|
.dshl.page .dshl-main { flex: 1; min-height: 0; grid-template-columns: minmax(0, 1fr) clamp(320px, 30vw, 420px); align-items: stretch; padding-bottom: 16px; }
|
|
36842
36889
|
/* Video page: player on top, then the status row, then subtitles/glossary filling what is left; chat on the right. */
|
|
@@ -36844,6 +36891,9 @@ html[data-platform='darwin']:not([data-fullscreen]) .dshl.page .dshl-top :is(but
|
|
|
36844
36891
|
.dshl.page .dshl-col > .dshl-drop { flex: 1; aspect-ratio: auto; min-height: 0; }
|
|
36845
36892
|
.dshl.page .dshl-stage { flex: none; width: 100%; max-height: max(240px, calc(100cqh - 270px)); }
|
|
36846
36893
|
.dshl.page .dshl-side { position: static; height: 100%; max-height: none; }
|
|
36894
|
+
/* Workbench open: the video and its subtitles keep a column on the left; the bench takes the rest. */
|
|
36895
|
+
.dshl.page.bench .dshl-main { grid-template-columns: clamp(340px, 32vw, 520px) minmax(0, 1fr); }
|
|
36896
|
+
.dshl-benchbox { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain; }
|
|
36847
36897
|
.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); }
|
|
36848
36898
|
.dshl-under .dshl-sidetabs { align-items: center; padding: 0 12px 0 10px; }
|
|
36849
36899
|
.dshl-under .dshl-sidetabs > button { flex: none; padding: 0 14px; }
|
|
@@ -36855,6 +36905,9 @@ html[data-platform='darwin']:not([data-fullscreen]) .dshl.page .dshl-top :is(but
|
|
|
36855
36905
|
.dshl.page.theater .dshl-stage { max-height: 100cqh; }
|
|
36856
36906
|
@media (max-width: 1000px) {
|
|
36857
36907
|
.dshl.page { display: block; overflow: auto; }
|
|
36908
|
+
.dshl.page.bench .dshl-main { grid-template-columns: minmax(0, 1fr); }
|
|
36909
|
+
.dshl.page.bench .dshl-side { height: auto; min-height: 0; }
|
|
36910
|
+
.dshl-benchbox { overflow: visible; }
|
|
36858
36911
|
.dshl.page .dshl-main { grid-template-columns: minmax(0, 1fr); align-items: start; }
|
|
36859
36912
|
.dshl.page .dshl-col { container-type: normal; }
|
|
36860
36913
|
.dshl.page .dshl-col > .dshl-drop { min-height: 320px; aspect-ratio: 16 / 9; }
|
|
@@ -36863,6 +36916,10 @@ html[data-platform='darwin']:not([data-fullscreen]) .dshl.page .dshl-top :is(but
|
|
|
36863
36916
|
.dshl.page .dshl-side { height: 70vh; min-height: 420px; }
|
|
36864
36917
|
}
|
|
36865
36918
|
@media (max-width: 600px) {
|
|
36919
|
+
/* Tabs keep their row; the search, line/reading switch and copy move to one of their own. */
|
|
36920
|
+
.dshl-under .dshl-sidetabs { flex-wrap: wrap; row-gap: 0; }
|
|
36921
|
+
.dshl-tools.inline { flex: 1 0 100%; justify-content: flex-start; padding: 6px 0 8px; }
|
|
36922
|
+
.dshl-tools.inline .dshl-field { flex: 1 1 auto; }
|
|
36866
36923
|
.dshl-top { padding: 10px 12px; }
|
|
36867
36924
|
.dshl-bar { flex-wrap: nowrap; gap: 6px; }
|
|
36868
36925
|
.dshl-bar .dshl-field { flex: 1 1 0; height: 38px; padding: 0 4px 0 10px; gap: 6px; }
|
|
@@ -37054,6 +37111,7 @@ html[data-platform='darwin']:not([data-fullscreen]) .dshl.page .dshl-top :is(but
|
|
|
37054
37111
|
.dshl-dl { display: grid; gap: 14px; padding: 0 6px; }
|
|
37055
37112
|
.dshl-dl .grp { display: grid; gap: 8px; }
|
|
37056
37113
|
.dshl-dl .cap { color: var(--muted); font-size: 12px; font-weight: 550; }
|
|
37114
|
+
.dshl-dl-note { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.6; }
|
|
37057
37115
|
.dshl-dl .cards { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
37058
37116
|
.dshl-card-opt { display: grid; align-content: start; gap: 2px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--l1); color: var(--ink); text-align: left; cursor: pointer; }
|
|
37059
37117
|
.dshl-card-opt b { font-size: 13px; font-weight: 600; }
|
|
@@ -37110,7 +37168,7 @@ html[data-platform='darwin']:not([data-fullscreen]) .dshl.page .dshl-top :is(but
|
|
|
37110
37168
|
.dshl-lines { display: flex; flex-direction: column; gap: 2px; flex: 1; min-height: 0; margin: 0 -6px; overflow: auto; }
|
|
37111
37169
|
.dshl:not(.page) .dshl-lines { max-height: calc(100vh - 330px); }
|
|
37112
37170
|
@media (max-width: 1000px) { .dshl:not(.page) .dshl-lines { max-height: 60vh; } }
|
|
37113
|
-
.dshl-line { display: grid; grid-template-columns: 44px 1fr; gap: 6px; padding: 7px 6px; border: 0; border-radius: 8px; background: none; color: var(--ink); text-align: left; cursor: pointer; }
|
|
37171
|
+
.dshl-line { display: grid; grid-template-columns: 44px 1fr 14px; gap: 6px; padding: 7px 6px; border: 0; border-radius: 8px; background: none; color: var(--ink); text-align: left; cursor: pointer; }
|
|
37114
37172
|
.dshl-lines > .pad { padding: 16px 6px; }
|
|
37115
37173
|
.dshl-line:hover { background: var(--hover); }
|
|
37116
37174
|
.dshl-line.active { background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
@@ -37120,6 +37178,12 @@ html[data-platform='darwin']:not([data-fullscreen]) .dshl.page .dshl-top :is(but
|
|
|
37120
37178
|
.dshl-line .main { font-weight: 550; }
|
|
37121
37179
|
.dshl-line .sub { color: var(--muted); font-size: 13px; }
|
|
37122
37180
|
.dshl-line.raw .main, .dshl-line.raw .sub { opacity: .62; }
|
|
37181
|
+
/* Polished lines: a quiet tick at the end; first-pass lines stay lighter until polished. */
|
|
37182
|
+
.dshl-line .mark { display: flex; padding-top: 4px; color: var(--faint); opacity: .7; }
|
|
37183
|
+
.dshl-line.active .mark { color: var(--accent); }
|
|
37184
|
+
/* Player: a faint tick on the subtitle button while the line on screen is polished (explained on hover). */
|
|
37185
|
+
.dshl-okmark { display: flex; margin: 0 -2px 0 1px; color: rgba(255,255,255,.45); }
|
|
37186
|
+
.dshl-okmark svg { width: 9px; height: 9px; }
|
|
37123
37187
|
|
|
37124
37188
|
/* Reading (paragraphs by speaker) */
|
|
37125
37189
|
.dshl-read { display: grid; align-content: start; gap: 16px; flex: 1; min-height: 0; overflow: auto; padding: 2px 2px 12px; }
|
|
@@ -37276,6 +37340,26 @@ function ensureStyles() {
|
|
|
37276
37340
|
|
|
37277
37341
|
// src/client/VideoPage.tsx
|
|
37278
37342
|
const PAGE_SESSION = "dsh-live.pageSession";
|
|
37343
|
+
/** The id of the video page among DS-Harness's main panels (beside the conversation, Plugins…). */
|
|
37344
|
+
const PANEL_ID = "vidi";
|
|
37345
|
+
let panelHost;
|
|
37346
|
+
function setPanelHost(host) {
|
|
37347
|
+
panelHost = host;
|
|
37348
|
+
if (!host) return () => {};
|
|
37349
|
+
const sync = () => {
|
|
37350
|
+
const here = host.info.getSnapshot().activePanelId === PANEL_ID;
|
|
37351
|
+
if (pageStore.get().open !== here) pageStore.set({
|
|
37352
|
+
open: here,
|
|
37353
|
+
...here ? {} : { pending: void 0 }
|
|
37354
|
+
});
|
|
37355
|
+
};
|
|
37356
|
+
const off = host.info.subscribe(sync);
|
|
37357
|
+
return () => {
|
|
37358
|
+
off();
|
|
37359
|
+
if (panelHost === host) panelHost = void 0;
|
|
37360
|
+
};
|
|
37361
|
+
}
|
|
37362
|
+
const inPanel = () => !!panelHost;
|
|
37279
37363
|
const samePath = (a, b) => !!a && !!b && a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
|
|
37280
37364
|
/**
|
|
37281
37365
|
* The video page's own conversation: reused while it exists, created when not.
|
|
@@ -37325,6 +37409,16 @@ async function showPage(sessions, opts) {
|
|
|
37325
37409
|
const current = sessions.current();
|
|
37326
37410
|
const sid = opts.sessionId ?? await pageSession(sessions, opts.workspaces, opts.title ?? "Video");
|
|
37327
37411
|
if (sid !== current) sessions.open(sid);
|
|
37412
|
+
if (panelHost) {
|
|
37413
|
+
pageStore.set({
|
|
37414
|
+
open: true,
|
|
37415
|
+
sessionId: sid,
|
|
37416
|
+
returnTo: void 0,
|
|
37417
|
+
pending: opts.pending
|
|
37418
|
+
});
|
|
37419
|
+
panelHost.select(PANEL_ID);
|
|
37420
|
+
return;
|
|
37421
|
+
}
|
|
37328
37422
|
const prev = pageStore.get();
|
|
37329
37423
|
pageStore.set({
|
|
37330
37424
|
open: true,
|
|
@@ -37341,7 +37435,27 @@ function closePage(sessions) {
|
|
|
37341
37435
|
});
|
|
37342
37436
|
if (returnTo && sessions.has(returnTo)) sessions.open(returnTo);
|
|
37343
37437
|
}
|
|
37344
|
-
/**
|
|
37438
|
+
/** The video page as a main panel (newer DS-Harness): the app's sidebar and its toggle stay. */
|
|
37439
|
+
function VideoPanel(props) {
|
|
37440
|
+
const { sessions, t } = props;
|
|
37441
|
+
const page = (0, react.useSyncExternalStore)(pageStore.subscribe, pageStore.get);
|
|
37442
|
+
const face = (0, react.useSyncExternalStore)(sessions.subscribe, () => page.sessionId ? sessions.face(page.sessionId) : void 0);
|
|
37443
|
+
const chat = (0, react.useSyncExternalStore)(sessions.subscribe, () => page.sessionId ? sessions.chat(page.sessionId) : void 0);
|
|
37444
|
+
if (!page.sessionId) return null;
|
|
37445
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
37446
|
+
className: "dshl-panelwrap",
|
|
37447
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveView, {
|
|
37448
|
+
sessionId: page.sessionId,
|
|
37449
|
+
t,
|
|
37450
|
+
page: true,
|
|
37451
|
+
chatFace: face,
|
|
37452
|
+
chatSource: chat,
|
|
37453
|
+
pending: page.pending,
|
|
37454
|
+
onPendingDone: () => pageStore.set({ pending: void 0 })
|
|
37455
|
+
}, page.sessionId)
|
|
37456
|
+
});
|
|
37457
|
+
}
|
|
37458
|
+
/** Full-window video page (shell.overlay, older DS-Harness): player, subtitles, terms and the chat with DeepSeek. */
|
|
37345
37459
|
function VideoPage(props) {
|
|
37346
37460
|
const { sessions, t } = props;
|
|
37347
37461
|
const page = (0, react.useSyncExternalStore)(pageStore.subscribe, pageStore.get);
|
|
@@ -37352,7 +37466,7 @@ function VideoPage(props) {
|
|
|
37352
37466
|
role: "alert",
|
|
37353
37467
|
children: t("page.failed").replace("{e}", page.error)
|
|
37354
37468
|
});
|
|
37355
|
-
if (!page.open || !page.sessionId) return null;
|
|
37469
|
+
if (inPanel() || !page.open || !page.sessionId) return null;
|
|
37356
37470
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
37357
37471
|
className: "dshl-pagewrap",
|
|
37358
37472
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveView, {
|
|
@@ -37457,7 +37571,8 @@ function PasteChip(props) {
|
|
|
37457
37571
|
/**
|
|
37458
37572
|
* dsh-vidi browser half:
|
|
37459
37573
|
* - a "Video" tab beside Chat in every conversation (conversation.view);
|
|
37460
|
-
* - a "
|
|
37574
|
+
* - a "Vidi video" button in the sidebar footer that opens the video page: one of the app's
|
|
37575
|
+
* main panels where DS-Harness has them (ctx.layout), else a full-window page (shell.overlay);
|
|
37461
37576
|
* - a prompt above the composer when a video link is pasted (conversation.input.dock);
|
|
37462
37577
|
* - "show this video" requests from DeepSeek's live_open tool and the /video command.
|
|
37463
37578
|
*/
|
|
@@ -37553,6 +37668,24 @@ function apply(ctx) {
|
|
|
37553
37668
|
t
|
|
37554
37669
|
})
|
|
37555
37670
|
}, VideoPage));
|
|
37671
|
+
ctx.inject(["layout"], (c) => {
|
|
37672
|
+
c.effect(() => setPanelHost({
|
|
37673
|
+
select: (id) => c.layout.selectPanel(id),
|
|
37674
|
+
info: {
|
|
37675
|
+
getSnapshot: () => c.layout.panelInfo.getSnapshot(),
|
|
37676
|
+
subscribe: (fn) => c.layout.panelInfo.subscribe(fn)
|
|
37677
|
+
}
|
|
37678
|
+
}), "dsh-vidi: main panel host");
|
|
37679
|
+
c.slots.inject("main", () => c.slots.register({
|
|
37680
|
+
name: "main",
|
|
37681
|
+
key: PANEL_ID,
|
|
37682
|
+
locale: NS,
|
|
37683
|
+
inject: () => ({
|
|
37684
|
+
sessions: sessions(),
|
|
37685
|
+
t
|
|
37686
|
+
})
|
|
37687
|
+
}, VideoPanel));
|
|
37688
|
+
});
|
|
37556
37689
|
ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
37557
37690
|
name: "conversation.input.dock",
|
|
37558
37691
|
id: "dsh-vidi.paste",
|
package/lib/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir, networkInterfaces } from "node:os";
|
|
3
|
-
import { basename, extname, join, normalize, resolve, sep } from "node:path";
|
|
3
|
+
import { basename, dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { execFile, spawn } from "node:child_process";
|
|
6
6
|
import { promisify } from "node:util";
|
|
@@ -161,6 +161,16 @@ function defaultExports(targets) {
|
|
|
161
161
|
}
|
|
162
162
|
// src/glossary.ts
|
|
163
163
|
const MAX = 80;
|
|
164
|
+
/** Does the text mention the term (its correct form or a known wrong one)? Latin words match whole words only. */
|
|
165
|
+
function mentions(text, t) {
|
|
166
|
+
return [t.src, ...t.aliases ?? []].some((w) => {
|
|
167
|
+
const k = w.trim().toLowerCase();
|
|
168
|
+
if (!k) return false;
|
|
169
|
+
if (!/^[\x00-\x7f]+$/.test(k)) return text.includes(k);
|
|
170
|
+
const esc = k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
171
|
+
return new RegExp(`(^|[^a-z0-9])${esc}($|[^a-z0-9])`).test(text);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
164
174
|
function readTerms(file) {
|
|
165
175
|
if (!file || !existsSync(file)) return [];
|
|
166
176
|
try {
|
|
@@ -174,7 +184,9 @@ function readTerms(file) {
|
|
|
174
184
|
* holds terms confirmed by polished text, and gets corrected as polishing
|
|
175
185
|
* goes on). Used by recognition (correct spellings as hints), translation
|
|
176
186
|
* and the next polishing round. Users can correct or remove entries; their
|
|
177
|
-
* entries win and are kept in a shared library for later videos.
|
|
187
|
+
* entries win and are kept in a shared library for later videos. A library
|
|
188
|
+
* entry joins a video's glossary only once that video mentions it (title,
|
|
189
|
+
* description or subtitles), so one video's names never show up in another.
|
|
178
190
|
*/
|
|
179
191
|
var Glossary = class {
|
|
180
192
|
file;
|
|
@@ -183,20 +195,24 @@ var Glossary = class {
|
|
|
183
195
|
log;
|
|
184
196
|
library;
|
|
185
197
|
terms = [];
|
|
198
|
+
/** Library entries this video has not mentioned yet: not shown, not in any prompt. */
|
|
199
|
+
shelf = [];
|
|
186
200
|
constructor(file, targets, onChange, log, seed, library) {
|
|
187
201
|
this.file = file;
|
|
188
202
|
this.targets = targets;
|
|
189
203
|
this.onChange = onChange;
|
|
190
204
|
this.log = log;
|
|
191
205
|
this.library = library;
|
|
192
|
-
|
|
206
|
+
const lib = readTerms(library).map((t) => ({
|
|
193
207
|
...t,
|
|
194
|
-
origin:
|
|
208
|
+
origin: "user"
|
|
195
209
|
}));
|
|
196
|
-
|
|
210
|
+
const inLib = (t) => lib.some((x) => x.src.toLowerCase() === t.src.toLowerCase());
|
|
211
|
+
this.terms = readTerms(file).map((t) => ({
|
|
197
212
|
...t,
|
|
198
|
-
origin: "
|
|
199
|
-
});
|
|
213
|
+
origin: t.origin ?? "auto"
|
|
214
|
+
})).filter((t) => t.origin !== "user" || !inLib(t));
|
|
215
|
+
this.shelf = lib.filter((t) => !this.find(t.src));
|
|
200
216
|
for (const line of seed.split("\n").map((l) => l.trim()).filter(Boolean)) {
|
|
201
217
|
const [src, rest] = line.split(/\s*=\s*/);
|
|
202
218
|
if (src && !this.find(src)) this.terms.push({
|
|
@@ -206,6 +222,17 @@ var Glossary = class {
|
|
|
206
222
|
});
|
|
207
223
|
}
|
|
208
224
|
}
|
|
225
|
+
/** New text from this video (title, description, subtitle lines): bring in the library entries it mentions. */
|
|
226
|
+
notice(text) {
|
|
227
|
+
if (!this.shelf.length || !text) return;
|
|
228
|
+
const low = text.toLowerCase();
|
|
229
|
+
const hit = this.shelf.filter((t) => mentions(low, t) && !this.find(t.src));
|
|
230
|
+
if (!hit.length) return;
|
|
231
|
+
this.shelf = this.shelf.filter((t) => !hit.includes(t));
|
|
232
|
+
this.terms.push(...hit);
|
|
233
|
+
this.log(`glossary: ${hit.map((t) => t.src).join(", ")} from the library`);
|
|
234
|
+
this.save();
|
|
235
|
+
}
|
|
209
236
|
setTargets(targets) {
|
|
210
237
|
this.targets = targets;
|
|
211
238
|
}
|
|
@@ -304,6 +331,7 @@ var Glossary = class {
|
|
|
304
331
|
});
|
|
305
332
|
this.save();
|
|
306
333
|
const t = this.find(src);
|
|
334
|
+
this.shelf = this.shelf.filter((x) => x.src.toLowerCase() !== src.toLowerCase());
|
|
307
335
|
this.saveLibrary((lib) => [...lib.filter((x) => x.src.toLowerCase() !== src.toLowerCase()), {
|
|
308
336
|
...t,
|
|
309
337
|
origin: "user"
|
|
@@ -313,6 +341,7 @@ var Glossary = class {
|
|
|
313
341
|
const t = this.find(src);
|
|
314
342
|
if (!t) return;
|
|
315
343
|
this.terms = this.terms.filter((x) => x !== t);
|
|
344
|
+
this.shelf = this.shelf.filter((x) => x.src.toLowerCase() !== t.src.toLowerCase());
|
|
316
345
|
this.save();
|
|
317
346
|
this.saveLibrary((lib) => lib.filter((x) => x.src.toLowerCase() !== t.src.toLowerCase()));
|
|
318
347
|
}
|
|
@@ -1082,6 +1111,46 @@ function downloadOriginal(url, dir, browser, onProgress, signal) {
|
|
|
1082
1111
|
});
|
|
1083
1112
|
});
|
|
1084
1113
|
}
|
|
1114
|
+
/** Download only the sound of a page URL (best audio stream), next to the job files. Resolves to the saved file path. */
|
|
1115
|
+
function downloadAudio(url, dir, browser, onProgress) {
|
|
1116
|
+
return new Promise((resolve, reject) => {
|
|
1117
|
+
const args = [
|
|
1118
|
+
"--no-warnings",
|
|
1119
|
+
"--no-playlist",
|
|
1120
|
+
...cookieArgs(browser),
|
|
1121
|
+
"-f",
|
|
1122
|
+
"bestaudio[ext=m4a]/bestaudio/best",
|
|
1123
|
+
"--newline",
|
|
1124
|
+
"--progress-template",
|
|
1125
|
+
"download:%(progress._percent_str)s",
|
|
1126
|
+
"-o",
|
|
1127
|
+
join(dir, "audio-src.%(ext)s"),
|
|
1128
|
+
"--print",
|
|
1129
|
+
"after_move:filepath",
|
|
1130
|
+
url
|
|
1131
|
+
];
|
|
1132
|
+
const p = spawn("yt-dlp", args, { stdio: [
|
|
1133
|
+
"ignore",
|
|
1134
|
+
"pipe",
|
|
1135
|
+
"pipe"
|
|
1136
|
+
] });
|
|
1137
|
+
let out = "", err = "";
|
|
1138
|
+
p.stdout.on("data", (d) => {
|
|
1139
|
+
out += d.toString();
|
|
1140
|
+
const m = d.toString().match(/([\d.]+)%\s*$/m);
|
|
1141
|
+
if (m) onProgress(Math.min(99, Math.round(Number(m[1]))));
|
|
1142
|
+
});
|
|
1143
|
+
p.stderr.on("data", (d) => {
|
|
1144
|
+
err += d.toString();
|
|
1145
|
+
});
|
|
1146
|
+
p.on("error", reject);
|
|
1147
|
+
p.on("exit", (code) => {
|
|
1148
|
+
const file = out.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("/")).at(-1);
|
|
1149
|
+
if (code === 0 && file && existsSync(file)) resolve(file);
|
|
1150
|
+
else reject(new Error(ytdlpError({ stderr: err })));
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1085
1154
|
/** Keep the most recent downloaded original of a job dir, if any. */
|
|
1086
1155
|
function findOriginal(dir) {
|
|
1087
1156
|
if (!existsSync(dir)) return void 0;
|
|
@@ -1272,6 +1341,9 @@ var Restream = class extends EventEmitter {
|
|
|
1272
1341
|
blank;
|
|
1273
1342
|
startAt = 0;
|
|
1274
1343
|
spawnedAt = 0;
|
|
1344
|
+
watchdog;
|
|
1345
|
+
lastOut = 0;
|
|
1346
|
+
stuckSince = Date.now();
|
|
1275
1347
|
constructor(source, opts, targets, log) {
|
|
1276
1348
|
super();
|
|
1277
1349
|
this.source = source;
|
|
@@ -1415,6 +1487,18 @@ var Restream = class extends EventEmitter {
|
|
|
1415
1487
|
});
|
|
1416
1488
|
if (burn) this.feedSubtitles(w, h);
|
|
1417
1489
|
for (const p of this.pushes.values()) this.connect(p.target.id);
|
|
1490
|
+
this.watchdog = setInterval(() => {
|
|
1491
|
+
if (this.stopped) return;
|
|
1492
|
+
if (this.outTime > this.lastOut) {
|
|
1493
|
+
this.lastOut = this.outTime;
|
|
1494
|
+
this.stuckSince = Date.now();
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
if (Date.now() - this.stuckSince > 25e3) {
|
|
1498
|
+
this.log("restream: the encoder stopped producing");
|
|
1499
|
+
this.stop("stalled");
|
|
1500
|
+
}
|
|
1501
|
+
}, 5e3);
|
|
1418
1502
|
this.changed();
|
|
1419
1503
|
}
|
|
1420
1504
|
/** Subtitle pictures for media times from the start on, kept about a second ahead of the encoder. */
|
|
@@ -1562,6 +1646,7 @@ var Restream = class extends EventEmitter {
|
|
|
1562
1646
|
async stop(message) {
|
|
1563
1647
|
if (this.stopped) return;
|
|
1564
1648
|
this.stopped = true;
|
|
1649
|
+
clearInterval(this.watchdog);
|
|
1565
1650
|
process.off("exit", this.killAll);
|
|
1566
1651
|
clearInterval(this.subsTimer);
|
|
1567
1652
|
for (const p of this.pushes.values()) {
|
|
@@ -1666,6 +1751,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
1666
1751
|
still: (at) => this.still(at),
|
|
1667
1752
|
onLine: (cue, src, tr, speaker) => {
|
|
1668
1753
|
if (!this.cues.includes(cue)) return;
|
|
1754
|
+
this.deps.glossary.notice(src);
|
|
1669
1755
|
cue.src = src;
|
|
1670
1756
|
if (speaker) cue.speaker = speaker;
|
|
1671
1757
|
if (Object.keys(tr).length) cue.tr = {
|
|
@@ -1760,6 +1846,11 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
1760
1846
|
job.cues.push(...[...seen.values()].sort((a, b) => a.start - b.start));
|
|
1761
1847
|
job.nextId = Math.max(0, ...job.cues.map((c) => c.id)) + 1;
|
|
1762
1848
|
if (existsSync(join(dir, "brief.txt"))) job.brief = readFileSync(join(dir, "brief.txt"), "utf8");
|
|
1849
|
+
deps.glossary.notice([
|
|
1850
|
+
job.title ?? "",
|
|
1851
|
+
job.brief,
|
|
1852
|
+
...job.cues.map((c) => c.src)
|
|
1853
|
+
].join("\n"));
|
|
1763
1854
|
job.siteLang = j.siteLang;
|
|
1764
1855
|
const q = j;
|
|
1765
1856
|
if (q.height) job.height = q.height;
|
|
@@ -2301,6 +2392,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2301
2392
|
if (cue.final && key !== void 0) this.drafts.delete(key);
|
|
2302
2393
|
this.emit("cue", cue);
|
|
2303
2394
|
if (cue.final) {
|
|
2395
|
+
this.deps.glossary.notice(cue.src);
|
|
2304
2396
|
if (this.opts.mode === "realtime" && !this.provider?.translates && cue.tr === void 0 && this.targets().length) this.translate(cue);
|
|
2305
2397
|
else {
|
|
2306
2398
|
this.record(cue);
|
|
@@ -2409,6 +2501,10 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2409
2501
|
return;
|
|
2410
2502
|
}
|
|
2411
2503
|
if (!this.segments().length) throw new Error("nothing recorded yet");
|
|
2504
|
+
if (this.vod && this.duration && opts.from >= this.duration - 1.5) opts = {
|
|
2505
|
+
...opts,
|
|
2506
|
+
from: 0
|
|
2507
|
+
};
|
|
2412
2508
|
const r = new Restream({
|
|
2413
2509
|
input: (from) => this.restreamInput(from),
|
|
2414
2510
|
rowsAt: (t) => this.rowsAt(t, opts.style)
|
|
@@ -2742,6 +2838,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2742
2838
|
try {
|
|
2743
2839
|
writeFileSync(join(this.deps.dir, "brief.txt"), this.brief);
|
|
2744
2840
|
} catch {}
|
|
2841
|
+
this.deps.glossary.notice(`${this.title ?? ""}\n${this.brief}`);
|
|
2745
2842
|
}
|
|
2746
2843
|
/** The site's subtitle text around [from, to] (media seconds), per track — a reference, not ground truth. */
|
|
2747
2844
|
siteText(from, to) {
|
|
@@ -3017,6 +3114,86 @@ var Downloads = class {
|
|
|
3017
3114
|
});
|
|
3018
3115
|
return task;
|
|
3019
3116
|
}
|
|
3117
|
+
/**
|
|
3118
|
+
* Just the sound, as a file people can play anywhere (M4A, or the site's own
|
|
3119
|
+
* MP3/M4A as is). Uses what is already here (original, recording, source
|
|
3120
|
+
* file); otherwise fetches only the audio stream, never the whole video.
|
|
3121
|
+
*/
|
|
3122
|
+
audio(opts) {
|
|
3123
|
+
const dir = join(this.jobsDir, opts.dirId);
|
|
3124
|
+
const task = this.add({
|
|
3125
|
+
kind: "audio",
|
|
3126
|
+
conversationId: opts.conversationId,
|
|
3127
|
+
url: opts.url,
|
|
3128
|
+
dirId: opts.dirId,
|
|
3129
|
+
title: opts.title,
|
|
3130
|
+
saveAs: opts.saveAs
|
|
3131
|
+
});
|
|
3132
|
+
(async () => {
|
|
3133
|
+
const recording = join(dir, "recording.mp4");
|
|
3134
|
+
let source = opts.localFile ?? findOriginal(dir) ?? (existsSync(recording) ? recording : void 0);
|
|
3135
|
+
if (!source) {
|
|
3136
|
+
if (!opts.url) throw new Error("nothing to take the sound from yet");
|
|
3137
|
+
task.stage = "download";
|
|
3138
|
+
source = await downloadAudio(opts.url, dir, opts.browser, (pct) => {
|
|
3139
|
+
task.pct = Math.round(pct * .95);
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
if (dirname(source) === dir && /^\.(m4a|mp3)$/i.test(extname(source))) {
|
|
3143
|
+
task.file = basename(source);
|
|
3144
|
+
return;
|
|
3145
|
+
}
|
|
3146
|
+
task.stage = "mux";
|
|
3147
|
+
task.pct = Math.max(task.pct, 95);
|
|
3148
|
+
const out = join(dir, "audio.m4a");
|
|
3149
|
+
try {
|
|
3150
|
+
await run("ffmpeg", [
|
|
3151
|
+
"-v",
|
|
3152
|
+
"error",
|
|
3153
|
+
"-y",
|
|
3154
|
+
"-i",
|
|
3155
|
+
source,
|
|
3156
|
+
"-vn",
|
|
3157
|
+
"-map",
|
|
3158
|
+
"0:a:0",
|
|
3159
|
+
"-c:a",
|
|
3160
|
+
"copy",
|
|
3161
|
+
"-movflags",
|
|
3162
|
+
"+faststart",
|
|
3163
|
+
out
|
|
3164
|
+
], { timeout: 9e5 });
|
|
3165
|
+
} catch {
|
|
3166
|
+
await run("ffmpeg", [
|
|
3167
|
+
"-v",
|
|
3168
|
+
"error",
|
|
3169
|
+
"-y",
|
|
3170
|
+
"-i",
|
|
3171
|
+
source,
|
|
3172
|
+
"-vn",
|
|
3173
|
+
"-map",
|
|
3174
|
+
"0:a:0",
|
|
3175
|
+
"-c:a",
|
|
3176
|
+
"aac",
|
|
3177
|
+
"-b:a",
|
|
3178
|
+
"192k",
|
|
3179
|
+
"-movflags",
|
|
3180
|
+
"+faststart",
|
|
3181
|
+
out
|
|
3182
|
+
], { timeout: 18e5 });
|
|
3183
|
+
}
|
|
3184
|
+
task.file = basename(out);
|
|
3185
|
+
})().then(() => {
|
|
3186
|
+
task.status = "done";
|
|
3187
|
+
task.pct = 100;
|
|
3188
|
+
task.stage = void 0;
|
|
3189
|
+
}).catch((err) => {
|
|
3190
|
+
const msg = String(err.stderr || err.message);
|
|
3191
|
+
task.status = "error";
|
|
3192
|
+
task.error = /HTTP Error (412|429)/.test(msg) ? "The site is refusing requests for now (rate limit). Try again in a minute." : msg.slice(-300);
|
|
3193
|
+
this.log(`audio download failed: ${task.error}`);
|
|
3194
|
+
});
|
|
3195
|
+
return task;
|
|
3196
|
+
}
|
|
3020
3197
|
};
|
|
3021
3198
|
// src/keys.ts
|
|
3022
3199
|
/** Online speech services; each reads its key from the names DSH and other tools already use. */
|
|
@@ -5105,6 +5282,21 @@ function apply(ctx, config) {
|
|
|
5105
5282
|
saveAs: typeof b.saveAs === "string" ? b.saveAs.replace(/[\\/:*?"<>|]+/g, " ").slice(0, 150) : "video.mp4"
|
|
5106
5283
|
}));
|
|
5107
5284
|
}
|
|
5285
|
+
if (route === "audio" && req.method === "POST") {
|
|
5286
|
+
const b = await readJson(req);
|
|
5287
|
+
const job = jobs.get(String(b.jobId));
|
|
5288
|
+
if (!job) return send(res, 404, { error: "no such job" });
|
|
5289
|
+
const s = job.summary();
|
|
5290
|
+
return send(res, 200, downloads.audio({
|
|
5291
|
+
conversationId: String(b.conversationId ?? s.conversationId),
|
|
5292
|
+
dirId: s.id,
|
|
5293
|
+
title: s.title,
|
|
5294
|
+
url: s.source === "url" ? s.url : void 0,
|
|
5295
|
+
browser: s.loginBrowser,
|
|
5296
|
+
localFile: s.source === "file" ? s.url : void 0,
|
|
5297
|
+
saveAs: typeof b.saveAs === "string" ? b.saveAs.replace(/[\\/:*?"<>|]+/g, " ").slice(0, 150) : "audio"
|
|
5298
|
+
}));
|
|
5299
|
+
}
|
|
5108
5300
|
if (route === "job") {
|
|
5109
5301
|
const jid = url.searchParams.get("jobId");
|
|
5110
5302
|
const job = jid ? jobs.get(jid) : jobForConversation(url.searchParams.get("conversationId") ?? "");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vidi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|