dsh-vidi 0.7.2 → 0.7.4

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
@@ -35188,6 +35188,80 @@ function upload(file, onProgress) {
35188
35188
  });
35189
35189
  }
35190
35190
  /** A value to copy: plain text (not an input look-alike) and a text button. */
35191
+ /** The push stream key: copy it, or change it (typed, or a new random one). */
35192
+ function KeyRow({ value, t, onChanged }) {
35193
+ const [edit, setEdit] = (0, react.useState)(null);
35194
+ const [err, setErr] = (0, react.useState)("");
35195
+ const [done, setDone] = (0, react.useState)(false);
35196
+ const save = async (body) => {
35197
+ if (typeof body.key === "string" && !/^[A-Za-z0-9_-]{3,40}$/.test(body.key.trim())) {
35198
+ setErr(t("push.badKey"));
35199
+ return;
35200
+ }
35201
+ try {
35202
+ await post("pushkey", body);
35203
+ setEdit(null);
35204
+ setErr("");
35205
+ onChanged();
35206
+ } catch (e) {
35207
+ setErr(e.message);
35208
+ }
35209
+ };
35210
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("push.key") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [edit === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
35211
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35212
+ className: "val",
35213
+ children: value
35214
+ }),
35215
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35216
+ className: "dshl-link",
35217
+ onClick: () => {
35218
+ navigator.clipboard.writeText(value);
35219
+ setDone(true);
35220
+ setTimeout(() => setDone(false), 1500);
35221
+ },
35222
+ children: done ? t("push.copied") : t("push.copy")
35223
+ }),
35224
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35225
+ className: "dshl-link",
35226
+ onClick: () => setEdit(value),
35227
+ children: t("push.change")
35228
+ })
35229
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
35230
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35231
+ className: "dshl-keyedit",
35232
+ value: edit,
35233
+ spellCheck: false,
35234
+ autoFocus: true,
35235
+ onFocus: (e) => e.target.select(),
35236
+ onChange: (e) => setEdit(e.target.value),
35237
+ onKeyDown: (e) => {
35238
+ if (e.key === "Enter") save({ key: edit });
35239
+ if (e.key === "Escape") setEdit(null);
35240
+ }
35241
+ }),
35242
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35243
+ className: "dshl-link",
35244
+ onClick: () => void save({ key: edit }),
35245
+ children: t("push.save")
35246
+ }),
35247
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35248
+ className: "dshl-link",
35249
+ onClick: () => void save({ random: true }),
35250
+ children: t("push.random")
35251
+ }),
35252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35253
+ className: "dshl-link",
35254
+ onClick: () => {
35255
+ setEdit(null);
35256
+ setErr("");
35257
+ },
35258
+ children: t("player.cancel")
35259
+ })
35260
+ ] }), err && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35261
+ className: "err",
35262
+ children: err
35263
+ })] })] });
35264
+ }
35191
35265
  function CopyRow({ label, value, copy, copied }) {
35192
35266
  const [done, setDone] = (0, react.useState)(false);
35193
35267
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -35781,11 +35855,10 @@ function LiveView(props) {
35781
35855
  value: armed.rtmp,
35782
35856
  copy: t("push.copy"),
35783
35857
  copied: t("push.copied")
35784
- }), armed.key && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyRow, {
35785
- label: t("push.key"),
35858
+ }), armed.key && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyRow, {
35786
35859
  value: armed.key,
35787
- copy: t("push.copy"),
35788
- copied: t("push.copied")
35860
+ t,
35861
+ onChanged: () => void post("arm", options()).then((a) => setArmed(a.busy ? null : a)).catch(() => {})
35789
35862
  })] })]
35790
35863
  })
35791
35864
  ]
@@ -36878,6 +36951,10 @@ const en = {
36878
36951
  "rs.overlay": "Subtitles for OBS (Browser Source)",
36879
36952
  "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.",
36880
36953
  "rs.copy": "Copy",
36954
+ "push.change": "Change",
36955
+ "push.save": "Save",
36956
+ "push.random": "New random key",
36957
+ "push.badKey": "3–40 letters, digits, - or _",
36881
36958
  "side.hide": "Hide the chat",
36882
36959
  "side.show": "Show the chat",
36883
36960
  "side.resize": "Drag to change the width",
@@ -37262,6 +37339,10 @@ const zh = {
37262
37339
  "rs.overlay": "OBS 字幕叠加(浏览器源)",
37263
37340
  "rs.overlay.d": "在 OBS 里加一个「浏览器源」,填这个地址(1920×1080):透明背景上只有最新的字幕,叠在摄像头或任何画面上。地址后加 ?lines=zh,src&size=4.5 可以选行和字号。",
37264
37341
  "rs.copy": "复制",
37342
+ "push.change": "修改",
37343
+ "push.save": "保存",
37344
+ "push.random": "换一个",
37345
+ "push.badKey": "用 3–40 位字母、数字、- 或 _",
37265
37346
  "side.hide": "收起对话",
37266
37347
  "side.show": "展开对话",
37267
37348
  "side.resize": "拖动调整宽度",
@@ -37761,6 +37842,10 @@ const CSS = `
37761
37842
  .dshl-pushinfo dl { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 6px 16px; margin: 0; }
37762
37843
  .dshl-pushinfo dt { color: var(--faint); font-size: 12px; }
37763
37844
  .dshl-pushinfo dd { display: flex; align-items: center; gap: 14px; min-width: 0; margin: 0; }
37845
+ .dshl-keyedit { width: 140px; height: 26px; padding: 0 8px; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--ink); font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; }
37846
+ .dshl-keyedit:focus { outline: none; border-color: var(--accent); }
37847
+ .dshl-pushinfo dd:has(.err) { flex-wrap: wrap; row-gap: 4px; }
37848
+ .dshl-pushinfo dd .err { flex-basis: 100%; color: #d1434b; font-size: 12px; }
37764
37849
  .dshl-pushinfo .val { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; user-select: all; }
37765
37850
  .dshl-pushinfo .dshl-link { flex: none; font-size: 12.5px; }
37766
37851
 
package/lib/index.mjs CHANGED
@@ -2289,11 +2289,28 @@ var LiveJob = class LiveJob extends EventEmitter {
2289
2289
  }
2290
2290
  }, 400);
2291
2291
  }
2292
- ff.stderr.on("data", (d) => appendFileSync(join(this.deps.dir, "ffmpeg.log"), d));
2292
+ let rejected = false;
2293
+ ff.stderr.on("data", (d) => {
2294
+ appendFileSync(join(this.deps.dir, "ffmpeg.log"), d);
2295
+ if (this.opts.source === "push" && !rejected && /Unexpected stream/.test(d.toString())) {
2296
+ rejected = true;
2297
+ ff.kill("SIGKILL");
2298
+ }
2299
+ });
2293
2300
  ff.on("exit", (code) => {
2294
2301
  if (this.ffmpeg !== ff) return;
2295
2302
  this.ffmpeg = void 0;
2296
2303
  if (this.status !== "running") return;
2304
+ if (this.opts.source === "push" && this.message && (rejected || !this.segments().length)) {
2305
+ if (rejected) rmSync(hls, {
2306
+ recursive: true,
2307
+ force: true
2308
+ });
2309
+ setTimeout(() => {
2310
+ if (this.status === "running" && !this.ffmpeg) this.spawnFfmpeg(input);
2311
+ }, 500);
2312
+ return;
2313
+ }
2297
2314
  this.finishCapture(code === 0 ? "ended" : this.ffmpegError(code), code === 0 || this.segments().length > 0);
2298
2315
  });
2299
2316
  }
@@ -4715,6 +4732,8 @@ const HARD_CJK = 34;
4715
4732
  const HARD_LATIN = 110;
4716
4733
  /** A stream quiet this long ends its sentence (a pause without punctuation). */
4717
4734
  const QUIET_SEC = 1.6;
4735
+ /** …or this long when it is only a few words so far. */
4736
+ const QUIET_SHORT_SEC = 3.5;
4718
4737
  /**
4719
4738
  * Builds sentences from one text stream. The service stamps every delta
4720
4739
  * with the audio position it was produced at (elapsed_ms); a sentence's
@@ -4796,7 +4815,13 @@ var Stream = class {
4796
4815
  }
4797
4816
  close() {
4798
4817
  const text = this.text.trim();
4799
- if (text) this.done.push({
4818
+ const prev = this.done.at(-1);
4819
+ if (text && !/[\p{L}\p{N}]/u.test(text)) {
4820
+ if (prev) {
4821
+ prev.text += text;
4822
+ prev.end = Math.max(prev.end, this.open.at(-1).t);
4823
+ }
4824
+ } else if (text) this.done.push({
4800
4825
  text,
4801
4826
  start: this.open[0].t,
4802
4827
  end: this.open.at(-1).t,
@@ -4804,6 +4829,11 @@ var Stream = class {
4804
4829
  });
4805
4830
  this.open = [];
4806
4831
  }
4832
+ /** Long enough to stand as a line of its own (when cutting without punctuation). */
4833
+ get substantial() {
4834
+ const text = this.text.trim();
4835
+ return (this.cjk ? Array.from(text).length : text.length) >= (this.cjk ? 10 : 24);
4836
+ }
4807
4837
  };
4808
4838
  /**
4809
4839
  * OpenAI live interpretation (gpt-realtime-translate): audio streams in, the
@@ -4952,7 +4982,7 @@ var OpenAITranslateProvider = class {
4952
4982
  else {
4953
4983
  const next = this.src.done[this.srcUsed];
4954
4984
  if (next && this.tr.text.trim() && t - next.end >= 1.2) {
4955
- if (!this.tr.closeAtPause() && t - next.end >= 3) this.tr.close();
4985
+ if (!this.tr.closeAtPause() && t - next.end >= 3 && this.tr.substantial) this.tr.close();
4956
4986
  }
4957
4987
  this.tr.add(piece);
4958
4988
  }
@@ -4962,7 +4992,7 @@ var OpenAITranslateProvider = class {
4962
4992
  }
4963
4993
  /** Close streams that went quiet (a pause without punctuation), then pair what is complete. */
4964
4994
  settle() {
4965
- for (const s of [this.src, this.tr]) if (s.last !== void 0 && Date.now() - s.lastWall > QUIET_SEC * 1e3) s.close();
4995
+ for (const s of [this.src, this.tr]) if (s.last !== void 0 && Date.now() - s.lastWall > (s.substantial ? QUIET_SEC : QUIET_SHORT_SEC) * 1e3) s.close();
4966
4996
  this.pair(false);
4967
4997
  this.draft();
4968
4998
  }
@@ -5880,7 +5910,14 @@ function apply(ctx, config) {
5880
5910
  * page owns it.
5881
5911
  */
5882
5912
  let armed;
5883
- const PUSH_KEY = "dsh";
5913
+ const hadSettings = existsSync(settingsFile);
5914
+ const pushKey = () => {
5915
+ const saved = prefs().pushKey;
5916
+ if (saved) return saved;
5917
+ const key = hadSettings ? "dsh" : Math.random().toString(36).slice(2, 10);
5918
+ savePrefs({ pushKey: key });
5919
+ return key;
5920
+ };
5884
5921
  async function disarm() {
5885
5922
  const a = armed;
5886
5923
  armed = void 0;
@@ -5952,7 +5989,7 @@ function apply(ctx, config) {
5952
5989
  host: lanAddress(),
5953
5990
  rtmpPort: config.rtmpPort,
5954
5991
  srtPort: config.srtPort,
5955
- key: waiting ? PUSH_KEY : void 0
5992
+ key: waiting ? pushKey() : void 0
5956
5993
  },
5957
5994
  cookies: () => prefs().cookies
5958
5995
  };
@@ -6182,6 +6219,15 @@ function apply(ctx, config) {
6182
6219
  }
6183
6220
  return send(res, 200, job.summary());
6184
6221
  }
6222
+ if (route === "pushkey" && req.method === "POST") {
6223
+ if ([...jobs.values()].some((j) => j.opts.source === "push" && j.status === "running" && !j.message)) return send(res, 409, { error: "a stream is coming in; change the key after it ends" });
6224
+ const b = await readJson(req);
6225
+ const key = b.random ? Math.random().toString(36).slice(2, 10) : String(b.key ?? "").trim();
6226
+ if (!/^[A-Za-z0-9_-]{3,40}$/.test(key)) return send(res, 400, { error: "use 3–40 letters, digits, - or _" });
6227
+ savePrefs({ pushKey: key });
6228
+ await disarm();
6229
+ return send(res, 200, { key });
6230
+ }
6185
6231
  if (route === "arm" && req.method === "POST") {
6186
6232
  const b = await readJson(req);
6187
6233
  return send(res, 200, await arm({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vidi",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
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",