dsh-vidi 0.8.8 → 0.9.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/lib/client.js CHANGED
@@ -358,6 +358,17 @@ const Icon = {
358
358
  rx: "2"
359
359
  })
360
360
  }),
361
+ timer: () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
362
+ viewBox: "0 0 24 24",
363
+ width: "18",
364
+ height: "18",
365
+ ...P,
366
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
367
+ cx: "12",
368
+ cy: "13.5",
369
+ r: "7.5"
370
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 9.5v4l2.5 1.5M10 3h4M12 3v3" })]
371
+ }),
361
372
  live: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
362
373
  viewBox: "0 0 24 24",
363
374
  width: "16",
@@ -1062,7 +1073,7 @@ const BGS = {
1062
1073
  * position and background.
1063
1074
  */
1064
1075
  function SubtitleMenu(props) {
1065
- const { lines, onLines, options, site, name, style, onStyle, t } = props;
1076
+ const { lines, onLines, options, site, name, style, onStyle, spoken, t } = props;
1066
1077
  const main = lines[0] ?? "src";
1067
1078
  const second = lines[1] ?? "";
1068
1079
  const label = (k) => site.find((s) => s.key === k)?.label ?? name(k);
@@ -1127,6 +1138,21 @@ function SubtitleMenu(props) {
1127
1138
  className: "swap",
1128
1139
  onClick: () => onLines([second, main]),
1129
1140
  children: ["⇅ ", t("sub.swapMain")]
1141
+ }),
1142
+ spoken && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1143
+ className: "slot",
1144
+ title: t("set.spoken.tip"),
1145
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("set.spoken") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
1146
+ value: spoken.value,
1147
+ onChange: spoken.onChange,
1148
+ options: [{
1149
+ value: "auto",
1150
+ label: t("set.auto")
1151
+ }, ...spoken.options.map(([k, v]) => ({
1152
+ value: k,
1153
+ label: v
1154
+ }))]
1155
+ })]
1130
1156
  })
1131
1157
  ]
1132
1158
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -34384,14 +34410,9 @@ const CLOCK$1 = (s) => {
34384
34410
  return `${h ? `${h}:` : ""}${String(m).padStart(h ? 2 : 1, "0")}:${String(ss).padStart(2, "0")}`;
34385
34411
  };
34386
34412
  const host = (s) => s.replace(/^[a-z]+:\/\//i, "").split("/")[0] ?? s;
34387
- /**
34388
- * The player's restream menu: saved destinations (tick the ones to push to),
34389
- * adding one (platform, server address, stream key — copied from the
34390
- * platform's own streaming tool), subtitles drawn in or not, picture size,
34391
- * then start. While running, each destination shows its own state.
34392
- */
34393
34413
  function RestreamPanel(props) {
34394
- const { jobId, status, now, style, t } = props;
34414
+ const { jobId, status, now, style, timing, t } = props;
34415
+ const fromNow = () => timing ? timing.fromFor(timing.mode) : now;
34395
34416
  const running = !!status?.running;
34396
34417
  const [targets, setTargets] = (0, react.useState)([]);
34397
34418
  const [picked, setPicked] = (0, react.useState)(() => {
@@ -34463,7 +34484,7 @@ function RestreamPanel(props) {
34463
34484
  jobId,
34464
34485
  action: "start",
34465
34486
  targets: picked.filter((id) => targets.some((x) => x.id === id)),
34466
- from: now,
34487
+ from: fromNow(),
34467
34488
  burn,
34468
34489
  height,
34469
34490
  style
@@ -34491,12 +34512,39 @@ function RestreamPanel(props) {
34491
34512
  jobId,
34492
34513
  action: "start",
34493
34514
  targets: [id],
34494
- from: now,
34515
+ from: fromNow(),
34495
34516
  burn,
34496
34517
  height,
34497
34518
  style
34498
34519
  }).catch((e) => setError(e.message));
34499
34520
  };
34521
+ const switchMode = async (m) => {
34522
+ if (!timing || m === timing.mode) return;
34523
+ timing.onMode(m);
34524
+ if (!running) return;
34525
+ const ids = status.targets.map((x) => x.id);
34526
+ setBusy(true);
34527
+ setError("");
34528
+ try {
34529
+ await post$2("restream", {
34530
+ jobId,
34531
+ action: "stop"
34532
+ });
34533
+ await post$2("restream", {
34534
+ jobId,
34535
+ action: "start",
34536
+ targets: ids,
34537
+ from: timing.fromFor(m),
34538
+ burn,
34539
+ height,
34540
+ style
34541
+ });
34542
+ } catch (e) {
34543
+ setError(e.message);
34544
+ } finally {
34545
+ setBusy(false);
34546
+ }
34547
+ };
34500
34548
  const stateOf = (id) => running ? status.targets.find((x) => x.id === id) : void 0;
34501
34549
  const chosen = picked.filter((id) => targets.some((x) => x.id === id));
34502
34550
  if (editing) {
@@ -34678,6 +34726,32 @@ function RestreamPanel(props) {
34678
34726
  className: "rs-err",
34679
34727
  children: status.targets.filter((x) => x.message && x.state !== "live").map((x) => `${pname(x.platform)}:${x.message}`).join(";")
34680
34728
  }),
34729
+ timing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34730
+ className: "rs-mode",
34731
+ children: [
34732
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34733
+ className: "rs-sub",
34734
+ children: t("rs.mode")
34735
+ }),
34736
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34737
+ className: "seg",
34738
+ children: [
34739
+ "live",
34740
+ "realtime",
34741
+ "polished"
34742
+ ].map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
34743
+ className: timing.mode === m ? "on" : "",
34744
+ disabled: busy,
34745
+ onClick: () => void switchMode(m),
34746
+ children: t(`mode.${m}`)
34747
+ }, m))
34748
+ }),
34749
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
34750
+ className: "rs-hint",
34751
+ children: t(`rs.mode.${timing.mode}`).replace("{s}", String(Math.max(1, Math.round(timing.behindFor(timing.mode)))))
34752
+ })
34753
+ ]
34754
+ }),
34681
34755
  !running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
34682
34756
  className: "rs-opts",
34683
34757
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -34849,7 +34923,7 @@ async function post$1(path, body) {
34849
34923
  return j;
34850
34924
  }
34851
34925
  function Settings(props) {
34852
- const { engine, onEngine, mode, onMode, spoken, onSpoken, langOptions, login, onLogin, browsers, providers, shared, name, onKey, disabled, open, onOpen, update, onCheckUpdate, t } = props;
34926
+ const { engine, onEngine, login, onLogin, browsers, providers, shared, name, onKey, disabled, open, onOpen, update, onCheckUpdate, t } = props;
34853
34927
  const [checking, setChecking] = (0, react.useState)(false);
34854
34928
  const [checked, setChecked] = (0, react.useState)(false);
34855
34929
  const [installing, setInstalling] = (0, react.useState)(false);
@@ -35161,43 +35235,6 @@ function Settings(props) {
35161
35235
  })
35162
35236
  ]
35163
35237
  }),
35164
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35165
- className: "srow col",
35166
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
35167
- className: "pair",
35168
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("set.mode") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35169
- className: "dshl-seg",
35170
- children: [
35171
- "live",
35172
- "realtime",
35173
- "polished"
35174
- ].map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35175
- className: mode === m ? "on" : "",
35176
- title: t(`mode.${m}.tip`),
35177
- onClick: () => onMode(m),
35178
- children: t(`mode.${m}`)
35179
- }, m))
35180
- })]
35181
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35182
- className: "hint",
35183
- children: t(`mode.${mode}.tip`)
35184
- })]
35185
- }),
35186
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35187
- className: "srow",
35188
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("set.spoken") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
35189
- value: spoken,
35190
- disabled,
35191
- onChange: onSpoken,
35192
- options: [{
35193
- value: "auto",
35194
- label: t("set.auto")
35195
- }, ...langOptions.map(([k, v]) => ({
35196
- value: k,
35197
- label: v
35198
- }))]
35199
- })]
35200
- }),
35201
35238
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35202
35239
  className: "srow",
35203
35240
  title: t("set.login.tip"),
@@ -36014,6 +36051,18 @@ function LiveView(props) {
36014
36051
  const missing = deps.filter((d) => !d.ok && (d.name === "ffmpeg" || d.name === "yt-dlp" || (engine || "local") === "local" && d.name.startsWith("whisper")));
36015
36052
  const needsModel = !hasTranslator && shownLines.some((l) => l !== "src");
36016
36053
  const hasMain = !!job && !(job.status === "error" && !job.files.some((f) => /\.(mp4|m4a|srt|vtt)$/.test(f)));
36054
+ const liveEdge = job?.recorded ?? now;
36055
+ const pushFrom = (m) => {
36056
+ const firstPass = subs.ready ?? subs.at;
36057
+ const at = m === "live" ? liveEdge - 2 : m === "realtime" ? firstPass : Math.max(lastPolished, firstPass - 150);
36058
+ return Math.max(0, Math.min(at, liveEdge));
36059
+ };
36060
+ const pushTiming = job && !job.vod && job.status === "running" ? {
36061
+ mode,
36062
+ onMode: changeMode,
36063
+ fromFor: pushFrom,
36064
+ behindFor: (m) => liveEdge - pushFrom(m)
36065
+ } : void 0;
36017
36066
  (0, react.useEffect)(() => {
36018
36067
  if (job && !hasMain) setSrcTabRaw(job.source === "device" ? "camera" : job.source === "push" ? "push" : job.source === "file" ? "file" : "link");
36019
36068
  }, [job?.id, hasMain]);
@@ -36388,11 +36437,6 @@ function LiveView(props) {
36388
36437
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Settings, {
36389
36438
  engine: engine || "local",
36390
36439
  onEngine: setEngine,
36391
- mode,
36392
- onMode: changeMode,
36393
- spoken,
36394
- onSpoken: setSpoken,
36395
- langOptions,
36396
36440
  login,
36397
36441
  onLogin: chooseLogin,
36398
36442
  browsers,
@@ -36580,9 +36624,38 @@ function LiveView(props) {
36580
36624
  name: nameWithSpoken,
36581
36625
  style: subStyle,
36582
36626
  onStyle: setSubStyle,
36627
+ spoken: {
36628
+ value: spoken,
36629
+ options: langOptions,
36630
+ onChange: setSpoken
36631
+ },
36583
36632
  t
36584
36633
  }) })
36585
36634
  },
36635
+ ...!job.vod && job.status === "running" ? [{
36636
+ id: "mode",
36637
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.timer, {}),
36638
+ title: t("set.mode"),
36639
+ label: t(`mode.${mode}`),
36640
+ content: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36641
+ className: "dshl-modemenu",
36642
+ children: [
36643
+ "live",
36644
+ "realtime",
36645
+ "polished"
36646
+ ].map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
36647
+ className: `mm ${mode === m ? "on" : ""}`,
36648
+ onClick: () => changeMode(m),
36649
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
36650
+ className: "n",
36651
+ children: [t(`mode.${m}`), mode === m && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.check, {})]
36652
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
36653
+ className: "d",
36654
+ children: t(`mode.${m}.tip`)
36655
+ })]
36656
+ }, m))
36657
+ })
36658
+ }] : [],
36586
36659
  {
36587
36660
  id: "restream",
36588
36661
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.push, {}),
@@ -36594,6 +36667,7 @@ function LiveView(props) {
36594
36667
  status: job.restream,
36595
36668
  now,
36596
36669
  style: subStyle,
36670
+ timing: pushTiming,
36597
36671
  t
36598
36672
  })
36599
36673
  },
@@ -37418,6 +37492,11 @@ const en = {
37418
37492
  "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.",
37419
37493
  "dev.denied": "DeepSeek Harness needs permission to use the {d}. Open System Settings, switch it on in the list, then retry.",
37420
37494
  "dev.and": " and ",
37495
+ "set.spoken.tip": "The language people speak in this video; Auto detects it.",
37496
+ "rs.mode": "Subtitle mode of the push-out",
37497
+ "rs.mode.live": "Pushed out about {s} s behind; subtitles come a little after the words.",
37498
+ "rs.mode.realtime": "Pushed out about {s} s behind, with first-pass subtitles in step.",
37499
+ "rs.mode.polished": "Pushed out about {s} s behind, with polished subtitles.",
37421
37500
  "own.live": "Live",
37422
37501
  "own.delayed": "As pushed out",
37423
37502
  "own.toLive": "Show the live picture large",
@@ -37862,6 +37941,11 @@ const zh = {
37862
37941
  "dev.blocked": "设备没有传来画面和声音:DeepSeek Harness 需要系统授权。点「打开系统设置」,在列表里打开 DeepSeek Harness,再点重试。",
37863
37942
  "dev.denied": "DeepSeek Harness 需要{d}权限。点「打开系统设置」,在列表里打开它,再点重试。",
37864
37943
  "dev.and": "和",
37944
+ "set.spoken.tip": "视频里说的是哪种语言;自动 = 自己识别。",
37945
+ "rs.mode": "推流用的字幕模式",
37946
+ "rs.mode.live": "推出去比实时晚约 {s} 秒,字幕会比说话稍晚出现。",
37947
+ "rs.mode.realtime": "推出去比实时晚约 {s} 秒,带对齐好的初版字幕。",
37948
+ "rs.mode.polished": "推出去比实时晚约 {s} 秒,带精修字幕。",
37865
37949
  "own.live": "实时画面",
37866
37950
  "own.delayed": "推流画面",
37867
37951
  "own.toLive": "放大实时画面",
@@ -38402,6 +38486,9 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
38402
38486
  outline: 1px solid rgba(255,255,255,.28); box-shadow: 0 6px 24px rgba(0,0,0,.5); cursor: pointer; transition: bottom .25s; }
38403
38487
  .dshl-stage.idle .dshl-vbox.pip, .dshl-stage.idle .dshl-livecam.pip { bottom: 14px; }
38404
38488
  .dshl-stage .pip .dshl-sub { transform: translateX(-50%) !important; }
38489
+ .dshl-rs .rs-mode { display: grid; gap: 6px; margin: 10px 0 4px; }
38490
+ .dshl-rs .rs-mode .seg { display: flex; }
38491
+ .dshl-rs .rs-mode .seg button { flex: 1; }
38405
38492
  .dshl-piplabel { position: absolute; top: 6px; left: 6px; z-index: 3; padding: 2px 7px; border-radius: 5px; background: rgba(0,0,0,.6); color: #fff; font-size: 11px; pointer-events: none; }
38406
38493
  .dshl-stage.idle { cursor: none; }
38407
38494
  .dshl-holdnote { position: absolute; top: 14px; left: 50%; z-index: 4; display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 999px; transform: translateX(-50%);
@@ -38526,6 +38613,13 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
38526
38613
  .dshl-menu .dshl-seg button.on { background: rgba(255,255,255,.92); color: #111; box-shadow: none; }
38527
38614
  .dshl-menu .dshl-switch { background: rgba(255,255,255,.22); }
38528
38615
  .dshl-menu .dshl-switch.on { background: var(--accent); }
38616
+ .dshl-modemenu { display: grid; gap: 4px; width: 300px; max-width: 100%; }
38617
+ .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
+ .dshl-modemenu .mm:hover { background: rgba(255,255,255,.08); }
38619
+ .dshl-modemenu .mm.on { background: rgba(255,255,255,.12); }
38620
+ .dshl-modemenu .n { display: flex; align-items: center; justify-content: space-between; font-size: 13.5px; font-weight: 600; }
38621
+ .dshl-modemenu .n svg { width: 14px; height: 14px; }
38622
+ .dshl-modemenu .d { color: rgba(255,255,255,.62); font-size: 12px; line-height: 1.45; }
38529
38623
  .dshl-menu .opt { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 32px; padding: 0 6px; color: var(--muted); }
38530
38624
  .dshl-menu .opt .dshl-seg { max-width: 100%; overflow-x: auto; scrollbar-width: none; }
38531
38625
  .dshl-menu .opt input[type=range] { flex: 1; max-width: 180px; accent-color: var(--accent); }
@@ -38893,8 +38987,14 @@ async function openPage(sessions, opts = {}) {
38893
38987
  }
38894
38988
  async function showPage(sessions, opts) {
38895
38989
  const current = sessions.current();
38896
- const sid = opts.sessionId ?? await pageSession(sessions, opts.workspaces, opts.title ?? "Video");
38897
- if (sid !== current) sessions.open(sid);
38990
+ const fresh = () => pageSession(sessions, opts.workspaces, opts.title ?? "Video");
38991
+ let sid = opts.sessionId && sessions.has(opts.sessionId) ? opts.sessionId : await fresh();
38992
+ if (sid !== current) try {
38993
+ sessions.open(sid);
38994
+ } catch {
38995
+ sid = await fresh();
38996
+ sessions.open(sid);
38997
+ }
38898
38998
  if (panelHost) {
38899
38999
  pageStore.set({
38900
39000
  open: true,
@@ -39037,7 +39137,11 @@ function SidebarButton(props) {
39037
39137
  const pct = item?.subs === "running" && item.duration ? Math.min(99, Math.round(item.at / item.duration * 100)) : void 0;
39038
39138
  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");
39039
39139
  const thumb = busy && frame && (!item || frame.jobId === item.jobId) ? frame.dataUrl : void 0;
39040
- const open = () => void openPage(sessions, item ? { sessionId: item.conversationId } : {
39140
+ const open = () => void openPage(sessions, item ? {
39141
+ sessionId: item.conversationId,
39142
+ workspaces,
39143
+ title: t("view.live")
39144
+ } : {
39041
39145
  workspaces,
39042
39146
  title: t("view.live")
39043
39147
  });
package/lib/index.mjs CHANGED
@@ -1912,8 +1912,6 @@ function deviceInput(c, q) {
1912
1912
  if (process.platform === "darwin") return {
1913
1913
  args: [
1914
1914
  ...q,
1915
- "-use_wallclock_as_timestamps",
1916
- "1",
1917
1915
  "-f",
1918
1916
  "avfoundation",
1919
1917
  "-framerate",
@@ -1998,6 +1996,8 @@ var LiveJob = class LiveJob extends EventEmitter {
1998
1996
  /** Real-time picture (own sources): the newest JPEG frame, and whether frames are coming. */
1999
1997
  lastFrame;
2000
1998
  previewOn = false;
1999
+ /** Closed by the user: no longer offered anywhere (its files stay). */
2000
+ closed = false;
2001
2001
  id;
2002
2002
  status = "resolving";
2003
2003
  message;
@@ -2451,6 +2451,7 @@ var LiveJob = class LiveJob extends EventEmitter {
2451
2451
  ...picture,
2452
2452
  "-map",
2453
2453
  input.audio,
2454
+ ...input.fps ? ["-af", "aresample=async=1:min_hard_comp=0.1:first_pts=0"] : [],
2454
2455
  "-c:a",
2455
2456
  "aac",
2456
2457
  "-b:a",
@@ -6558,6 +6559,7 @@ function apply(ctx, config) {
6558
6559
  const job = jobs.get(String(b.jobId));
6559
6560
  if (!job) return send(res, 404, { error: "no such job" });
6560
6561
  await job.close("closed");
6562
+ job.closed = true;
6561
6563
  const conv = job.summary().conversationId;
6562
6564
  if (byConversation.get(conv) === job.id) byConversation.set(conv, "");
6563
6565
  try {
@@ -6601,7 +6603,7 @@ function apply(ctx, config) {
6601
6603
  return send(res, 200, job.summary());
6602
6604
  }
6603
6605
  if (route === "activity") return send(res, 200, {
6604
- items: [...jobs.values()].map((j) => j.summary()).filter((s) => s.status === "running" && !s.message || s.status === "resolving" || s.subs?.state === "running").map((s) => ({
6606
+ items: [...jobs.values()].filter((j) => !j.closed).map((j) => j.summary()).filter((s) => s.status === "running" && !s.message || s.status === "resolving" || s.subs?.state === "running").map((s) => ({
6605
6607
  jobId: s.id,
6606
6608
  conversationId: s.conversationId,
6607
6609
  title: s.title,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vidi",
3
- "version": "0.8.8",
3
+ "version": "0.9.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",