dsh-vidi 0.9.0 → 0.9.1
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 +153 -6
- package/lib/index.mjs +182 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -35376,6 +35376,92 @@ function useDevices(enabled) {
|
|
|
35376
35376
|
}, [enabled, devices]);
|
|
35377
35377
|
return devices;
|
|
35378
35378
|
}
|
|
35379
|
+
/** "3 分钟前", "5 小时前", "2 天前", then the date. */
|
|
35380
|
+
function ago(ts, ui) {
|
|
35381
|
+
if (!ts) return "";
|
|
35382
|
+
const s = Math.max(0, (Date.now() - ts) / 1e3);
|
|
35383
|
+
if (s < 60) return ui === "zh" ? "刚刚" : "just now";
|
|
35384
|
+
const [n, unit] = s < 3600 ? [Math.round(s / 60), ui === "zh" ? " 分钟前" : " min ago"] : s < 86400 ? [Math.round(s / 3600), ui === "zh" ? " 小时前" : " h ago"] : s < 604800 ? [Math.round(s / 86400), ui === "zh" ? " 天前" : " d ago"] : [0, ""];
|
|
35385
|
+
if (unit) return `${n}${unit}`;
|
|
35386
|
+
return new Date(ts).toLocaleDateString(ui === "zh" ? "zh-CN" : "en-US", {
|
|
35387
|
+
month: "short",
|
|
35388
|
+
day: "numeric"
|
|
35389
|
+
});
|
|
35390
|
+
}
|
|
35391
|
+
/** Past videos under the start box: the newest few, more on request. */
|
|
35392
|
+
function History({ t, ui, onOpen }) {
|
|
35393
|
+
const [items, setItems] = (0, react.useState)([]);
|
|
35394
|
+
const [total, setTotal] = (0, react.useState)(0);
|
|
35395
|
+
const load = (offset, limit) => fetch(`/live/api/history?offset=${offset}&limit=${limit}`).then((r) => r.json()).then((j) => {
|
|
35396
|
+
setItems((v) => offset ? [...v, ...j.items ?? []] : j.items ?? []);
|
|
35397
|
+
setTotal(j.total ?? 0);
|
|
35398
|
+
}).catch(() => {});
|
|
35399
|
+
(0, react.useEffect)(() => {
|
|
35400
|
+
load(0, 5);
|
|
35401
|
+
}, []);
|
|
35402
|
+
if (!items.length) return null;
|
|
35403
|
+
const from = (v) => v.source === "file" ? t("src.file") : v.source === "device" ? t("src.camera") : v.source === "push" ? t("src.push") : siteName(v.url ?? "") || (() => {
|
|
35404
|
+
try {
|
|
35405
|
+
return new URL(v.url ?? "").hostname.replace(/^www\./, "");
|
|
35406
|
+
} catch {
|
|
35407
|
+
return t("src.link");
|
|
35408
|
+
}
|
|
35409
|
+
})();
|
|
35410
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
35411
|
+
className: "dshl-history",
|
|
35412
|
+
children: [
|
|
35413
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35414
|
+
className: "hh",
|
|
35415
|
+
children: t("hist.title")
|
|
35416
|
+
}),
|
|
35417
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35418
|
+
className: "hl",
|
|
35419
|
+
children: items.map((v) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
35420
|
+
className: "hi",
|
|
35421
|
+
onClick: () => onOpen(v),
|
|
35422
|
+
children: [
|
|
35423
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
35424
|
+
className: "th",
|
|
35425
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
35426
|
+
src: `/live/api/thumb?jobId=${encodeURIComponent(v.id)}`,
|
|
35427
|
+
alt: "",
|
|
35428
|
+
onError: (e) => {
|
|
35429
|
+
e.currentTarget.style.visibility = "hidden";
|
|
35430
|
+
}
|
|
35431
|
+
}), v.duration ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35432
|
+
className: "dur",
|
|
35433
|
+
children: CLOCK(v.duration)
|
|
35434
|
+
}) : null]
|
|
35435
|
+
}),
|
|
35436
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
35437
|
+
className: "tx",
|
|
35438
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35439
|
+
className: "tt",
|
|
35440
|
+
children: v.title || (v.source === "push" ? from(v) : t("hist.untitled"))
|
|
35441
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35442
|
+
className: "mm",
|
|
35443
|
+
children: [
|
|
35444
|
+
v.title || v.source !== "push" ? from(v) : "",
|
|
35445
|
+
v.live ? t("hist.live") : "",
|
|
35446
|
+
ago(v.startedAt, ui)
|
|
35447
|
+
].filter(Boolean).join(" · ")
|
|
35448
|
+
})]
|
|
35449
|
+
}),
|
|
35450
|
+
v.running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
35451
|
+
className: "chip",
|
|
35452
|
+
children: t("hist.running")
|
|
35453
|
+
})
|
|
35454
|
+
]
|
|
35455
|
+
}, v.id))
|
|
35456
|
+
}),
|
|
35457
|
+
items.length < total && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35458
|
+
className: "dshl-link more",
|
|
35459
|
+
onClick: () => void load(items.length, 10),
|
|
35460
|
+
children: t("hist.more").replace("{n}", String(total - items.length))
|
|
35461
|
+
})
|
|
35462
|
+
]
|
|
35463
|
+
});
|
|
35464
|
+
}
|
|
35379
35465
|
/** The push stream key: copy it, or change it (typed, or a new random one). */
|
|
35380
35466
|
function KeyRow({ value, t, onChanged }) {
|
|
35381
35467
|
const [edit, setEdit] = (0, react.useState)(null);
|
|
@@ -36123,6 +36209,22 @@ function LiveView(props) {
|
|
|
36123
36209
|
return "camera";
|
|
36124
36210
|
}
|
|
36125
36211
|
};
|
|
36212
|
+
const openPast = async (v) => {
|
|
36213
|
+
setError("");
|
|
36214
|
+
try {
|
|
36215
|
+
if (v.conversationId !== sessionId && props.onOpenVideo && await props.onOpenVideo(v.conversationId, v.id)) return;
|
|
36216
|
+
const j = await post("reopen", {
|
|
36217
|
+
jobId: v.id,
|
|
36218
|
+
conversationId: sessionId
|
|
36219
|
+
});
|
|
36220
|
+
setCues(j.cues ?? []);
|
|
36221
|
+
setTerms(j.terms ?? []);
|
|
36222
|
+
setJob(j);
|
|
36223
|
+
setArmed(null);
|
|
36224
|
+
} catch (e) {
|
|
36225
|
+
setError(e.message);
|
|
36226
|
+
}
|
|
36227
|
+
};
|
|
36126
36228
|
const emptyBox = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
36127
36229
|
className: "dshl-drop",
|
|
36128
36230
|
children: [
|
|
@@ -36132,6 +36234,11 @@ function LiveView(props) {
|
|
|
36132
36234
|
children: sourceBar
|
|
36133
36235
|
}),
|
|
36134
36236
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: srcTab === "camera" && devices && !mics.length ? t("src.cam.none") : t(srcTab === "file" ? "src.file.body" : srcTab === "camera" ? "src.cam.body" : srcTab === "push" ? "src.push.body" : "empty.body") }),
|
|
36237
|
+
pageMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(History, {
|
|
36238
|
+
t,
|
|
36239
|
+
ui,
|
|
36240
|
+
onOpen: (v) => void openPast(v)
|
|
36241
|
+
}),
|
|
36135
36242
|
srcTab === "push" && armed?.rtmp && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
36136
36243
|
className: "dshl-pushinfo",
|
|
36137
36244
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyRow, {
|
|
@@ -37492,6 +37599,11 @@ const en = {
|
|
|
37492
37599
|
"dev.blocked": "No picture or sound from the device: DeepSeek Harness needs the system’s permission. Open System Settings, switch DeepSeek Harness on, then retry.",
|
|
37493
37600
|
"dev.denied": "DeepSeek Harness needs permission to use the {d}. Open System Settings, switch it on in the list, then retry.",
|
|
37494
37601
|
"dev.and": " and ",
|
|
37602
|
+
"hist.title": "Recent videos",
|
|
37603
|
+
"hist.untitled": "Untitled video",
|
|
37604
|
+
"hist.live": "live",
|
|
37605
|
+
"hist.running": "Live now",
|
|
37606
|
+
"hist.more": "More ({n})",
|
|
37495
37607
|
"set.spoken.tip": "The language people speak in this video; Auto detects it.",
|
|
37496
37608
|
"rs.mode": "Subtitle mode of the push-out",
|
|
37497
37609
|
"rs.mode.live": "Pushed out about {s} s behind; subtitles come a little after the words.",
|
|
@@ -37941,6 +38053,11 @@ const zh = {
|
|
|
37941
38053
|
"dev.blocked": "设备没有传来画面和声音:DeepSeek Harness 需要系统授权。点「打开系统设置」,在列表里打开 DeepSeek Harness,再点重试。",
|
|
37942
38054
|
"dev.denied": "DeepSeek Harness 需要{d}权限。点「打开系统设置」,在列表里打开它,再点重试。",
|
|
37943
38055
|
"dev.and": "和",
|
|
38056
|
+
"hist.title": "最近的视频",
|
|
38057
|
+
"hist.untitled": "未命名视频",
|
|
38058
|
+
"hist.live": "直播",
|
|
38059
|
+
"hist.running": "直播中",
|
|
38060
|
+
"hist.more": "更多(还有 {n} 个)",
|
|
37944
38061
|
"set.spoken.tip": "视频里说的是哪种语言;自动 = 自己识别。",
|
|
37945
38062
|
"rs.mode": "推流用的字幕模式",
|
|
37946
38063
|
"rs.mode.live": "推出去比实时晚约 {s} 秒,字幕会比说话稍晚出现。",
|
|
@@ -38613,6 +38730,19 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
|
|
|
38613
38730
|
.dshl-menu .dshl-seg button.on { background: rgba(255,255,255,.92); color: #111; box-shadow: none; }
|
|
38614
38731
|
.dshl-menu .dshl-switch { background: rgba(255,255,255,.22); }
|
|
38615
38732
|
.dshl-menu .dshl-switch.on { background: var(--accent); }
|
|
38733
|
+
.dshl-history { width: 100%; max-width: 680px; margin-top: 40px; text-align: left; }
|
|
38734
|
+
.dshl-history .hh { margin: 0 0 8px 4px; color: var(--faint); font-size: 12.5px; }
|
|
38735
|
+
.dshl-history .hl { display: grid; gap: 2px; }
|
|
38736
|
+
.dshl-history .hi { display: flex; align-items: center; gap: 12px; width: 100%; padding: 6px; border: 0; border-radius: 10px; background: none; color: var(--ink); text-align: left; cursor: pointer; }
|
|
38737
|
+
.dshl-history .hi:hover { background: var(--hover); }
|
|
38738
|
+
.dshl-history .th { position: relative; flex: none; width: 96px; aspect-ratio: 16 / 9; overflow: hidden; border-radius: 6px; background: var(--l2); }
|
|
38739
|
+
.dshl-history .th img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
38740
|
+
.dshl-history .dur { position: absolute; right: 4px; bottom: 3px; padding: 0 4px; border-radius: 3px; background: rgba(0,0,0,.65); color: #fff; font-size: 10.5px; line-height: 16px; font-variant-numeric: tabular-nums; }
|
|
38741
|
+
.dshl-history .tx { display: grid; gap: 3px; min-width: 0; flex: 1; }
|
|
38742
|
+
.dshl-history .tt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; font-weight: 550; }
|
|
38743
|
+
.dshl-history .mm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--faint); font-size: 12px; }
|
|
38744
|
+
.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
|
+
.dshl-history .more { margin: 8px 0 0 6px; font-size: 12.5px; }
|
|
38616
38746
|
.dshl-modemenu { display: grid; gap: 4px; width: 300px; max-width: 100%; }
|
|
38617
38747
|
.dshl-modemenu .mm { display: grid; gap: 3px; padding: 9px 10px; border: 0; border-radius: 8px; background: none; color: inherit; text-align: left; cursor: pointer; }
|
|
38618
38748
|
.dshl-modemenu .mm:hover { background: rgba(255,255,255,.08); }
|
|
@@ -38987,7 +39117,11 @@ async function openPage(sessions, opts = {}) {
|
|
|
38987
39117
|
}
|
|
38988
39118
|
async function showPage(sessions, opts) {
|
|
38989
39119
|
const current = sessions.current();
|
|
38990
|
-
const fresh = () =>
|
|
39120
|
+
const fresh = async () => {
|
|
39121
|
+
let id = await pageSession(sessions, opts.workspaces, opts.title ?? "Video");
|
|
39122
|
+
if (await fetch(`/live/api/job?conversationId=${encodeURIComponent(id)}`).then((r) => r.json()).catch(() => null)) id = await pageSession(sessions, opts.workspaces, opts.title ?? "Video", true);
|
|
39123
|
+
return id;
|
|
39124
|
+
};
|
|
38991
39125
|
let sid = opts.sessionId && sessions.has(opts.sessionId) ? opts.sessionId : await fresh();
|
|
38992
39126
|
if (sid !== current) try {
|
|
38993
39127
|
sessions.open(sid);
|
|
@@ -39052,6 +39186,23 @@ function videoHandlers(sessions, workspaces, sessionId, fallbackTitle) {
|
|
|
39052
39186
|
return false;
|
|
39053
39187
|
}
|
|
39054
39188
|
},
|
|
39189
|
+
/** Open a past video in its own conversation; false when that conversation is gone. */
|
|
39190
|
+
onOpenVideo: async (conversationId, jobId) => {
|
|
39191
|
+
if (!conversationId || !sessions.has(conversationId)) return false;
|
|
39192
|
+
await fetch("/live/api/reopen", {
|
|
39193
|
+
method: "POST",
|
|
39194
|
+
headers: { "content-type": "application/json" },
|
|
39195
|
+
body: JSON.stringify({
|
|
39196
|
+
jobId,
|
|
39197
|
+
conversationId
|
|
39198
|
+
})
|
|
39199
|
+
}).catch(() => {});
|
|
39200
|
+
await openPage(sessions, {
|
|
39201
|
+
sessionId: conversationId,
|
|
39202
|
+
workspaces
|
|
39203
|
+
});
|
|
39204
|
+
return true;
|
|
39205
|
+
},
|
|
39055
39206
|
onTitle: (title) => {
|
|
39056
39207
|
own().then((yes) => {
|
|
39057
39208
|
const now = sessions.info?.(sessionId)?.title;
|
|
@@ -39137,11 +39288,7 @@ function SidebarButton(props) {
|
|
|
39137
39288
|
const pct = item?.subs === "running" && item.duration ? Math.min(99, Math.round(item.at / item.duration * 100)) : void 0;
|
|
39138
39289
|
const meta = !busy ? "" : item?.subs === "running" ? pct !== void 0 ? t("side.subs").replace("{p}", String(pct)) : t("side.subsLive") : item?.recording ? t("side.recording") : item?.loading ? t("side.loading") : t("side.downloading");
|
|
39139
39290
|
const thumb = busy && frame && (!item || frame.jobId === item.jobId) ? frame.dataUrl : void 0;
|
|
39140
|
-
const open = () => void openPage(sessions,
|
|
39141
|
-
sessionId: item.conversationId,
|
|
39142
|
-
workspaces,
|
|
39143
|
-
title: t("view.live")
|
|
39144
|
-
} : {
|
|
39291
|
+
const open = () => void openPage(sessions, {
|
|
39145
39292
|
workspaces,
|
|
39146
39293
|
title: t("view.live")
|
|
39147
39294
|
});
|
package/lib/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
1
2
|
import { appendFileSync, chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { homedir, networkInterfaces } from "node:os";
|
|
3
4
|
import { basename, dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
4
5
|
import z from "@deepseek-ai/schemastery";
|
|
5
|
-
import { execFile, spawn } from "node:child_process";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { EventEmitter } from "node:events";
|
|
8
8
|
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
@@ -2111,6 +2111,12 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2111
2111
|
this.persist();
|
|
2112
2112
|
this.emit("state", this.summary());
|
|
2113
2113
|
}
|
|
2114
|
+
/** Show this video in another conversation (opened again from the history): saved, so it stays there. */
|
|
2115
|
+
rebind(conversationId) {
|
|
2116
|
+
this.opts.conversationId = conversationId;
|
|
2117
|
+
this.closed = false;
|
|
2118
|
+
this.persist();
|
|
2119
|
+
}
|
|
2114
2120
|
/** job.json: enough to bring the job back after a restart (finished, not recording). */
|
|
2115
2121
|
persist() {
|
|
2116
2122
|
if (this.opts.source === "push" && this.message) return;
|
|
@@ -6430,6 +6436,137 @@ function apply(ctx, config) {
|
|
|
6430
6436
|
};
|
|
6431
6437
|
}
|
|
6432
6438
|
/** The conversation's video: in memory, or the latest one on disk (after a restart). */
|
|
6439
|
+
/** A video by id: the running one, or brought back from its folder. */
|
|
6440
|
+
function loadJob(id) {
|
|
6441
|
+
if (jobs.has(id)) return jobs.get(id);
|
|
6442
|
+
if (!/^[\w-]+$/.test(id)) return void 0;
|
|
6443
|
+
const dir = join(config.dataDir, "jobs", id);
|
|
6444
|
+
const file = join(dir, "job.json");
|
|
6445
|
+
if (!existsSync(file)) return void 0;
|
|
6446
|
+
try {
|
|
6447
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
6448
|
+
let job;
|
|
6449
|
+
job = LiveJob.restore(dir, jobDeps(dir, raw.opts, () => job));
|
|
6450
|
+
if (job) jobs.set(job.id, job);
|
|
6451
|
+
return job;
|
|
6452
|
+
} catch {
|
|
6453
|
+
return;
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
/** Past videos for the start page, newest first: those with something to watch. */
|
|
6457
|
+
const historyCache = /* @__PURE__ */ new Map();
|
|
6458
|
+
function history() {
|
|
6459
|
+
const root = join(config.dataDir, "jobs");
|
|
6460
|
+
if (!existsSync(root)) return [];
|
|
6461
|
+
const out = [];
|
|
6462
|
+
for (const name of readdirSync(root).filter((n) => /^[\w-]+$/.test(n)).sort().reverse()) {
|
|
6463
|
+
const dir = join(root, name), file = join(dir, "job.json");
|
|
6464
|
+
const live = jobs.get(name);
|
|
6465
|
+
if (live && live.summary().status === "running" && !live.summary().message) {
|
|
6466
|
+
const s = live.summary();
|
|
6467
|
+
out.push({
|
|
6468
|
+
id: s.id,
|
|
6469
|
+
title: s.title || (s.source === "url" ? s.url : ""),
|
|
6470
|
+
source: s.source,
|
|
6471
|
+
url: s.source === "url" ? s.url : void 0,
|
|
6472
|
+
startedAt: s.startedAt,
|
|
6473
|
+
duration: s.vod ? s.duration : s.recorded,
|
|
6474
|
+
live: !s.vod,
|
|
6475
|
+
running: true,
|
|
6476
|
+
conversationId: s.conversationId
|
|
6477
|
+
});
|
|
6478
|
+
continue;
|
|
6479
|
+
}
|
|
6480
|
+
let mtime = 0;
|
|
6481
|
+
try {
|
|
6482
|
+
mtime = statSync(file).mtimeMs;
|
|
6483
|
+
} catch {
|
|
6484
|
+
continue;
|
|
6485
|
+
}
|
|
6486
|
+
let hit = historyCache.get(name);
|
|
6487
|
+
if (!hit || hit.mtime !== mtime) {
|
|
6488
|
+
let item = null;
|
|
6489
|
+
try {
|
|
6490
|
+
const j = JSON.parse(readFileSync(file, "utf8"));
|
|
6491
|
+
if ((existsSync(join(dir, "recording.mp4")) || existsSync(join(dir, "hls", "index.m3u8"))) && j.status !== "error") {
|
|
6492
|
+
const device = j.opts.source === "device" ? (() => {
|
|
6493
|
+
try {
|
|
6494
|
+
return JSON.parse(j.opts.url).title;
|
|
6495
|
+
} catch {
|
|
6496
|
+
return "";
|
|
6497
|
+
}
|
|
6498
|
+
})() : "";
|
|
6499
|
+
let duration = j.duration;
|
|
6500
|
+
if (!duration) try {
|
|
6501
|
+
duration = [...readFileSync(join(dir, "hls", "index.m3u8"), "utf8").matchAll(/#EXTINF:([\d.]+)/g)].reduce((n, m) => n + Number(m[1]), 0) || void 0;
|
|
6502
|
+
} catch {}
|
|
6503
|
+
item = {
|
|
6504
|
+
id: j.id,
|
|
6505
|
+
title: j.title || device || (j.opts.source === "url" ? j.opts.url : ""),
|
|
6506
|
+
source: j.opts.source,
|
|
6507
|
+
url: j.opts.source === "url" ? j.opts.url : void 0,
|
|
6508
|
+
startedAt: j.startedAt,
|
|
6509
|
+
duration,
|
|
6510
|
+
live: !j.vod,
|
|
6511
|
+
running: false,
|
|
6512
|
+
conversationId: j.opts.conversationId
|
|
6513
|
+
};
|
|
6514
|
+
}
|
|
6515
|
+
} catch {}
|
|
6516
|
+
hit = {
|
|
6517
|
+
mtime,
|
|
6518
|
+
item
|
|
6519
|
+
};
|
|
6520
|
+
historyCache.set(name, hit);
|
|
6521
|
+
}
|
|
6522
|
+
if (hit.item) out.push(hit.item);
|
|
6523
|
+
}
|
|
6524
|
+
return out;
|
|
6525
|
+
}
|
|
6526
|
+
/** A small picture of a video for its history row (made once, kept in its folder). */
|
|
6527
|
+
async function thumbnail(id) {
|
|
6528
|
+
if (!/^[\w-]+$/.test(id)) return void 0;
|
|
6529
|
+
const dir = join(config.dataDir, "jobs", id);
|
|
6530
|
+
const out = join(dir, "thumb.jpg");
|
|
6531
|
+
if (existsSync(out)) return out;
|
|
6532
|
+
for (const cover of ["cover.jpg", "cover.png"]) if (existsSync(join(dir, cover))) return join(dir, cover);
|
|
6533
|
+
const src = existsSync(join(dir, "recording.mp4")) ? join(dir, "recording.mp4") : existsSync(join(dir, "hls", "index.m3u8")) ? join(dir, "hls", "index.m3u8") : void 0;
|
|
6534
|
+
if (!src) return void 0;
|
|
6535
|
+
if (await new Promise((resolve) => {
|
|
6536
|
+
execFile("ffmpeg", [
|
|
6537
|
+
"-v",
|
|
6538
|
+
"error",
|
|
6539
|
+
"-y",
|
|
6540
|
+
"-i",
|
|
6541
|
+
src,
|
|
6542
|
+
"-ss",
|
|
6543
|
+
"3",
|
|
6544
|
+
"-frames:v",
|
|
6545
|
+
"1",
|
|
6546
|
+
"-vf",
|
|
6547
|
+
"scale=320:-2",
|
|
6548
|
+
"-q:v",
|
|
6549
|
+
"5",
|
|
6550
|
+
out
|
|
6551
|
+
], { timeout: 2e4 }, (err) => resolve(!err && existsSync(out)));
|
|
6552
|
+
})) return out;
|
|
6553
|
+
return await new Promise((resolve) => {
|
|
6554
|
+
execFile("ffmpeg", [
|
|
6555
|
+
"-v",
|
|
6556
|
+
"error",
|
|
6557
|
+
"-y",
|
|
6558
|
+
"-i",
|
|
6559
|
+
src,
|
|
6560
|
+
"-frames:v",
|
|
6561
|
+
"1",
|
|
6562
|
+
"-vf",
|
|
6563
|
+
"scale=320:-2",
|
|
6564
|
+
"-q:v",
|
|
6565
|
+
"5",
|
|
6566
|
+
out
|
|
6567
|
+
], { timeout: 2e4 }, (err) => resolve(!err && existsSync(out)));
|
|
6568
|
+
}) ? out : void 0;
|
|
6569
|
+
}
|
|
6433
6570
|
function jobForConversation(conversationId) {
|
|
6434
6571
|
const id = byConversation.get(conversationId);
|
|
6435
6572
|
if (id === "") return void 0;
|
|
@@ -6890,6 +7027,50 @@ function apply(ctx, config) {
|
|
|
6890
7027
|
saveAs: typeof b.saveAs === "string" ? b.saveAs.replace(/[\\/:*?"<>|]+/g, " ").slice(0, 150) : "audio"
|
|
6891
7028
|
}));
|
|
6892
7029
|
}
|
|
7030
|
+
if (route === "history") {
|
|
7031
|
+
const all = history();
|
|
7032
|
+
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
7033
|
+
const limit = Math.min(50, Math.max(1, Number(url.searchParams.get("limit")) || 5));
|
|
7034
|
+
return send(res, 200, {
|
|
7035
|
+
items: all.slice(offset, offset + limit),
|
|
7036
|
+
total: all.length
|
|
7037
|
+
});
|
|
7038
|
+
}
|
|
7039
|
+
if (route === "thumb") {
|
|
7040
|
+
const file = await thumbnail(url.searchParams.get("jobId") ?? "");
|
|
7041
|
+
if (!file) {
|
|
7042
|
+
res.writeHead(404);
|
|
7043
|
+
res.end();
|
|
7044
|
+
return;
|
|
7045
|
+
}
|
|
7046
|
+
res.writeHead(200, {
|
|
7047
|
+
"content-type": file.endsWith(".png") ? "image/png" : "image/jpeg",
|
|
7048
|
+
"cache-control": "max-age=3600"
|
|
7049
|
+
});
|
|
7050
|
+
createReadStream(file).pipe(res);
|
|
7051
|
+
return;
|
|
7052
|
+
}
|
|
7053
|
+
if (route === "reopen" && req.method === "POST") {
|
|
7054
|
+
const b = await readJson(req);
|
|
7055
|
+
const job = loadJob(String(b.jobId));
|
|
7056
|
+
const conv = String(b.conversationId ?? "");
|
|
7057
|
+
if (!job || !conv) return send(res, 404, { error: "no such video" });
|
|
7058
|
+
try {
|
|
7059
|
+
rmSync(join(job.summary().dir, "closed"), { force: true });
|
|
7060
|
+
} catch {}
|
|
7061
|
+
const prev = byConversation.get(conv);
|
|
7062
|
+
if (prev && prev !== job.id) {
|
|
7063
|
+
const other = jobs.get(prev);
|
|
7064
|
+
if (other && other.summary().status !== "running") byConversation.delete(conv);
|
|
7065
|
+
}
|
|
7066
|
+
job.rebind(conv);
|
|
7067
|
+
byConversation.set(conv, job.id);
|
|
7068
|
+
return send(res, 200, {
|
|
7069
|
+
...job.summary(),
|
|
7070
|
+
cues: job.cues,
|
|
7071
|
+
terms: job.terms
|
|
7072
|
+
});
|
|
7073
|
+
}
|
|
6893
7074
|
if (route === "preview") {
|
|
6894
7075
|
const job = jobs.get(url.searchParams.get("jobId") ?? "");
|
|
6895
7076
|
if (!job) return send(res, 404, { error: "no such job" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vidi",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
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",
|