dsh-vidi 0.7.0 → 0.7.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 CHANGED
@@ -34066,10 +34066,21 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
34066
34066
  ]
34067
34067
  }),
34068
34068
  openMenu && (() => {
34069
+ const phone = narrow && window.innerWidth < 700 && !full && !pageFull;
34070
+ const floating = narrow && !phone && !full && !pageFull;
34071
+ const r = floating ? stage.current?.getBoundingClientRect() : void 0;
34072
+ const place = r ? {
34073
+ position: "fixed",
34074
+ right: Math.max(8, window.innerWidth - r.right + 8),
34075
+ bottom: Math.max(8, window.innerHeight - r.bottom + 56),
34076
+ width: Math.min(360, window.innerWidth - 16),
34077
+ maxHeight: Math.max(240, r.bottom - 56 - 12)
34078
+ } : void 0;
34069
34079
  const panel = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34070
- className: `dshl-menu ${narrow ? "sheet" : ""}`,
34080
+ className: `dshl-menu ${phone ? "sheet" : ""}`,
34071
34081
  role: "dialog",
34072
34082
  "aria-label": openMenu.title,
34083
+ style: place,
34073
34084
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34074
34085
  className: "head",
34075
34086
  children: [
@@ -34094,13 +34105,13 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
34094
34105
  children: openMenu.content
34095
34106
  })]
34096
34107
  });
34097
- return narrow && !full && !pageFull ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
34108
+ return phone ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
34098
34109
  className: "dshl-sheetwrap",
34099
34110
  onMouseDown: (e) => {
34100
34111
  if (e.target === e.currentTarget) setMenu(null);
34101
34112
  },
34102
34113
  children: panel
34103
- }), document.body) : panel;
34114
+ }), document.body) : floating ? (0, react_dom.createPortal)(panel, document.body) : panel;
34104
34115
  })(),
34105
34116
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34106
34117
  className: "dshl-controls",
@@ -34628,38 +34639,49 @@ function RestreamPanel(props) {
34628
34639
  ]
34629
34640
  });
34630
34641
  }
34631
- /** The subtitle page for OBS's Browser Source: lay the subtitles over your own picture without encoding anything. */
34642
+ /** Subtitle pages: the overlay for OBS's Browser Source, and the big screen of subtitles only. */
34632
34643
  function OverlayLink({ t }) {
34633
- const [url, setUrl] = (0, react.useState)("");
34634
- const [copied, setCopied] = (0, react.useState)(false);
34644
+ const [urls, setUrls] = (0, react.useState)({});
34645
+ const [copied, setCopied] = (0, react.useState)("");
34635
34646
  (0, react.useEffect)(() => {
34636
- fetch("/live/api/overlay").then((r) => r.json()).then((j) => setUrl(j.url ?? "")).catch(() => {});
34647
+ fetch("/live/api/overlay").then((r) => r.json()).then(setUrls).catch(() => {});
34637
34648
  }, []);
34638
- if (!url) return null;
34639
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34649
+ if (!urls.url) return null;
34650
+ const row = (label, url, note, open) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34640
34651
  className: "rs-overlay",
34641
34652
  children: [
34642
34653
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34643
34654
  className: "rs-sub",
34644
- children: t("rs.overlay")
34655
+ children: label
34645
34656
  }),
34646
34657
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
34647
34658
  className: "rs-url",
34648
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: url }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
34649
- className: "dshl-link",
34650
- onClick: () => void navigator.clipboard.writeText(url).then(() => {
34651
- setCopied(true);
34652
- setTimeout(() => setCopied(false), 1500);
34659
+ children: [
34660
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: url }),
34661
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
34662
+ className: "dshl-link",
34663
+ onClick: () => void navigator.clipboard.writeText(url).then(() => {
34664
+ setCopied(url);
34665
+ setTimeout(() => setCopied(""), 1500);
34666
+ }),
34667
+ children: copied === url ? t("tr.copied") : t("rs.copy")
34653
34668
  }),
34654
- children: copied ? t("tr.copied") : t("rs.copy")
34655
- })]
34669
+ open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
34670
+ className: "dshl-link",
34671
+ href: url,
34672
+ target: "_blank",
34673
+ rel: "noreferrer",
34674
+ children: t("rs.openPage")
34675
+ })
34676
+ ]
34656
34677
  }),
34657
34678
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
34658
34679
  className: "rs-hint",
34659
- children: t("rs.overlay.d")
34680
+ children: note
34660
34681
  })
34661
34682
  ]
34662
34683
  });
34684
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [row(t("rs.overlay"), urls.url, t("rs.overlay.d")), urls.screen && row(t("rs.screen"), urls.screenLan ?? urls.screen, t("rs.screen.d").replace("{local}", urls.screen), true)] });
34663
34685
  }
34664
34686
 
34665
34687
  // src/client/Settings.tsx
@@ -36299,7 +36321,7 @@ function SubsStarter(props) {
36299
36321
  const box = (0, react.useRef)(null);
36300
36322
  const pop = (0, react.useRef)(null);
36301
36323
  const toggle = () => {
36302
- setSheet((box.current?.closest(".dshl-stage")?.clientWidth ?? 999) < 560 && !document.fullscreenElement);
36324
+ setSheet(window.innerWidth < 700 && !document.fullscreenElement);
36303
36325
  setOpen((v) => !v);
36304
36326
  };
36305
36327
  (0, react.useEffect)(() => {
@@ -36801,6 +36823,9 @@ const en = {
36801
36823
  "rs.overlay": "Subtitles for OBS (Browser Source)",
36802
36824
  "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.",
36803
36825
  "rs.copy": "Copy",
36826
+ "rs.openPage": "Open",
36827
+ "rs.screen": "Subtitle screen (16:9)",
36828
+ "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.",
36804
36829
  "rs.content": "Sends",
36805
36830
  "rs.c.burn": "Picture + subtitles",
36806
36831
  "rs.c.clean": "Picture only",
@@ -37179,6 +37204,9 @@ const zh = {
37179
37204
  "rs.overlay": "OBS 字幕叠加(浏览器源)",
37180
37205
  "rs.overlay.d": "在 OBS 里加一个「浏览器源」,填这个地址(1920×1080):透明背景上只有最新的字幕,叠在摄像头或任何画面上。地址后加 ?lines=zh,src&size=4.5 可以选行和字号。",
37181
37206
  "rs.copy": "复制",
37207
+ "rs.openPage": "打开",
37208
+ "rs.screen": "字幕大屏(16:9)",
37209
+ "rs.screen.d": "只有字幕、字很大,适合大屏和投影:在同一网络里任何浏览器打开、全屏即可(本机用 {local})。地址后加 ?theme=light 换浅色;也可以作为 OBS 的一个独立画面来源。",
37182
37210
  "rs.content": "内容",
37183
37211
  "rs.c.burn": "画面 + 字幕",
37184
37212
  "rs.c.clean": "只要画面",
@@ -37966,8 +37994,9 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
37966
37994
  .dshl-dl .dshl-chips .chip { background: transparent; }
37967
37995
 
37968
37996
  /* Menus as a bottom sheet on small players (phones) */
37969
- .dshl-sheetwrap { position: fixed; inset: 0; z-index: 2147483002; display: flex; align-items: flex-end; background: rgba(0,0,0,.4); }
37970
- .dshl-menu.sheet { position: relative; right: auto; bottom: auto; width: 100%; max-height: 78vh; padding-bottom: env(safe-area-inset-bottom); border: 0; border-radius: 16px 16px 0 0;
37997
+ .dshl-sheetwrap { position: fixed; inset: 0; z-index: 2147483002; display: flex; align-items: flex-end; justify-content: center; background: rgba(0,0,0,.4); }
37998
+ body > .dshl-menu { z-index: 2147483002; }
37999
+ .dshl-menu.sheet { position: relative; right: auto; bottom: auto; width: 100%; max-width: 560px; max-height: 78vh; padding-bottom: env(safe-area-inset-bottom); border: 0; border-radius: 16px 16px 0 0;
37971
38000
  font-family: system-ui, -apple-system, "PingFang SC", sans-serif; font-size: 15px; }
37972
38001
  .dshl-menu.sheet .head { padding: 14px 12px 8px 18px; font-size: 16px; }
37973
38002
  .dshl-menu.sheet .body { padding: 4px 14px 16px; }
package/lib/index.mjs CHANGED
@@ -5046,11 +5046,19 @@ function startOverlay(port, current, onJob, log) {
5046
5046
  res.end(PAGE);
5047
5047
  return;
5048
5048
  }
5049
+ if (url.pathname === "/screen" || url.pathname === "/screen/") {
5050
+ res.writeHead(200, {
5051
+ "content-type": "text/html; charset=utf-8",
5052
+ "cache-control": "no-store"
5053
+ });
5054
+ res.end(SCREEN);
5055
+ return;
5056
+ }
5049
5057
  res.writeHead(404);
5050
5058
  res.end();
5051
5059
  });
5052
5060
  server.on("error", (err) => log(`overlay: port ${port} unavailable (${err.message})`));
5053
- server.listen(port, "127.0.0.1", () => log(`overlay: http://127.0.0.1:${port}/overlay`));
5061
+ server.listen(port, () => log(`overlay: http://127.0.0.1:${port}/overlay, /screen`));
5054
5062
  return () => {
5055
5063
  offJob();
5056
5064
  detach?.();
@@ -5098,6 +5106,63 @@ function connect() {
5098
5106
  }
5099
5107
  connect()
5100
5108
  <\/script></body></html>`;
5109
+ /**
5110
+ * Subtitles as the whole picture, for a big screen (a hall, a projector, a
5111
+ * second monitor, or OBS as a source of its own): 16:9, the current line large
5112
+ * with the original under it, the two before it smaller and dimmed above.
5113
+ * …/screen?lines=zh,src&theme=dark|light
5114
+ */
5115
+ const SCREEN = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vidi · subtitles</title>
5116
+ <style>
5117
+ html,body{margin:0;height:100%;background:#000;overflow:hidden;cursor:none}
5118
+ body.light{background:#f4f3ef}
5119
+ #stage{position:absolute;inset:0;margin:auto;width:min(100vw,177.78vh);height:min(56.25vw,100vh);display:flex;flex-direction:column;justify-content:flex-end;box-sizing:border-box;padding:4% 6% 7%;
5120
+ font-family:"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans CJK SC",system-ui,sans-serif;color:#fff;container-type:inline-size}
5121
+ body.light #stage{color:#151513}
5122
+ .cue{text-align:center;text-wrap:balance;line-break:strict;transition:opacity .4s,font-size .3s}
5123
+ .cue+.cue{margin-top:3.2cqw}
5124
+ .cue .main{display:block;font-weight:600;line-height:1.35}
5125
+ .cue .sub{display:block;margin-top:.8cqw;opacity:.62;line-height:1.4}
5126
+ .cue.now .main{font-size:5.4cqw}.cue.now .sub{font-size:2.9cqw}
5127
+ .cue.old .main{font-size:3cqw}.cue.old .sub{font-size:1.9cqw}
5128
+ .cue.old{opacity:.38}.cue.old2{opacity:.2}
5129
+ .cue.draft .main{opacity:.75}
5130
+ </style></head><body><div id="stage"></div>
5131
+ <script>
5132
+ const q = new URLSearchParams(location.search)
5133
+ const lines = (q.get('lines') || 'zh,src').split(',')
5134
+ if (q.get('theme') === 'light') document.body.classList.add('light')
5135
+ const stage = document.getElementById('stage')
5136
+ let done = [], draft = null
5137
+ function texts(c) {
5138
+ const seen = new Set()
5139
+ return lines.map(k => (k === 'src' ? c.src : (c.tr || {})[k]) || '').map(s => s.trim()).filter(s => s && !seen.has(s) && seen.add(s))
5140
+ }
5141
+ function render() {
5142
+ const list = [...done.slice(-3), ...(draft ? [draft] : [])].slice(-3)
5143
+ stage.innerHTML = ''
5144
+ list.forEach((c, i) => {
5145
+ const t = texts(c); if (!t.length) return
5146
+ const d = document.createElement('div')
5147
+ const age = list.length - 1 - i
5148
+ d.className = 'cue ' + (age === 0 ? 'now' : age === 1 ? 'old' : 'old old2') + (c.final ? '' : ' draft')
5149
+ const m = document.createElement('span'); m.className = 'main'; m.textContent = t[0]; d.appendChild(m)
5150
+ if (t[1]) { const s = document.createElement('span'); s.className = 'sub'; s.textContent = t[1]; d.appendChild(s) }
5151
+ stage.appendChild(d)
5152
+ })
5153
+ }
5154
+ function connect() {
5155
+ const es = new EventSource('/overlay/events')
5156
+ es.onmessage = e => {
5157
+ let c; try { c = JSON.parse(e.data) } catch { return }
5158
+ if (!c) return
5159
+ if (c.final) { draft = null; if (!done.length || done.at(-1).src !== c.src) done.push(c); done = done.slice(-6) } else draft = c
5160
+ render()
5161
+ }
5162
+ es.onerror = () => { es.close(); setTimeout(connect, 2000) }
5163
+ }
5164
+ connect()
5165
+ <\/script></body></html>`;
5101
5166
  // src/tools.ts
5102
5167
  const CLOCK = (s) => {
5103
5168
  const h = Math.floor(s / 3600), m = Math.floor(s / 60) % 60, ss = Math.floor(s % 60);
@@ -5865,7 +5930,11 @@ function apply(ctx, config) {
5865
5930
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
5866
5931
  const route = url.pathname.replace(/^\/live\/api\/?/, "");
5867
5932
  try {
5868
- if (route === "overlay") return send(res, 200, { url: `http://127.0.0.1:${config.overlayPort}/overlay` });
5933
+ if (route === "overlay") return send(res, 200, {
5934
+ url: `http://127.0.0.1:${config.overlayPort}/overlay`,
5935
+ screen: `http://127.0.0.1:${config.overlayPort}/screen`,
5936
+ screenLan: `http://${lanAddress()}:${config.overlayPort}/screen`
5937
+ });
5869
5938
  if (route === "deps") {
5870
5939
  const [deps] = await Promise.all([checkDeps(config.whisperModel), keys.refresh()]);
5871
5940
  localReady = deps.filter((d) => [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vidi",
3
- "version": "0.7.0",
3
+ "version": "0.7.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",