dsh-vidi 0.2.1 → 0.2.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 +38 -2
- package/lib/index.mjs +108 -0
- package/package.json +1 -1
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,7 +36758,17 @@ 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); }
|
|
36764
|
+
/* Desktop app: the page covers the whole window. Windows: start under the title bar. macOS: the
|
|
36765
|
+
traffic lights sit over the top-left, so the top bar leaves them room, lines its controls up
|
|
36766
|
+
with them, and its empty space moves the window like the app's own top strip. Full screen and
|
|
36767
|
+
the web page need none of this. */
|
|
36768
|
+
.dshl-pagewrap { top: var(--dsh-frame-chrome-top, 0px); }
|
|
36769
|
+
html[data-platform='darwin']:not([data-fullscreen]) .dshl.page .dshl-top { padding: 4px 16px 8px 84px; -webkit-app-region: drag; app-region: drag; }
|
|
36770
|
+
html[data-platform='darwin']:not([data-fullscreen]) .dshl.page .dshl-top :is(button, input, label, a, select, .dshl-pick) { -webkit-app-region: no-drag; app-region: no-drag; }
|
|
36771
|
+
@media (max-width: 600px) { html[data-platform='darwin']:not([data-fullscreen]) .dshl.page .dshl-top { padding-left: 80px; } }
|
|
36760
36772
|
.dshl.page { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
|
36761
36773
|
.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; }
|
|
36762
36774
|
/* Video page: player on top, then the status row, then subtitles/glossary filling what is left; chat on the right. */
|
|
@@ -37196,6 +37208,7 @@ function ensureStyles() {
|
|
|
37196
37208
|
|
|
37197
37209
|
// src/client/VideoPage.tsx
|
|
37198
37210
|
const PAGE_SESSION = "dsh-live.pageSession";
|
|
37211
|
+
const samePath = (a, b) => !!a && !!b && a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
|
|
37199
37212
|
/**
|
|
37200
37213
|
* The video page's own conversation: reused while it exists, created when not.
|
|
37201
37214
|
* It lives in a "Video" workspace on the video folder, so DeepSeek's own file
|
|
@@ -37207,8 +37220,15 @@ async function pageSession(sessions, workspaces, title) {
|
|
|
37207
37220
|
saved = localStorage.getItem(PAGE_SESSION);
|
|
37208
37221
|
} catch {}
|
|
37209
37222
|
if (saved && sessions.has(saved)) return saved;
|
|
37210
|
-
const deps = await fetch("/live/api/deps").then((r) => r.json())
|
|
37211
|
-
|
|
37223
|
+
const deps = await fetch("/live/api/deps").then((r) => r.json());
|
|
37224
|
+
if (!deps?.dataDir) throw new Error("the video plugin is not running yet");
|
|
37225
|
+
const known = () => workspaces.list?.getSnapshot().items?.find((w) => samePath(w.path, deps.dataDir));
|
|
37226
|
+
let ws = known();
|
|
37227
|
+
if (!ws) ws = await workspaces.create({ path: deps.dataDir }).catch((err) => {
|
|
37228
|
+
const again = known();
|
|
37229
|
+
if (again) return again;
|
|
37230
|
+
throw err;
|
|
37231
|
+
});
|
|
37212
37232
|
const workspaceId = ws.workspaceId ?? ws.id;
|
|
37213
37233
|
if (workspaceId && ws.title !== title) await workspaces.rename(workspaceId, title).catch(() => {});
|
|
37214
37234
|
const id = await sessions.create(workspaceId ? { workspaceId } : { cwd: deps.dataDir });
|
|
@@ -37223,6 +37243,17 @@ async function pageSession(sessions, workspaces, title) {
|
|
|
37223
37243
|
* The page's session becomes current so its chat is live; closing goes back.
|
|
37224
37244
|
*/
|
|
37225
37245
|
async function openPage(sessions, opts = {}) {
|
|
37246
|
+
try {
|
|
37247
|
+
await showPage(sessions, opts);
|
|
37248
|
+
} catch (err) {
|
|
37249
|
+
console.error("[dsh-vidi] could not open the video page", err);
|
|
37250
|
+
pageStore.set({ error: err.message || String(err) });
|
|
37251
|
+
setTimeout(() => {
|
|
37252
|
+
if (pageStore.get().error) pageStore.set({ error: void 0 });
|
|
37253
|
+
}, 6e3);
|
|
37254
|
+
}
|
|
37255
|
+
}
|
|
37256
|
+
async function showPage(sessions, opts) {
|
|
37226
37257
|
const current = sessions.current();
|
|
37227
37258
|
const sid = opts.sessionId ?? await pageSession(sessions, opts.workspaces, opts.title ?? "Video");
|
|
37228
37259
|
if (sid !== current) sessions.open(sid);
|
|
@@ -37248,6 +37279,11 @@ function VideoPage(props) {
|
|
|
37248
37279
|
const page = (0, react.useSyncExternalStore)(pageStore.subscribe, pageStore.get);
|
|
37249
37280
|
const face = (0, react.useSyncExternalStore)(sessions.subscribe, () => page.sessionId ? sessions.face(page.sessionId) : void 0);
|
|
37250
37281
|
const chat = (0, react.useSyncExternalStore)(sessions.subscribe, () => page.sessionId ? sessions.chat(page.sessionId) : void 0);
|
|
37282
|
+
if (page.error && !page.open) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
37283
|
+
className: "dshl-toast",
|
|
37284
|
+
role: "alert",
|
|
37285
|
+
children: t("page.failed").replace("{e}", page.error)
|
|
37286
|
+
});
|
|
37251
37287
|
if (!page.open || !page.sessionId) return null;
|
|
37252
37288
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
37253
37289
|
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.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",
|