dsh-vidi 0.7.1 → 0.7.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 CHANGED
@@ -255,6 +255,23 @@ const Icon = {
255
255
  strokeWidth: 2.6,
256
256
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5 12.5l4.5 4.5L19 7.5" })
257
257
  }),
258
+ sidePanel: () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
259
+ viewBox: "0 0 24 24",
260
+ width: "18",
261
+ height: "18",
262
+ fill: "none",
263
+ stroke: "currentColor",
264
+ strokeWidth: "1.8",
265
+ strokeLinecap: "round",
266
+ strokeLinejoin: "round",
267
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
268
+ x: "3",
269
+ y: "4",
270
+ width: "18",
271
+ height: "16",
272
+ rx: "2.5"
273
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M15 4v16" })]
274
+ }),
258
275
  plus: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
259
276
  viewBox: "0 0 24 24",
260
277
  width: "14",
@@ -844,7 +861,9 @@ function Pick(props) {
844
861
  if (!list.current?.contains(e.target) && !btn.current?.contains(e.target)) close(false);
845
862
  };
846
863
  const gone = (e) => {
847
- if (!list.current?.contains(e.target)) close(false);
864
+ const t = e.target;
865
+ if (list.current?.contains(t)) return;
866
+ if (e.type === "resize" || t === document || t === document.documentElement || t instanceof Node && btn.current && t.contains(btn.current)) close(false);
848
867
  };
849
868
  document.addEventListener("pointerdown", away, true);
850
869
  window.addEventListener("resize", gone);
@@ -35239,6 +35258,29 @@ function LiveView(props) {
35239
35258
  const [subStyle, setSubStyle] = (0, react.useState)(() => loadStyle(lines));
35240
35259
  const [delay, setDelay] = (0, react.useState)(8);
35241
35260
  const [theater, setTheater] = (0, react.useState)(false);
35261
+ const [sideOpen, setSideOpenRaw] = (0, react.useState)(() => store.get("sideOpen", true));
35262
+ const setSideOpen = (v) => {
35263
+ setSideOpenRaw(v);
35264
+ store.set("sideOpen", v);
35265
+ };
35266
+ const [sideW, setSideWRaw] = (0, react.useState)(() => store.get("sideW", 380));
35267
+ const mainRef = (0, react.useRef)(null);
35268
+ const dragSide = (e) => {
35269
+ e.preventDefault();
35270
+ const box = mainRef.current?.getBoundingClientRect();
35271
+ if (!box) return;
35272
+ const move = (ev) => setSideWRaw(Math.round(Math.min(box.width * .6, Math.max(280, box.right - ev.clientX - 8))));
35273
+ const up = () => {
35274
+ window.removeEventListener("pointermove", move);
35275
+ window.removeEventListener("pointerup", up);
35276
+ setSideWRaw((w) => {
35277
+ store.set("sideW", w);
35278
+ return w;
35279
+ });
35280
+ };
35281
+ window.addEventListener("pointermove", move);
35282
+ window.addEventListener("pointerup", up);
35283
+ };
35242
35284
  const [tab, setTab] = (0, react.useState)("transcript");
35243
35285
  const [query, setQuery] = (0, react.useState)("");
35244
35286
  const [trMode, setTrMode] = (0, react.useState)("lines");
@@ -35951,7 +35993,8 @@ function LiveView(props) {
35951
35993
  })]
35952
35994
  }) : null;
35953
35995
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35954
- className: `dshl ${theater && !activeBench ? "theater" : ""} ${pageMode ? "page" : ""} ${activeBench ? "bench" : ""}`,
35996
+ className: `dshl ${theater && !activeBench ? "theater" : ""} ${pageMode ? "page" : ""} ${activeBench ? "bench" : ""} ${pageMode && !sideOpen && !activeBench ? "noside" : ""}`,
35997
+ style: pageMode ? { ["--side-w"]: `${sideW}px` } : void 0,
35955
35998
  "data-dsh-own-drop": "dsh-vidi",
35956
35999
  onDragEnter: (e) => {
35957
36000
  if (e.dataTransfer.types.includes("Files")) e.stopPropagation();
@@ -36053,6 +36096,13 @@ function LiveView(props) {
36053
36096
  open: settingsOpen,
36054
36097
  onOpen: setSettingsOpen,
36055
36098
  t
36099
+ }),
36100
+ pageMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36101
+ className: `dshl-ctl icon ${sideOpen ? "on" : ""}`,
36102
+ title: t(sideOpen ? "side.hide" : "side.show"),
36103
+ "aria-pressed": sideOpen,
36104
+ onClick: () => setSideOpen(!sideOpen),
36105
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.sidePanel, {})
36056
36106
  })
36057
36107
  ]
36058
36108
  })
@@ -36082,6 +36132,7 @@ function LiveView(props) {
36082
36132
  children: emptyBox
36083
36133
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
36084
36134
  className: "dshl-main",
36135
+ ref: mainRef,
36085
36136
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36086
36137
  className: "dshl-col",
36087
36138
  children: !hasMain || !job ? emptyBox : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
@@ -36233,9 +36284,13 @@ function LiveView(props) {
36233
36284
  children: transcriptBox
36234
36285
  })
36235
36286
  ] })
36236
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
36287
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
36237
36288
  className: "dshl-side",
36238
- children: activeBench && view ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36289
+ children: [pageMode && !activeBench && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36290
+ className: "dshl-resize",
36291
+ onPointerDown: dragSide,
36292
+ title: t("side.resize")
36293
+ }), activeBench && view ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36239
36294
  className: "dshl-benchbox",
36240
36295
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Guard, {
36241
36296
  name: `workbench ${activeBench.id}`,
@@ -36252,7 +36307,7 @@ function LiveView(props) {
36252
36307
  hasVideo: !!job,
36253
36308
  jobsDir,
36254
36309
  t
36255
- }) : transcriptBox
36310
+ }) : transcriptBox]
36256
36311
  })]
36257
36312
  }),
36258
36313
  dragging && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -36823,6 +36878,9 @@ const en = {
36823
36878
  "rs.overlay": "Subtitles for OBS (Browser Source)",
36824
36879
  "rs.overlay.d": "Add a Browser Source in OBS with this address (1920×1080): the newest subtitles on a transparent background, over your camera or any picture. Add ?lines=zh,src&size=4.5 to choose lines and size.",
36825
36880
  "rs.copy": "Copy",
36881
+ "side.hide": "Hide the chat",
36882
+ "side.show": "Show the chat",
36883
+ "side.resize": "Drag to change the width",
36826
36884
  "rs.openPage": "Open",
36827
36885
  "rs.screen": "Subtitle screen (16:9)",
36828
36886
  "rs.screen.d": "Only the subtitles, large, for a big screen or projector: open it in any browser on this network and go full screen (on this computer: {local}). Add ?theme=light for a light screen; it can also be an OBS source of its own.",
@@ -37204,6 +37262,9 @@ const zh = {
37204
37262
  "rs.overlay": "OBS 字幕叠加(浏览器源)",
37205
37263
  "rs.overlay.d": "在 OBS 里加一个「浏览器源」,填这个地址(1920×1080):透明背景上只有最新的字幕,叠在摄像头或任何画面上。地址后加 ?lines=zh,src&size=4.5 可以选行和字号。",
37206
37264
  "rs.copy": "复制",
37265
+ "side.hide": "收起对话",
37266
+ "side.show": "展开对话",
37267
+ "side.resize": "拖动调整宽度",
37207
37268
  "rs.openPage": "打开",
37208
37269
  "rs.screen": "字幕大屏(16:9)",
37209
37270
  "rs.screen.d": "只有字幕、字很大,适合大屏和投影:在同一网络里任何浏览器打开、全屏即可(本机用 {local})。地址后加 ?theme=light 换浅色;也可以作为 OBS 的一个独立画面来源。",
@@ -37736,12 +37797,18 @@ html[data-platform='darwin'] .dshl-panelwrap .dshl.page .dshl-top { padding-top:
37736
37797
  html[data-platform='darwin'] .dshl.page .dshl-top { -webkit-app-region: drag; app-region: drag; }
37737
37798
  html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .dshl-select, .dshl-pick) { -webkit-app-region: no-drag; app-region: no-drag; }
37738
37799
  .dshl.page { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
37739
- .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; }
37800
+ .dshl.page .dshl-main { flex: 1; min-height: 0; grid-template-columns: minmax(0, 1fr) var(--side-w, clamp(320px, 30vw, 420px)); align-items: stretch; padding-bottom: 16px; }
37740
37801
  /* Video page: player on top, then the status row, then subtitles/glossary filling what is left; chat on the right. */
37741
37802
  .dshl.page .dshl-col { display: flex; flex-direction: column; min-height: 0; container-type: size; }
37742
37803
  .dshl.page .dshl-col > .dshl-drop { flex: 1; aspect-ratio: auto; min-height: 0; }
37743
37804
  .dshl.page .dshl-stage { flex: none; width: 100%; max-height: max(240px, calc(100cqh - 270px)); }
37744
- .dshl.page .dshl-side { position: static; height: 100%; max-height: none; }
37805
+ .dshl.page .dshl-side { position: relative; top: auto; height: 100%; max-height: none; }
37806
+ .dshl.page.noside .dshl-main { grid-template-columns: minmax(0, 1fr); }
37807
+ .dshl.page.noside .dshl-side { display: none; }
37808
+ /* Drag the chat panel's left edge to change its width; the player and subtitles take the rest. */
37809
+ .dshl-resize { position: absolute; top: 0; bottom: 0; left: 0; z-index: 3; width: 8px; cursor: col-resize; }
37810
+ .dshl-resize::after { content: ''; position: absolute; top: 50%; left: 2px; width: 3px; height: 36px; border-radius: 2px; background: var(--line); transform: translateY(-50%); opacity: 0; transition: opacity .15s; }
37811
+ .dshl-resize:hover::after { opacity: 1; }
37745
37812
  /* Workbench open: the video and its subtitles keep a column on the left; the bench takes the rest. */
37746
37813
  .dshl.page.bench .dshl-main { grid-template-columns: clamp(340px, 32vw, 520px) minmax(0, 1fr); }
37747
37814
  .dshl-lag { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; margin: -2px 0 8px; padding: 7px 12px; border-radius: 8px; background: var(--l1, rgba(0,0,0,.04)); color: var(--muted); font-size: 12.5px; }
@@ -37762,6 +37829,7 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
37762
37829
  .dshl.page.theater .dshl-stage { max-height: 100cqh; }
37763
37830
  @media (max-width: 1000px) {
37764
37831
  .dshl.page { display: block; overflow: auto; }
37832
+ .dshl-resize { display: none; }
37765
37833
  .dshl.page.bench .dshl-main { grid-template-columns: minmax(0, 1fr); }
37766
37834
  .dshl.page.bench .dshl-side { height: auto; min-height: 0; }
37767
37835
  .dshl-benchbox { overflow: visible; }
package/lib/index.mjs CHANGED
@@ -4725,6 +4725,8 @@ var Stream = class {
4725
4725
  cjk;
4726
4726
  open = [];
4727
4727
  done = [];
4728
+ /** When the last piece arrived (wall clock, ms). */
4729
+ lastWall = 0;
4728
4730
  constructor(cjk) {
4729
4731
  this.cjk = cjk;
4730
4732
  }
@@ -4754,18 +4756,51 @@ var Stream = class {
4754
4756
  this.push(p);
4755
4757
  }
4756
4758
  push(p) {
4759
+ this.lastWall = Date.now();
4757
4760
  this.open.push(p);
4758
4761
  const text = this.text.trim();
4759
4762
  const len = this.cjk ? Array.from(text).length : text.length;
4760
4763
  const soft = this.cjk ? SOFT_CJK : SOFT_LATIN, hard = this.cjk ? HARD_CJK : HARD_LATIN;
4761
4764
  if (END.test(text) || len >= soft && /[,,、;;::]\s*$/.test(text) || len >= hard) this.close();
4762
4765
  }
4766
+ /** End the sentence at its last comma or full stop (the rest starts the next one); false when it has none. */
4767
+ closeAtPause() {
4768
+ const text = this.text;
4769
+ let cut = -1;
4770
+ for (let i = text.length - 2; i > 0; i--) if (/[,。、;:!?,;:!?]/.test(text[i])) {
4771
+ cut = i + 1;
4772
+ break;
4773
+ }
4774
+ if (cut <= 0) return false;
4775
+ const head = [], tail = [];
4776
+ let n = 0;
4777
+ for (const p of this.open) {
4778
+ if (n + p.text.length <= cut) head.push(p);
4779
+ else if (n >= cut) tail.push(p);
4780
+ else {
4781
+ head.push({
4782
+ t: p.t,
4783
+ text: p.text.slice(0, cut - n)
4784
+ });
4785
+ tail.push({
4786
+ t: p.t,
4787
+ text: p.text.slice(cut - n)
4788
+ });
4789
+ }
4790
+ n += p.text.length;
4791
+ }
4792
+ this.open = head;
4793
+ this.close();
4794
+ this.open = tail;
4795
+ return true;
4796
+ }
4763
4797
  close() {
4764
4798
  const text = this.text.trim();
4765
4799
  if (text) this.done.push({
4766
4800
  text,
4767
4801
  start: this.open[0].t,
4768
- end: this.open.at(-1).t
4802
+ end: this.open.at(-1).t,
4803
+ wall: Date.now()
4769
4804
  });
4770
4805
  this.open = [];
4771
4806
  }
@@ -4791,6 +4826,11 @@ var OpenAITranslateProvider = class {
4791
4826
  reconnects = 0;
4792
4827
  /** Audio sent so far (s); a session's positions count from `base`. */
4793
4828
  sent = 0;
4829
+ /** Audio waiting to be sent: the service listens in real time, so bursts (a recording read from its
4830
+ * start) go up at most a little faster than that. */
4831
+ queue = [];
4832
+ queued = 0;
4833
+ drain;
4794
4834
  base = 0;
4795
4835
  src = new Stream(false);
4796
4836
  tr;
@@ -4801,6 +4841,8 @@ var OpenAITranslateProvider = class {
4801
4841
  seq = 0;
4802
4842
  /** End of the last finished line (s): everything before it is on screen. */
4803
4843
  readyAt = 0;
4844
+ /** Where the last line ends on screen (lines never overlap). */
4845
+ lastEnd = 0;
4804
4846
  /** Arrival delay of text after its audio position, for the page (s). */
4805
4847
  lag = 0;
4806
4848
  connectedAt = 0;
@@ -4821,6 +4863,7 @@ var OpenAITranslateProvider = class {
4821
4863
  this.onError = onError;
4822
4864
  await this.connect();
4823
4865
  this.quiet = setInterval(() => this.settle(), 400);
4866
+ this.drain = setInterval(() => this.sendDue(), 100);
4824
4867
  }
4825
4868
  connect() {
4826
4869
  return new Promise((resolve, reject) => {
@@ -4874,35 +4917,52 @@ var OpenAITranslateProvider = class {
4874
4917
  }
4875
4918
  push(pcm) {
4876
4919
  if (this.stopped) return;
4877
- this.send({
4878
- type: "session.input_audio_buffer.append",
4879
- audio: this.up.push(Buffer.from(pcm)).toString("base64")
4880
- });
4881
- this.sent += pcm.length / BYTES_PER_SEC;
4920
+ this.queue.push(Buffer.from(pcm));
4921
+ this.queued += pcm.length / BYTES_PER_SEC;
4922
+ this.sendDue();
4923
+ }
4924
+ /** Send queued audio, everything including silence, no faster than 1.5× real time since the session began. */
4925
+ sendDue() {
4926
+ if (this.ws?.readyState !== WebSocket.OPEN) return;
4927
+ const allowed = this.base + (Date.now() - this.connectedAt) / 1e3 * 1.5 + 2;
4928
+ while (this.queue.length && this.sent < allowed) {
4929
+ const pcm = this.queue.shift();
4930
+ this.queued -= pcm.length / BYTES_PER_SEC;
4931
+ this.send({
4932
+ type: "session.input_audio_buffer.append",
4933
+ audio: this.up.push(pcm).toString("base64")
4934
+ });
4935
+ this.sent += pcm.length / BYTES_PER_SEC;
4936
+ }
4882
4937
  }
4883
- /** Audio sent but not yet in a finished line. */
4938
+ /** Audio received but not yet in a finished line (queued, or sent and still being worked on). */
4884
4939
  backlog() {
4885
- return Math.max(0, this.sent - this.readyAt);
4940
+ return this.queued + Math.max(0, this.sent - this.readyAt);
4886
4941
  }
4887
4942
  onEvent(ev) {
4888
4943
  if (ev.type === "session.input_transcript.delta" || ev.type === "session.output_transcript.delta") {
4889
4944
  const t = this.base + (ev.elapsed_ms ?? 0) / 1e3;
4890
- const arrived = this.base + (Date.now() - this.connectedAt) / 1e3;
4891
- this.lag = this.lag ? this.lag * .9 + (arrived - t) * .1 : arrived - t;
4945
+ const behind = Math.max(0, this.sent - t);
4946
+ this.lag = this.lag ? this.lag * .9 + behind * .1 : behind;
4892
4947
  const piece = {
4893
4948
  t,
4894
4949
  text: String(ev.delta ?? "")
4895
4950
  };
4896
4951
  if (ev.type === "session.input_transcript.delta") this.src.add(piece);
4897
- else this.tr.add(piece);
4952
+ else {
4953
+ const next = this.src.done[this.srcUsed];
4954
+ if (next && this.tr.text.trim() && t - next.end >= 1.2) {
4955
+ if (!this.tr.closeAtPause() && t - next.end >= 3) this.tr.close();
4956
+ }
4957
+ this.tr.add(piece);
4958
+ }
4898
4959
  this.pair(false);
4899
4960
  this.draft();
4900
4961
  } else if (ev.type === "error") this.onError?.(/* @__PURE__ */ new Error(`OpenAI: ${ev.error?.message ?? "error"}`));
4901
4962
  }
4902
4963
  /** Close streams that went quiet (a pause without punctuation), then pair what is complete. */
4903
4964
  settle() {
4904
- const now = this.sent;
4905
- for (const s of [this.src, this.tr]) if (s.last !== void 0 && now - s.last > QUIET_SEC + this.lag) s.close();
4965
+ for (const s of [this.src, this.tr]) if (s.last !== void 0 && Date.now() - s.lastWall > QUIET_SEC * 1e3) s.close();
4906
4966
  this.pair(false);
4907
4967
  this.draft();
4908
4968
  }
@@ -4913,7 +4973,10 @@ var OpenAITranslateProvider = class {
4913
4973
  */
4914
4974
  pair(all) {
4915
4975
  while (this.tr.done.length) {
4916
- const t = this.tr.done.shift();
4976
+ const t = this.tr.done[0];
4977
+ const srcReady = this.srcUsed < this.src.done.length && this.src.done[this.srcUsed].end <= t.end + .5;
4978
+ if (!all && !srcReady && this.src.text.trim() && Date.now() - t.wall < 1500) break;
4979
+ this.tr.done.shift();
4917
4980
  const want = Array.from(t.text).length * this.ratio;
4918
4981
  const src = [];
4919
4982
  let len = 0;
@@ -4936,28 +4999,37 @@ var OpenAITranslateProvider = class {
4936
4999
  }
4937
5000
  line(t, src) {
4938
5001
  const srcText = src.map((s) => s.text).join(" ").trim();
4939
- const start = Math.max(0, Math.min(t?.start ?? Infinity, src[0]?.start ?? Infinity) - .4);
4940
- const end = Math.max(t?.end ?? 0, src.at(-1)?.end ?? 0);
5002
+ const spoken = src.length ? {
5003
+ start: src[0].start - .6,
5004
+ end: src.at(-1).end - .2
5005
+ } : {
5006
+ start: (t?.start ?? 0) - 1.4,
5007
+ end: (t?.end ?? 0) - .8
5008
+ };
5009
+ const start = Math.max(0, this.lastEnd, spoken.start);
5010
+ const end = Math.max(start + 1, spoken.end);
5011
+ this.lastEnd = end;
4941
5012
  this.emit?.({
4942
5013
  key: `ot:${this.seq++}`,
4943
5014
  start,
4944
- end: Math.max(end, start + .8),
5015
+ end,
4945
5016
  src: srcText,
4946
5017
  tr: t ? { [this.opts.target]: t.text } : void 0,
4947
5018
  final: true
4948
5019
  });
4949
- this.readyAt = Math.max(this.readyAt, end);
5020
+ this.readyAt = Math.max(this.readyAt, t?.end ?? end, end);
4950
5021
  }
4951
5022
  /** The sentence being translated, as a draft line. */
4952
5023
  draft() {
4953
5024
  const tr = this.tr.text.trim();
4954
5025
  if (!tr) return;
4955
5026
  const srcText = [...this.src.done.slice(this.srcUsed).map((s) => s.text), this.src.text.trim()].filter(Boolean).join(" ");
4956
- const start = Math.max(0, Math.min(this.tr.start ?? Infinity, this.src.done[this.srcUsed]?.start ?? this.src.start ?? Infinity) - .4);
5027
+ const from = this.src.done[this.srcUsed]?.start ?? this.src.start;
5028
+ const start = Math.max(0, this.lastEnd, from !== void 0 ? from - .6 : (this.tr.start ?? 0) - 1.4);
4957
5029
  this.emit?.({
4958
5030
  key: `ot:${this.seq}`,
4959
5031
  start,
4960
- end: Math.max(this.tr.last ?? start, start + .8),
5032
+ end: Math.max(start + 1, (this.src.last ?? this.tr.last ?? start) - .2),
4961
5033
  src: srcText,
4962
5034
  tr: { [this.opts.target]: tr },
4963
5035
  final: false
@@ -4970,6 +5042,7 @@ var OpenAITranslateProvider = class {
4970
5042
  async stop() {
4971
5043
  await new Promise((r) => setTimeout(r, 2500));
4972
5044
  if (this.quiet) clearInterval(this.quiet);
5045
+ if (this.drain) clearInterval(this.drain);
4973
5046
  this.src.close();
4974
5047
  this.tr.close();
4975
5048
  this.pair(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vidi",
3
- "version": "0.7.1",
3
+ "version": "0.7.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",