dsh-vidi 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +30 -2
- package/lib/index.mjs +108 -0
- package/package.json +11 -6
package/lib/client.js
CHANGED
|
@@ -36192,6 +36192,7 @@ const en = {
|
|
|
36192
36192
|
"chat.p.download": "Download the original of this video.",
|
|
36193
36193
|
"page.back": "Back to chats",
|
|
36194
36194
|
"page.open": "Vidi video",
|
|
36195
|
+
"page.failed": "The video page could not open: {e}",
|
|
36195
36196
|
"side.subs": "Subtitles {p}%",
|
|
36196
36197
|
"side.subsLive": "Subtitling",
|
|
36197
36198
|
"side.recording": "Recording",
|
|
@@ -36506,6 +36507,7 @@ const zh = {
|
|
|
36506
36507
|
"chat.p.download": "下载这个视频的原片。",
|
|
36507
36508
|
"page.back": "返回对话",
|
|
36508
36509
|
"page.open": "Vidi 视频",
|
|
36510
|
+
"page.failed": "视频页打不开:{e}",
|
|
36509
36511
|
"side.subs": "字幕 {p}%",
|
|
36510
36512
|
"side.subsLive": "生成字幕中",
|
|
36511
36513
|
"side.recording": "录制中",
|
|
@@ -36756,6 +36758,8 @@ const CSS = `
|
|
|
36756
36758
|
@media (max-width: 1000px) { .dshl-main { grid-template-columns: minmax(0, 1fr); } }
|
|
36757
36759
|
|
|
36758
36760
|
/* Video page (full window over the app) */
|
|
36761
|
+
.dshl-toast { position: fixed; left: 50%; bottom: 28px; z-index: 2147483001; max-width: min(560px, calc(100vw - 32px)); padding: 10px 16px; border-radius: 10px;
|
|
36762
|
+
background: rgba(28,28,31,.95); color: #f2f2f3; font: 13.5px/1.5 system-ui, -apple-system, sans-serif; box-shadow: 0 10px 30px rgba(0,0,0,.3); transform: translateX(-50%); pointer-events: none; }
|
|
36759
36763
|
.dshl-pagewrap { position: absolute; inset: 0; z-index: 1; pointer-events: auto; background: var(--dsw-alias-bg-base, #fff); }
|
|
36760
36764
|
.dshl.page { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
|
36761
36765
|
.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; }
|
|
@@ -37196,6 +37200,7 @@ function ensureStyles() {
|
|
|
37196
37200
|
|
|
37197
37201
|
// src/client/VideoPage.tsx
|
|
37198
37202
|
const PAGE_SESSION = "dsh-live.pageSession";
|
|
37203
|
+
const samePath = (a, b) => !!a && !!b && a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
|
|
37199
37204
|
/**
|
|
37200
37205
|
* The video page's own conversation: reused while it exists, created when not.
|
|
37201
37206
|
* It lives in a "Video" workspace on the video folder, so DeepSeek's own file
|
|
@@ -37207,8 +37212,15 @@ async function pageSession(sessions, workspaces, title) {
|
|
|
37207
37212
|
saved = localStorage.getItem(PAGE_SESSION);
|
|
37208
37213
|
} catch {}
|
|
37209
37214
|
if (saved && sessions.has(saved)) return saved;
|
|
37210
|
-
const deps = await fetch("/live/api/deps").then((r) => r.json())
|
|
37211
|
-
|
|
37215
|
+
const deps = await fetch("/live/api/deps").then((r) => r.json());
|
|
37216
|
+
if (!deps?.dataDir) throw new Error("the video plugin is not running yet");
|
|
37217
|
+
const known = () => workspaces.list?.getSnapshot().items?.find((w) => samePath(w.path, deps.dataDir));
|
|
37218
|
+
let ws = known();
|
|
37219
|
+
if (!ws) ws = await workspaces.create({ path: deps.dataDir }).catch((err) => {
|
|
37220
|
+
const again = known();
|
|
37221
|
+
if (again) return again;
|
|
37222
|
+
throw err;
|
|
37223
|
+
});
|
|
37212
37224
|
const workspaceId = ws.workspaceId ?? ws.id;
|
|
37213
37225
|
if (workspaceId && ws.title !== title) await workspaces.rename(workspaceId, title).catch(() => {});
|
|
37214
37226
|
const id = await sessions.create(workspaceId ? { workspaceId } : { cwd: deps.dataDir });
|
|
@@ -37223,6 +37235,17 @@ async function pageSession(sessions, workspaces, title) {
|
|
|
37223
37235
|
* The page's session becomes current so its chat is live; closing goes back.
|
|
37224
37236
|
*/
|
|
37225
37237
|
async function openPage(sessions, opts = {}) {
|
|
37238
|
+
try {
|
|
37239
|
+
await showPage(sessions, opts);
|
|
37240
|
+
} catch (err) {
|
|
37241
|
+
console.error("[dsh-vidi] could not open the video page", err);
|
|
37242
|
+
pageStore.set({ error: err.message || String(err) });
|
|
37243
|
+
setTimeout(() => {
|
|
37244
|
+
if (pageStore.get().error) pageStore.set({ error: void 0 });
|
|
37245
|
+
}, 6e3);
|
|
37246
|
+
}
|
|
37247
|
+
}
|
|
37248
|
+
async function showPage(sessions, opts) {
|
|
37226
37249
|
const current = sessions.current();
|
|
37227
37250
|
const sid = opts.sessionId ?? await pageSession(sessions, opts.workspaces, opts.title ?? "Video");
|
|
37228
37251
|
if (sid !== current) sessions.open(sid);
|
|
@@ -37248,6 +37271,11 @@ function VideoPage(props) {
|
|
|
37248
37271
|
const page = (0, react.useSyncExternalStore)(pageStore.subscribe, pageStore.get);
|
|
37249
37272
|
const face = (0, react.useSyncExternalStore)(sessions.subscribe, () => page.sessionId ? sessions.face(page.sessionId) : void 0);
|
|
37250
37273
|
const chat = (0, react.useSyncExternalStore)(sessions.subscribe, () => page.sessionId ? sessions.chat(page.sessionId) : void 0);
|
|
37274
|
+
if (page.error && !page.open) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
37275
|
+
className: "dshl-toast",
|
|
37276
|
+
role: "alert",
|
|
37277
|
+
children: t("page.failed").replace("{e}", page.error)
|
|
37278
|
+
});
|
|
37251
37279
|
if (!page.open || !page.sessionId) return null;
|
|
37252
37280
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
37253
37281
|
className: "dshl-pagewrap",
|
package/lib/index.mjs
CHANGED
|
@@ -2641,6 +2641,92 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2641
2641
|
for (const f of readdirSync(dir)) if (f.startsWith(`.${name}.`)) rmSync(join(dir, f), { force: true });
|
|
2642
2642
|
return out;
|
|
2643
2643
|
}
|
|
2644
|
+
/**
|
|
2645
|
+
* Cut [from, to] into an MP4 with the subtitles drawn into the picture,
|
|
2646
|
+
* looking exactly like the player (and the restream): each change of the
|
|
2647
|
+
* lines on screen becomes one transparent picture, shown for its stretch.
|
|
2648
|
+
*/
|
|
2649
|
+
async burnClip(from, to, style, opts = {}) {
|
|
2650
|
+
if (!(to > from)) throw new Error("end must be after start");
|
|
2651
|
+
const h = opts.height ?? 1080, w = Math.round(h * 16 / 9);
|
|
2652
|
+
const dir = join(this.deps.dir, "clips");
|
|
2653
|
+
mkdirSync(dir, { recursive: true });
|
|
2654
|
+
const name = `clip_${from.toFixed(1)}-${to.toFixed(1)}_subs`.replace(/\./g, "_");
|
|
2655
|
+
const out = opts.out ?? join(dir, `${name}.mp4`);
|
|
2656
|
+
const tmp = join(dir, `.${name}`);
|
|
2657
|
+
mkdirSync(tmp, { recursive: true });
|
|
2658
|
+
const spans = [];
|
|
2659
|
+
for (let t = from; t < to; t += .1) {
|
|
2660
|
+
const rows = this.rowsAt(t, style);
|
|
2661
|
+
const key = JSON.stringify(rows);
|
|
2662
|
+
const last = spans.at(-1);
|
|
2663
|
+
if (last && last.key === key) last.end = Math.min(to, t + .1);
|
|
2664
|
+
else spans.push({
|
|
2665
|
+
start: t,
|
|
2666
|
+
end: Math.min(to, t + .1),
|
|
2667
|
+
key,
|
|
2668
|
+
rows
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
const pictures = /* @__PURE__ */ new Map();
|
|
2672
|
+
const lines = ["ffconcat version 1.0"];
|
|
2673
|
+
for (const s of spans) {
|
|
2674
|
+
let file = pictures.get(s.key);
|
|
2675
|
+
if (!file) {
|
|
2676
|
+
file = join(tmp, `${pictures.size}.png`);
|
|
2677
|
+
const png = await drawSubtitles(s.rows, style, w, h);
|
|
2678
|
+
if (!png) throw new Error("subtitles could not be drawn on this computer");
|
|
2679
|
+
writeFileSync(file, png);
|
|
2680
|
+
pictures.set(s.key, file);
|
|
2681
|
+
}
|
|
2682
|
+
lines.push(`file '${file.replace(/'/g, "'\\''")}'`, `duration ${(s.end - s.start).toFixed(3)}`);
|
|
2683
|
+
}
|
|
2684
|
+
lines.push(`file '${pictures.get(spans.at(-1).key).replace(/'/g, "'\\''")}'`);
|
|
2685
|
+
const list = join(tmp, "subs.ffconcat");
|
|
2686
|
+
writeFileSync(list, lines.join("\n") + "\n");
|
|
2687
|
+
const { args } = this.mediaFor(from, to);
|
|
2688
|
+
try {
|
|
2689
|
+
await run$1("ffmpeg", [
|
|
2690
|
+
"-v",
|
|
2691
|
+
"error",
|
|
2692
|
+
"-y",
|
|
2693
|
+
...args,
|
|
2694
|
+
"-f",
|
|
2695
|
+
"concat",
|
|
2696
|
+
"-safe",
|
|
2697
|
+
"0",
|
|
2698
|
+
"-i",
|
|
2699
|
+
list,
|
|
2700
|
+
"-t",
|
|
2701
|
+
String(to - from),
|
|
2702
|
+
"-filter_complex",
|
|
2703
|
+
`[0:v]scale=${w}:${h}:force_original_aspect_ratio=decrease,pad=${w}:${h}:(ow-iw)/2:(oh-ih)/2,setsar=1,setpts=PTS-STARTPTS[v];[1:v]format=rgba,setpts=PTS-STARTPTS[s];[v][s]overlay=0:0:eof_action=repeat,format=yuv420p[o]`,
|
|
2704
|
+
"-map",
|
|
2705
|
+
"[o]",
|
|
2706
|
+
"-map",
|
|
2707
|
+
"0:a?",
|
|
2708
|
+
"-c:v",
|
|
2709
|
+
"libx264",
|
|
2710
|
+
"-preset",
|
|
2711
|
+
"veryfast",
|
|
2712
|
+
"-crf",
|
|
2713
|
+
"20",
|
|
2714
|
+
"-c:a",
|
|
2715
|
+
"aac",
|
|
2716
|
+
"-b:a",
|
|
2717
|
+
"160k",
|
|
2718
|
+
"-movflags",
|
|
2719
|
+
"+faststart",
|
|
2720
|
+
out
|
|
2721
|
+
], { timeout: 9e5 });
|
|
2722
|
+
} finally {
|
|
2723
|
+
rmSync(tmp, {
|
|
2724
|
+
recursive: true,
|
|
2725
|
+
force: true
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
return out;
|
|
2729
|
+
}
|
|
2644
2730
|
/** Language actually spoken (setting, or what recognition detected). */
|
|
2645
2731
|
spokenLang() {
|
|
2646
2732
|
if (this.opts.srcLang !== "auto") return this.opts.srcLang;
|
|
@@ -4660,6 +4746,7 @@ function apply(ctx, config) {
|
|
|
4660
4746
|
jobs.set(id, job);
|
|
4661
4747
|
if (!waiting) byConversation.set(opts.conversationId, id);
|
|
4662
4748
|
job.autoSubs = true;
|
|
4749
|
+
bus.emit("job", job);
|
|
4663
4750
|
job.start();
|
|
4664
4751
|
return job;
|
|
4665
4752
|
}
|
|
@@ -5201,6 +5288,27 @@ function apply(ctx, config) {
|
|
|
5201
5288
|
},
|
|
5202
5289
|
subsDefaults: lastOptions
|
|
5203
5290
|
});
|
|
5291
|
+
/**
|
|
5292
|
+
* For companion plugins (a Studio edition and the like): the jobs with their
|
|
5293
|
+
* tools — frames, clips with subtitles drawn in, cues and events — and the
|
|
5294
|
+
* subtitle model, so a companion adds its own workflow without a copy of any
|
|
5295
|
+
* of this. Inject 'vidi' to use it.
|
|
5296
|
+
*/
|
|
5297
|
+
const vidiService = {
|
|
5298
|
+
version: 1,
|
|
5299
|
+
dataDir: config.dataDir,
|
|
5300
|
+
job: (conversationId) => jobForConversation(conversationId),
|
|
5301
|
+
jobById: (id) => jobs.get(id),
|
|
5302
|
+
jobs: () => [...jobs.values()],
|
|
5303
|
+
onJob: (fn) => {
|
|
5304
|
+
bus.on("job", fn);
|
|
5305
|
+
return () => {
|
|
5306
|
+
bus.off("job", fn);
|
|
5307
|
+
};
|
|
5308
|
+
},
|
|
5309
|
+
llm: () => llm
|
|
5310
|
+
};
|
|
5311
|
+
ctx.effect(() => ctx.provide("vidi", vidiService), "dsh-vidi: companion service");
|
|
5204
5312
|
ctx.effect(() => ctx.webServer.register({
|
|
5205
5313
|
kind: "prefix",
|
|
5206
5314
|
path: "/live/api",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vidi",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "A video tool for DeepSeek Harness: watch live streams, videos and podcasts with real-time subtitles in any language, then clip and download",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
@@ -57,9 +57,6 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@deepseek-ai/cordis": "^4.1.0",
|
|
61
|
-
"@deepseek-ai/dsh-llm": "0.0.1-rc.1",
|
|
62
|
-
"@deepseek-ai/schemastery": "^3.19.0",
|
|
63
60
|
"@napi-rs/canvas": "1.1.0"
|
|
64
61
|
},
|
|
65
62
|
"devDependencies": {
|
|
@@ -70,7 +67,10 @@
|
|
|
70
67
|
"react": "^18.3.1",
|
|
71
68
|
"react-dom": "^18.3.1",
|
|
72
69
|
"tsdown": "^0.22.14",
|
|
73
|
-
"typescript": "^5.9.2"
|
|
70
|
+
"typescript": "^5.9.2",
|
|
71
|
+
"@deepseek-ai/cordis": "^4.1.0",
|
|
72
|
+
"@deepseek-ai/dsh-llm": "0.0.1-rc.1",
|
|
73
|
+
"@deepseek-ai/schemastery": "^3.19.0"
|
|
74
74
|
},
|
|
75
75
|
"keywords": [
|
|
76
76
|
"dsh",
|
|
@@ -82,5 +82,10 @@
|
|
|
82
82
|
"subtitles",
|
|
83
83
|
"translation",
|
|
84
84
|
"podcast"
|
|
85
|
-
]
|
|
85
|
+
],
|
|
86
|
+
"peerDependencies": {
|
|
87
|
+
"@deepseek-ai/cordis": "*",
|
|
88
|
+
"@deepseek-ai/dsh-llm": "*",
|
|
89
|
+
"@deepseek-ai/schemastery": "*"
|
|
90
|
+
}
|
|
86
91
|
}
|